diff --git a/communications/slack/slack.go b/communications/slack/slack.go index 3627455c..e6dad7fb 100644 --- a/communications/slack/slack.go +++ b/communications/slack/slack.go @@ -175,7 +175,7 @@ func (s *Slack) NewConnection() error { // WebsocketConnect creates a websocket dialer amd initiates a websocket // connection func (s *Slack) WebsocketConnect() error { - var Dialer websocket.Dialer + var dialer websocket.Dialer var err error websocketURL := s.Details.URL @@ -183,7 +183,7 @@ func (s *Slack) WebsocketConnect() error { websocketURL = s.ReconnectURL } - s.WebsocketConn, _, err = Dialer.Dial(websocketURL, http.Header{}) + s.WebsocketConn, _, err = dialer.Dial(websocketURL, http.Header{}) if err != nil { return err } diff --git a/config/README.md b/config/README.md index 2fe3b510..40fa38c2 100644 --- a/config/README.md +++ b/config/README.md @@ -82,7 +82,9 @@ have multiple deposit accounts for different FIAT deposit currencies. "Websocket": false, "UseSandbox": false, "RESTPollingDelay": 10, - "HTTPTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "httpTimeout": 15000000000, "AuthenticatedAPISupport": false, "APIKey": "Key", "APISecret": "Secret", diff --git a/config/config.go b/config/config.go index 98b2996c..5cebc182 100644 --- a/config/config.go +++ b/config/config.go @@ -26,18 +26,20 @@ import ( // Constants declared here are filename strings and test strings const ( - FXProviderFixer = "fixer" - EncryptedConfigFile = "config.dat" - ConfigFile = "config.json" - ConfigTestFile = "../testdata/configtest.json" - configFileEncryptionPrompt = 0 - configFileEncryptionEnabled = 1 - configFileEncryptionDisabled = -1 - configPairsLastUpdatedWarningThreshold = 30 // 30 days - configDefaultHTTPTimeout = time.Second * 15 - configMaxAuthFailres = 3 - defaultNTPAllowedDifference = 50000000 - defaultNTPAllowedNegativeDifference = 50000000 + FXProviderFixer = "fixer" + EncryptedConfigFile = "config.dat" + ConfigFile = "config.json" + ConfigTestFile = "../testdata/configtest.json" + configFileEncryptionPrompt = 0 + configFileEncryptionEnabled = 1 + configFileEncryptionDisabled = -1 + configPairsLastUpdatedWarningThreshold = 30 // 30 days + configDefaultHTTPTimeout = time.Second * 15 + configDefaultWebsocketResponseCheckTimeout = time.Millisecond * 30 + configDefaultWebsocketResponseMaxLimit = time.Second * 7 + configMaxAuthFailres = 3 + defaultNTPAllowedDifference = 50000000 + defaultNTPAllowedNegativeDifference = 50000000 ) // Constants here hold some messages @@ -157,6 +159,8 @@ type ExchangeConfig struct { UseSandbox bool `json:"useSandbox"` RESTPollingDelay time.Duration `json:"restPollingDelay"` HTTPTimeout time.Duration `json:"httpTimeout"` + WebsocketResponseCheckTimeout time.Duration `json:"websocketResponseCheckTimeout"` + WebsocketResponseMaxLimit time.Duration `json:"websocketResponseMaxLimit"` HTTPUserAgent string `json:"httpUserAgent"` HTTPDebugging bool `json:"httpDebugging"` AuthenticatedAPISupport bool `json:"authenticatedApiSupport"` @@ -824,6 +828,16 @@ func (c *Config) CheckExchangeConfigValues() error { c.Exchanges[i].HTTPTimeout = configDefaultHTTPTimeout } + if c.Exchanges[i].WebsocketResponseCheckTimeout <= 0 { + log.Warnf("Exchange %s Websocket response check timeout value not set, defaulting to %v.", c.Exchanges[i].Name, configDefaultWebsocketResponseCheckTimeout) + c.Exchanges[i].WebsocketResponseCheckTimeout = configDefaultWebsocketResponseCheckTimeout + } + + if c.Exchanges[i].WebsocketResponseMaxLimit <= 0 { + log.Warnf("Exchange %s Websocket response max limit value not set, defaulting to %v.", c.Exchanges[i].Name, configDefaultWebsocketResponseMaxLimit) + c.Exchanges[i].WebsocketResponseMaxLimit = configDefaultWebsocketResponseMaxLimit + } + err := c.CheckPairConsistency(c.Exchanges[i].Name) if err != nil { log.Errorf("Exchange %s: CheckPairConsistency error: %s", c.Exchanges[i].Name, err) diff --git a/config/config_test.go b/config/config_test.go index 6f0f4cce..1310099c 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -650,7 +650,6 @@ func TestUpdateExchangeConfig(t *testing.T) { // TestCheckExchangeConfigValues logic test func TestCheckExchangeConfigValues(t *testing.T) { checkExchangeConfigValues := Config{} - err := checkExchangeConfigValues.LoadConfig(ConfigTestFile) if err != nil { t.Errorf( @@ -665,12 +664,28 @@ func TestCheckExchangeConfigValues(t *testing.T) { ) } + checkExchangeConfigValues.Exchanges[0].WebsocketResponseMaxLimit = 0 + checkExchangeConfigValues.Exchanges[0].WebsocketResponseCheckTimeout = 0 checkExchangeConfigValues.Exchanges[0].HTTPTimeout = 0 - checkExchangeConfigValues.CheckExchangeConfigValues() + err = checkExchangeConfigValues.CheckExchangeConfigValues() + if err != nil { + t.Errorf("Test failed. checkExchangeConfigValues.CheckExchangeConfigValues: %s", + err.Error(), + ) + } + if checkExchangeConfigValues.Exchanges[0].HTTPTimeout == 0 { t.Fatalf("Test failed. Expected exchange %s to have updated HTTPTimeout value", checkExchangeConfigValues.Exchanges[0].Name) } + if checkExchangeConfigValues.Exchanges[0].WebsocketResponseMaxLimit == 0 { + t.Fatalf("Test failed. Expected exchange %s to have updated WebsocketResponseMaxLimit value", checkExchangeConfigValues.Exchanges[0].Name) + } + + if checkExchangeConfigValues.Exchanges[0].WebsocketResponseCheckTimeout == 0 { + t.Fatalf("Test failed. Expected exchange %s to have updated WebsocketResponseCheckTimeout value", checkExchangeConfigValues.Exchanges[0].Name) + } + checkExchangeConfigValues.Exchanges[0].APIKey = "Key" checkExchangeConfigValues.Exchanges[0].APISecret = "Secret" checkExchangeConfigValues.Exchanges[0].AuthenticatedAPISupport = true diff --git a/config_example.json b/config_example.json index 621dd928..9a776f74 100644 --- a/config_example.json +++ b/config_example.json @@ -168,6 +168,8 @@ "websocket": false, "useSandbox": false, "restPollingDelay": 10, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpTimeout": 15000000000, "httpUserAgent": "", "httpDebugging": false, @@ -209,6 +211,8 @@ "websocket": false, "useSandbox": false, "restPollingDelay": 10, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpTimeout": 15000000000, "httpUserAgent": "", "httpDebugging": false, @@ -250,6 +254,8 @@ "websocket": false, "useSandbox": false, "restPollingDelay": 10, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpTimeout": 15000000000, "httpUserAgent": "", "httpDebugging": false, @@ -291,6 +297,8 @@ "websocket": false, "useSandbox": false, "restPollingDelay": 10, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpTimeout": 15000000000, "httpUserAgent": "", "httpDebugging": false, @@ -334,6 +342,8 @@ "websocket": false, "useSandbox": false, "restPollingDelay": 10, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpTimeout": 15000000000, "httpUserAgent": "", "httpDebugging": false, @@ -376,6 +386,8 @@ "websocket": false, "useSandbox": false, "restPollingDelay": 10, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpTimeout": 15000000000, "httpUserAgent": "", "httpDebugging": false, @@ -387,7 +399,7 @@ "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "proxyAddress": "", "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "XRPM19,BCHM19,ADAM19,EOSM19,TRXM19,XBTUSD,XBT7D_U105,XBT7D_D95,XBTM19,XBTU19,ETHUSD,ETHM19,LTCM19", + "availablePairs": "XRPU19,BCHU19,ADAU19,ADAU19,TRXU19,XBTUSD,XBT7D_U105,XBT7D_D95,XBTU19,XBTZ19,ETHUSD,ETHU19,LTCU19", "enabledPairs": "XBTUSD", "baseCurrencies": "USD", "assetTypes": "SPOT", @@ -417,6 +429,8 @@ "websocket": false, "useSandbox": false, "restPollingDelay": 10, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpTimeout": 15000000000, "httpUserAgent": "", "httpDebugging": false, @@ -458,6 +472,8 @@ "websocket": false, "useSandbox": false, "restPollingDelay": 10, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpTimeout": 15000000000, "httpUserAgent": "", "httpDebugging": false, @@ -500,6 +516,8 @@ "websocket": true, "useSandbox": false, "restPollingDelay": 10, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpTimeout": 15000000000, "httpUserAgent": "", "httpDebugging": false, @@ -542,6 +560,8 @@ "websocket": false, "useSandbox": false, "restPollingDelay": 10, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpTimeout": 15000000000, "httpUserAgent": "", "httpDebugging": false, @@ -583,6 +603,8 @@ "websocket": false, "useSandbox": false, "restPollingDelay": 10, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpTimeout": 15000000000, "httpUserAgent": "", "httpDebugging": false, @@ -624,6 +646,8 @@ "websocket": false, "useSandbox": false, "restPollingDelay": 10, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpTimeout": 15000000000, "httpUserAgent": "", "httpDebugging": false, @@ -667,6 +691,8 @@ "websocket": false, "useSandbox": false, "restPollingDelay": 10, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpTimeout": 15000000000, "httpUserAgent": "", "httpDebugging": false, @@ -710,6 +736,8 @@ "websocket": false, "useSandbox": false, "restPollingDelay": 10, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpTimeout": 15000000000, "httpUserAgent": "", "httpDebugging": false, @@ -753,6 +781,8 @@ "websocket": false, "useSandbox": false, "restPollingDelay": 10, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpTimeout": 15000000000, "httpUserAgent": "", "httpDebugging": false, @@ -794,6 +824,8 @@ "websocket": false, "useSandbox": false, "restPollingDelay": 10, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpTimeout": 15000000000, "httpUserAgent": "", "httpDebugging": false, @@ -836,6 +868,8 @@ "websocket": false, "useSandbox": false, "restPollingDelay": 10, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpTimeout": 15000000000, "httpUserAgent": "", "httpDebugging": false, @@ -879,6 +913,8 @@ "websocket": false, "useSandbox": false, "restPollingDelay": 10, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpTimeout": 15000000000, "httpUserAgent": "", "httpDebugging": false, @@ -922,6 +958,8 @@ "websocket": false, "useSandbox": false, "restPollingDelay": 10, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpTimeout": 15000000000, "httpUserAgent": "", "httpDebugging": false, @@ -964,6 +1002,8 @@ "websocket": false, "useSandbox": false, "restPollingDelay": 10, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpTimeout": 15000000000, "httpUserAgent": "", "httpDebugging": false, @@ -1006,6 +1046,8 @@ "websocket": false, "useSandbox": false, "restPollingDelay": 10, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpTimeout": 15000000000, "httpUserAgent": "", "httpDebugging": false, @@ -1046,6 +1088,8 @@ "websocket": false, "useSandbox": false, "restPollingDelay": 10, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpTimeout": 15000000000, "httpUserAgent": "", "httpDebugging": false, @@ -1086,6 +1130,8 @@ "websocket": false, "useSandbox": false, "restPollingDelay": 10, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpTimeout": 15000000000, "httpUserAgent": "", "httpDebugging": false, @@ -1129,6 +1175,8 @@ "websocket": false, "useSandbox": false, "restPollingDelay": 10, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpTimeout": 15000000000, "httpUserAgent": "", "httpDebugging": false, @@ -1172,6 +1220,8 @@ "websocket": false, "useSandbox": false, "restPollingDelay": 10, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpTimeout": 15000000000, "httpUserAgent": "", "httpDebugging": false, @@ -1215,6 +1265,8 @@ "websocket": false, "useSandbox": false, "restPollingDelay": 10, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpTimeout": 15000000000, "httpUserAgent": "", "httpDebugging": false, @@ -1259,6 +1311,8 @@ "websocket": false, "useSandbox": false, "restPollingDelay": 10, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpTimeout": 15000000000, "httpUserAgent": "", "httpDebugging": false, diff --git a/exchanges/alphapoint/alphapoint_websocket.go b/exchanges/alphapoint/alphapoint_websocket.go index 7e4295f2..cdd7d20d 100644 --- a/exchanges/alphapoint/alphapoint_websocket.go +++ b/exchanges/alphapoint/alphapoint_websocket.go @@ -15,9 +15,9 @@ const ( // WebsocketClient starts a new webstocket connection func (a *Alphapoint) WebsocketClient() { for a.Enabled { - var Dialer websocket.Dialer + var dialer websocket.Dialer var err error - a.WebsocketConn, _, err = Dialer.Dial(a.WebsocketURL, http.Header{}) + a.WebsocketConn, _, err = dialer.Dial(a.WebsocketURL, http.Header{}) if err != nil { log.Errorf("%s Unable to connect to Websocket. Error: %s\n", a.Name, err) diff --git a/exchanges/alphapoint/alphapoint_wrapper.go b/exchanges/alphapoint/alphapoint_wrapper.go index d4c86cc6..b824daed 100644 --- a/exchanges/alphapoint/alphapoint_wrapper.go +++ b/exchanges/alphapoint/alphapoint_wrapper.go @@ -11,6 +11,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" ) // GetAccountInfo retrieves balances for all enabled currencies on the @@ -224,7 +225,7 @@ func (a *Alphapoint) WithdrawFiatFundsToInternationalBank(withdrawRequest *excha } // GetWebsocket returns a pointer to the exchange websocket -func (a *Alphapoint) GetWebsocket() (*exchange.Websocket, error) { +func (a *Alphapoint) GetWebsocket() (*wshandler.Websocket, error) { return nil, common.ErrNotYetImplemented } @@ -320,18 +321,18 @@ func (a *Alphapoint) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest // SubscribeToWebsocketChannels appends to ChannelsToSubscribe // which lets websocket.manageSubscriptions handle subscribing -func (a *Alphapoint) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (a *Alphapoint) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { return common.ErrFunctionNotSupported } // UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe // which lets websocket.manageSubscriptions handle unsubscribing -func (a *Alphapoint) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (a *Alphapoint) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { return common.ErrFunctionNotSupported } // GetSubscriptions returns a copied list of subscriptions -func (a *Alphapoint) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) { +func (a *Alphapoint) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/anx/anx.go b/exchanges/anx/anx.go index 573e8791..41d84c6c 100644 --- a/exchanges/anx/anx.go +++ b/exchanges/anx/anx.go @@ -14,6 +14,7 @@ import ( 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" ) @@ -72,7 +73,7 @@ func (a *ANX) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) a.APIUrlDefault = anxAPIURL a.APIUrl = a.APIUrlDefault - a.WebsocketInit() + a.Websocket = wshandler.New() } // Setup is run on startup to setup exchange with config values diff --git a/exchanges/anx/anx_wrapper.go b/exchanges/anx/anx_wrapper.go index ac568595..b8baef60 100644 --- a/exchanges/anx/anx_wrapper.go +++ b/exchanges/anx/anx_wrapper.go @@ -12,6 +12,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" ) @@ -361,7 +362,7 @@ func (a *ANX) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.Wit } // GetWebsocket returns a pointer to the exchange websocket -func (a *ANX) GetWebsocket() (*exchange.Websocket, error) { +func (a *ANX) GetWebsocket() (*wshandler.Websocket, error) { return nil, common.ErrFunctionNotSupported } @@ -448,18 +449,18 @@ func (a *ANX) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]ex // SubscribeToWebsocketChannels appends to ChannelsToSubscribe // which lets websocket.manageSubscriptions handle subscribing -func (a *ANX) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (a *ANX) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { return common.ErrFunctionNotSupported } // UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe // which lets websocket.manageSubscriptions handle unsubscribing -func (a *ANX) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (a *ANX) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { return common.ErrFunctionNotSupported } // GetSubscriptions returns a copied list of subscriptions -func (a *ANX) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) { +func (a *ANX) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/binance/binance.go b/exchanges/binance/binance.go index 74d6d37c..ab97e775 100644 --- a/exchanges/binance/binance.go +++ b/exchanges/binance/binance.go @@ -10,20 +10,20 @@ import ( "strconv" "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" ) // Binance is the overarching type across the Bithumb package type Binance struct { exchange.Base - WebsocketConn *websocket.Conn + WebsocketConn *wshandler.WebsocketConnection // Valid string list that is required by the exchange validLimits []int @@ -93,12 +93,14 @@ func (b *Binance) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) b.APIUrlDefault = apiURL b.APIUrl = b.APIUrlDefault - b.WebsocketInit() + b.Websocket = wshandler.New() b.WebsocketURL = binanceDefaultWebsocketURL - b.Websocket.Functionality = exchange.WebsocketTradeDataSupported | - exchange.WebsocketTickerSupported | - exchange.WebsocketKlineSupported | - exchange.WebsocketOrderbookSupported + b.Websocket.Functionality = wshandler.WebsocketTradeDataSupported | + wshandler.WebsocketTickerSupported | + wshandler.WebsocketKlineSupported | + wshandler.WebsocketOrderbookSupported + b.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + b.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout } // Setup takes in the supplied exchange configuration details and sets params @@ -138,17 +140,27 @@ func (b *Binance) Setup(exch *config.ExchangeConfig) { if err != nil { log.Fatal(err) } - err = b.WebsocketSetup(b.WSConnect, + err = b.Websocket.Setup(b.WSConnect, nil, nil, exch.Name, exch.Websocket, exch.Verbose, binanceDefaultWebsocketURL, - exch.WebsocketURL) + exch.WebsocketURL, + exch.AuthenticatedWebsocketAPISupport) if err != nil { log.Fatal(err) } + + b.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: b.Name, + URL: b.Websocket.GetWebsocketURL(), + ProxyURL: b.Websocket.GetProxyAddress(), + Verbose: b.Verbose, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } } } diff --git a/exchanges/binance/binance_websocket.go b/exchanges/binance/binance_websocket.go index fff453e4..c36cb768 100644 --- a/exchanges/binance/binance_websocket.go +++ b/exchanges/binance/binance_websocket.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "net/http" - "net/url" "strconv" "strings" "sync" @@ -16,6 +15,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" ) const ( @@ -70,7 +70,7 @@ func (b *Binance) UpdateLocalCache(ob *WebsocketDepthStream) error { ID, ok := lastUpdateID[ob.Pair] if !ok { m.Unlock() - return errors.New("binance_websocket.go - Unable to find lastUpdateID") + return fmt.Errorf("%v - Unable to find lastUpdateID", b.Name) } if ob.LastUpdateID+1 <= ID || ID >= ob.LastUpdateID+1 { @@ -124,10 +124,10 @@ func (b *Binance) UpdateLocalCache(ob *WebsocketDepthStream) error { // WSConnect intiates a websocket connection func (b *Binance) WSConnect() error { if !b.Websocket.IsEnabled() || !b.IsEnabled() { - return errors.New(exchange.WebsocketNotEnabled) + return errors.New(wshandler.WebsocketNotEnabled) } - var Dialer websocket.Dialer + var dialer websocket.Dialer var err error tick := strings.ToLower( @@ -152,18 +152,6 @@ func (b *Binance) WSConnect() error { kline + "/" + depth - - if b.Websocket.GetProxyAddress() != "" { - var u *url.URL - u, err = url.Parse(b.Websocket.GetProxyAddress()) - if err != nil { - return fmt.Errorf("binance_websocket.go - Unable to connect to parse proxy address. Error: %s", - err) - } - - Dialer.Proxy = http.ProxyURL(u) - } - for _, ePair := range b.GetEnabledCurrencies() { err = b.SeedLocalCache(ePair) if err != nil { @@ -171,9 +159,11 @@ func (b *Binance) WSConnect() error { } } - b.WebsocketConn, _, err = Dialer.Dial(wsurl, http.Header{}) + b.WebsocketConn.URL = wsurl + err = b.WebsocketConn.Dial(&dialer, http.Header{}) if err != nil { - return fmt.Errorf("binance_websocket.go - Unable to connect to Websocket. Error: %s", + return fmt.Errorf("%v - Unable to connect to Websocket. Error: %s", + b.Name, err) } @@ -182,18 +172,6 @@ func (b *Binance) WSConnect() error { return nil } -// WSReadData reads from the websocket connection and returns the response -func (b *Binance) WSReadData() (exchange.WebsocketResponse, error) { - msgType, resp, err := b.WebsocketConn.ReadMessage() - - if err != nil { - return exchange.WebsocketResponse{}, err - } - - b.Websocket.TrafficAlert <- struct{}{} - return exchange.WebsocketResponse{Type: msgType, Raw: resp}, nil -} - // WsHandleData handles websocket data from WsReadData func (b *Binance) WsHandleData() { b.Websocket.Wg.Add(1) @@ -208,134 +186,133 @@ func (b *Binance) WsHandleData() { return default: - read, err := b.WSReadData() + read, err := b.WebsocketConn.ReadMessage() if err != nil { b.Websocket.DataHandler <- err return } - - if read.Type == websocket.TextMessage { - multiStreamData := MultiStreamData{} - err = common.JSONDecode(read.Raw, &multiStreamData) + b.Websocket.TrafficAlert <- struct{}{} + var multiStreamData MultiStreamData + err = common.JSONDecode(read.Raw, &multiStreamData) + if err != nil { + b.Websocket.DataHandler <- fmt.Errorf("%v - Could not load multi stream data: %s", + b.Name, + read.Raw) + continue + } + streamType := strings.Split(multiStreamData.Stream, "@") + switch streamType[1] { + case "trade": + trade := TradeStream{} + err := common.JSONDecode(multiStreamData.Data, &trade) if err != nil { - b.Websocket.DataHandler <- fmt.Errorf("binance_websocket.go - Could not load multi stream data: %s", - string(read.Raw)) + b.Websocket.DataHandler <- fmt.Errorf("%v - Could not unmarshal trade data: %s", + b.Name, + err) continue } - streamType := strings.Split(multiStreamData.Stream, "@") - switch streamType[1] { - case "trade": - trade := TradeStream{} - err := common.JSONDecode(multiStreamData.Data, &trade) - if err != nil { - b.Websocket.DataHandler <- fmt.Errorf("binance_websocket.go - Could not unmarshal trade data: %s", - err) - continue - } - - price, err := strconv.ParseFloat(trade.Price, 64) - if err != nil { - b.Websocket.DataHandler <- fmt.Errorf("binance_websocket.go - price conversion error: %s", - err) - continue - } - - amount, err := strconv.ParseFloat(trade.Quantity, 64) - if err != nil { - b.Websocket.DataHandler <- fmt.Errorf("binance_websocket.go - amount conversion error: %s", - err) - continue - } - - b.Websocket.DataHandler <- exchange.TradeData{ - CurrencyPair: currency.NewPairFromString(trade.Symbol), - Timestamp: time.Unix(0, trade.TimeStamp*int64(time.Millisecond)), - Price: price, - Amount: amount, - Exchange: b.GetName(), - AssetType: "SPOT", - Side: trade.EventType, - } - continue - case "ticker": - t := TickerStream{} - - err := common.JSONDecode(multiStreamData.Data, &t) - if err != nil { - b.Websocket.DataHandler <- fmt.Errorf("binance_websocket.go - Could not convert to a TickerStream structure %s", - err.Error()) - continue - } - - var wsTicker exchange.TickerData - - wsTicker.Timestamp = time.Unix(t.EventTime/1000, 0) - wsTicker.Pair = currency.NewPairFromString(t.Symbol) - wsTicker.AssetType = ticker.Spot - wsTicker.Exchange = b.GetName() - wsTicker.ClosePrice, _ = strconv.ParseFloat(t.CurrDayClose, 64) - wsTicker.Quantity, _ = strconv.ParseFloat(t.TotalTradedVolume, 64) - wsTicker.OpenPrice, _ = strconv.ParseFloat(t.OpenPrice, 64) - wsTicker.HighPrice, _ = strconv.ParseFloat(t.HighPrice, 64) - wsTicker.LowPrice, _ = strconv.ParseFloat(t.LowPrice, 64) - - b.Websocket.DataHandler <- wsTicker - - continue - case "kline": - kline := KlineStream{} - - err := common.JSONDecode(multiStreamData.Data, &kline) - if err != nil { - b.Websocket.DataHandler <- fmt.Errorf("binance_websocket.go - Could not convert to a KlineStream structure %s", - err) - continue - } - - var wsKline exchange.KlineData - - wsKline.Timestamp = time.Unix(0, kline.EventTime) - wsKline.Pair = currency.NewPairFromString(kline.Symbol) - wsKline.AssetType = ticker.Spot - wsKline.Exchange = b.GetName() - wsKline.StartTime = time.Unix(0, kline.Kline.StartTime) - wsKline.CloseTime = time.Unix(0, kline.Kline.CloseTime) - wsKline.Interval = kline.Kline.Interval - wsKline.OpenPrice, _ = strconv.ParseFloat(kline.Kline.OpenPrice, 64) - wsKline.ClosePrice, _ = strconv.ParseFloat(kline.Kline.ClosePrice, 64) - wsKline.HighPrice, _ = strconv.ParseFloat(kline.Kline.HighPrice, 64) - wsKline.LowPrice, _ = strconv.ParseFloat(kline.Kline.LowPrice, 64) - wsKline.Volume, _ = strconv.ParseFloat(kline.Kline.Volume, 64) - - b.Websocket.DataHandler <- wsKline - continue - case "depth": - depth := WebsocketDepthStream{} - - err := common.JSONDecode(multiStreamData.Data, &depth) - if err != nil { - b.Websocket.DataHandler <- fmt.Errorf("binance_websocket.go - Could not convert to depthStream structure %s", - err) - continue - } - - err = b.UpdateLocalCache(&depth) - if err != nil { - b.Websocket.DataHandler <- fmt.Errorf("binance_websocket.go - UpdateLocalCache error: %s", - err) - continue - } - - currencyPair := currency.NewPairFromString(depth.Pair) - - b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ - Pair: currencyPair, - Asset: "SPOT", - Exchange: b.GetName(), - } + price, err := strconv.ParseFloat(trade.Price, 64) + if err != nil { + b.Websocket.DataHandler <- fmt.Errorf("%v - price conversion error: %s", + b.Name, + err) continue } + + amount, err := strconv.ParseFloat(trade.Quantity, 64) + if err != nil { + b.Websocket.DataHandler <- fmt.Errorf("%v - amount conversion error: %s", + b.Name, + err) + continue + } + + b.Websocket.DataHandler <- wshandler.TradeData{ + CurrencyPair: currency.NewPairFromString(trade.Symbol), + Timestamp: time.Unix(0, trade.TimeStamp), + Price: price, + Amount: amount, + Exchange: b.GetName(), + AssetType: "SPOT", + Side: trade.EventType, + } + continue + case "ticker": + t := TickerStream{} + err := common.JSONDecode(multiStreamData.Data, &t) + if err != nil { + b.Websocket.DataHandler <- fmt.Errorf("%v - Could not convert to a TickerStream structure %s", + b.Name, + err.Error()) + continue + } + + var wsTicker wshandler.TickerData + + wsTicker.Timestamp = time.Unix(t.EventTime/1000, 0) + wsTicker.Pair = currency.NewPairFromString(t.Symbol) + wsTicker.AssetType = ticker.Spot + wsTicker.Exchange = b.GetName() + wsTicker.ClosePrice, _ = strconv.ParseFloat(t.CurrDayClose, 64) + wsTicker.Quantity, _ = strconv.ParseFloat(t.TotalTradedVolume, 64) + wsTicker.OpenPrice, _ = strconv.ParseFloat(t.OpenPrice, 64) + wsTicker.HighPrice, _ = strconv.ParseFloat(t.HighPrice, 64) + wsTicker.LowPrice, _ = strconv.ParseFloat(t.LowPrice, 64) + + b.Websocket.DataHandler <- wsTicker + + continue + case "kline": + kline := KlineStream{} + err := common.JSONDecode(multiStreamData.Data, &kline) + if err != nil { + b.Websocket.DataHandler <- fmt.Errorf("%v - Could not convert to a KlineStream structure %s", + b.Name, + err) + continue + } + + var wsKline wshandler.KlineData + wsKline.Timestamp = time.Unix(0, kline.EventTime) + wsKline.Pair = currency.NewPairFromString(kline.Symbol) + wsKline.AssetType = ticker.Spot + wsKline.Exchange = b.GetName() + wsKline.StartTime = time.Unix(0, kline.Kline.StartTime) + wsKline.CloseTime = time.Unix(0, kline.Kline.CloseTime) + wsKline.Interval = kline.Kline.Interval + wsKline.OpenPrice, _ = strconv.ParseFloat(kline.Kline.OpenPrice, 64) + wsKline.ClosePrice, _ = strconv.ParseFloat(kline.Kline.ClosePrice, 64) + wsKline.HighPrice, _ = strconv.ParseFloat(kline.Kline.HighPrice, 64) + wsKline.LowPrice, _ = strconv.ParseFloat(kline.Kline.LowPrice, 64) + wsKline.Volume, _ = strconv.ParseFloat(kline.Kline.Volume, 64) + b.Websocket.DataHandler <- wsKline + continue + case "depth": + depth := WebsocketDepthStream{} + err := common.JSONDecode(multiStreamData.Data, &depth) + if err != nil { + b.Websocket.DataHandler <- fmt.Errorf("%v - Could not convert to depthStream structure %s", + b.Name, + err) + continue + } + + err = b.UpdateLocalCache(&depth) + if err != nil { + b.Websocket.DataHandler <- fmt.Errorf("%v - UpdateLocalCache error: %s", + b.Name, + err) + continue + } + + currencyPair := currency.NewPairFromString(depth.Pair) + b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ + Pair: currencyPair, + Asset: "SPOT", + Exchange: b.GetName(), + } + continue } } } diff --git a/exchanges/binance/binance_wrapper.go b/exchanges/binance/binance_wrapper.go index 194d63f1..fe1c169e 100644 --- a/exchanges/binance/binance_wrapper.go +++ b/exchanges/binance/binance_wrapper.go @@ -13,6 +13,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" ) @@ -318,7 +319,7 @@ func (b *Binance) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange } // GetWebsocket returns a pointer to the exchange websocket -func (b *Binance) GetWebsocket() (*exchange.Websocket, error) { +func (b *Binance) GetWebsocket() (*wshandler.Websocket, error) { return b.Websocket, nil } @@ -416,18 +417,18 @@ func (b *Binance) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ( // SubscribeToWebsocketChannels appends to ChannelsToSubscribe // which lets websocket.manageSubscriptions handle subscribing -func (b *Binance) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (b *Binance) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { return common.ErrFunctionNotSupported } // UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe // which lets websocket.manageSubscriptions handle unsubscribing -func (b *Binance) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (b *Binance) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { return common.ErrFunctionNotSupported } // GetSubscriptions returns a copied list of subscriptions -func (b *Binance) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) { +func (b *Binance) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { return b.Websocket.GetSubscriptions(), nil } diff --git a/exchanges/bitfinex/bitfinex.go b/exchanges/bitfinex/bitfinex.go index 8679d1ca..742f0854 100644 --- a/exchanges/bitfinex/bitfinex.go +++ b/exchanges/bitfinex/bitfinex.go @@ -6,16 +6,15 @@ import ( "net/http" "net/url" "strconv" - "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" ) @@ -86,9 +85,8 @@ const ( // depending on some factors (e.g. servers load, endpoint, etc.). type Bitfinex struct { exchange.Base - WebsocketConn *websocket.Conn + WebsocketConn *wshandler.WebsocketConnection WebsocketSubdChannels map[int]WebsocketChanInfo - wsRequestMtx sync.Mutex } // SetDefaults sets the basic defaults for bitfinex @@ -113,13 +111,15 @@ func (b *Bitfinex) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) b.APIUrlDefault = bitfinexAPIURLBase b.APIUrl = b.APIUrlDefault - b.WebsocketInit() - b.Websocket.Functionality = exchange.WebsocketTickerSupported | - exchange.WebsocketTradeDataSupported | - exchange.WebsocketOrderbookSupported | - exchange.WebsocketSubscribeSupported | - exchange.WebsocketUnsubscribeSupported | - exchange.WebsocketAuthenticatedEndpointsSupported + b.Websocket = wshandler.New() + b.Websocket.Functionality = wshandler.WebsocketTickerSupported | + wshandler.WebsocketTradeDataSupported | + wshandler.WebsocketOrderbookSupported | + wshandler.WebsocketSubscribeSupported | + wshandler.WebsocketUnsubscribeSupported | + wshandler.WebsocketAuthenticatedEndpointsSupported + b.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + b.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout } // Setup takes in the supplied exchange configuration details and sets params @@ -160,17 +160,28 @@ func (b *Bitfinex) Setup(exch *config.ExchangeConfig) { if err != nil { log.Fatal(err) } - err = b.WebsocketSetup(b.WsConnect, + err = b.Websocket.Setup(b.WsConnect, b.Subscribe, b.Unsubscribe, exch.Name, exch.Websocket, exch.Verbose, bitfinexWebsocket, - exch.WebsocketURL) + exch.WebsocketURL, + exch.AuthenticatedWebsocketAPISupport) if err != nil { log.Fatal(err) } + b.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: b.Name, + URL: b.Websocket.GetWebsocketURL(), + ProxyURL: b.Websocket.GetProxyAddress(), + Verbose: b.Verbose, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } + b.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + b.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout } } diff --git a/exchanges/bitfinex/bitfinex_test.go b/exchanges/bitfinex/bitfinex_test.go index 2d284ad7..60b3c500 100644 --- a/exchanges/bitfinex/bitfinex_test.go +++ b/exchanges/bitfinex/bitfinex_test.go @@ -13,6 +13,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 your own keys here to do better tests @@ -961,19 +962,23 @@ func TestWsAuth(t *testing.T) { b.SetDefaults() TestSetup(t) if !b.Websocket.IsEnabled() && !b.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() { - t.Skip(exchange.WebsocketNotEnabled) + t.Skip(wshandler.WebsocketNotEnabled) + } + b.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: b.Name, + URL: b.Websocket.GetWebsocketURL(), + Verbose: b.Verbose, + ResponseMaxLimit: exchange.DefaultWebsocketResponseMaxLimit, + ResponseCheckTimeout: exchange.DefaultWebsocketResponseCheckTimeout, } - var err error var dialer websocket.Dialer - b.WebsocketConn, _, err = dialer.Dial(b.Websocket.GetWebsocketURL(), - http.Header{}) + err := b.WebsocketConn.Dial(&dialer, http.Header{}) if err != nil { t.Fatal(err) } b.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() b.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() go b.WsDataHandler() - defer b.WebsocketConn.Close() err = b.WsSendAuth() if err != nil { t.Error(err) diff --git a/exchanges/bitfinex/bitfinex_websocket.go b/exchanges/bitfinex/bitfinex_websocket.go index 00249157..c0820282 100644 --- a/exchanges/bitfinex/bitfinex_websocket.go +++ b/exchanges/bitfinex/bitfinex_websocket.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "net/http" - "net/url" "reflect" "strconv" "time" @@ -14,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" ) @@ -59,21 +59,7 @@ func (b *Bitfinex) WsPingHandler() error { req := make(map[string]string) req["event"] = "ping" - return b.wsSend(req) -} - -// WsSend sends data to the websocket server -func (b *Bitfinex) wsSend(data interface{}) error { - b.wsRequestMtx.Lock() - defer b.wsRequestMtx.Unlock() - json, err := common.JSONEncode(data) - if err != nil { - return err - } - if b.Verbose { - log.Debugf("%v sending message to websocket %v", b.Name, data) - } - return b.WebsocketConn.WriteMessage(websocket.TextMessage, json) + return b.WebsocketConn.SendMessage(req) } // WsSendAuth sends a autheticated event payload @@ -94,7 +80,7 @@ func (b *Bitfinex) WsSendAuth() error { req["authPayload"] = payload - err := b.wsSend(req) + err := b.WebsocketConn.SendMessage(req) if err != nil { b.Websocket.SetCanUseAuthenticatedEndpoints(false) return err @@ -107,7 +93,7 @@ func (b *Bitfinex) WsSendUnauth() error { req := make(map[string]string) req["event"] = "unauth" - return b.wsSend(req) + return b.WebsocketConn.SendMessage(req) } // WsAddSubscriptionChannel adds a new subscription channel to the @@ -128,33 +114,28 @@ func (b *Bitfinex) WsAddSubscriptionChannel(chanID int, channel, pair string) { // WsConnect starts a new websocket connection func (b *Bitfinex) WsConnect() error { if !b.Websocket.IsEnabled() || !b.IsEnabled() { - return errors.New(exchange.WebsocketNotEnabled) + return errors.New(wshandler.WebsocketNotEnabled) } - var Dialer websocket.Dialer - var err error - - if b.Websocket.GetProxyAddress() != "" { - var proxy *url.URL - proxy, err = url.Parse(b.Websocket.GetProxyAddress()) - if err != nil { - return err - } - Dialer.Proxy = http.ProxyURL(proxy) + var dialer websocket.Dialer + b.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: b.Name, + URL: b.Websocket.GetWebsocketURL(), + ProxyURL: b.Websocket.GetProxyAddress(), + Verbose: b.Verbose, } - - b.WebsocketConn, _, err = Dialer.Dial(b.Websocket.GetWebsocketURL(), http.Header{}) + err := b.WebsocketConn.Dial(&dialer, http.Header{}) if err != nil { return fmt.Errorf("%v unable to connect to Websocket. Error: %s", b.Name, err) } - _, resp, err := b.WebsocketConn.ReadMessage() + resp, err := b.WebsocketConn.ReadMessage() if err != nil { return fmt.Errorf("%v unable to read from Websocket. Error: %s", b.Name, err) } - + b.Websocket.TrafficAlert <- struct{}{} var hs WebsocketHandshake - err = common.JSONDecode(resp, &hs) + err = common.JSONDecode(resp.Raw, &hs) if err != nil { return err } @@ -178,22 +159,6 @@ func (b *Bitfinex) WsConnect() error { return nil } -// WsReadData reads and handles websocket stream data -func (b *Bitfinex) WsReadData() (exchange.WebsocketResponse, error) { - msgType, resp, err := b.WebsocketConn.ReadMessage() - if err != nil { - return exchange.WebsocketResponse{}, err - } - - b.Websocket.TrafficAlert <- struct{}{} - - return exchange.WebsocketResponse{ - Type: msgType, - Raw: resp, - }, nil - -} - // WsDataHandler handles data from WsReadData func (b *Bitfinex) WsDataHandler() { b.Websocket.Wg.Add(1) @@ -208,11 +173,12 @@ func (b *Bitfinex) WsDataHandler() { return default: - stream, err := b.WsReadData() + stream, err := b.WebsocketConn.ReadMessage() if err != nil { b.Websocket.DataHandler <- err return } + b.Websocket.TrafficAlert <- struct{}{} if stream.Type == websocket.TextMessage { var result interface{} @@ -221,9 +187,6 @@ func (b *Bitfinex) WsDataHandler() { case "map[string]interface {}": eventData := result.(map[string]interface{}) event := eventData["event"] - if b.Verbose { - log.Debugf("%v Received message. Type '%v' Message: %v", b.Name, event, eventData) - } switch event { case "subscribed": b.WsAddSubscriptionChannel(int(eventData["chanId"].(float64)), @@ -306,7 +269,7 @@ func (b *Bitfinex) WsDataHandler() { } case "ticker": - b.Websocket.DataHandler <- exchange.TickerData{ + b.Websocket.DataHandler <- wshandler.TickerData{ Quantity: chanData[8].(float64), ClosePrice: chanData[7].(float64), HighPrice: chanData[9].(float64), @@ -476,7 +439,7 @@ func (b *Bitfinex) WsDataHandler() { newAmount *= -1 } - b.Websocket.DataHandler <- exchange.TradeData{ + b.Websocket.DataHandler <- wshandler.TradeData{ CurrencyPair: currency.NewPairFromString(chanInfo.Pair), Timestamp: time.Unix(trades[0].Timestamp, 0), Price: trades[0].Price, @@ -524,7 +487,7 @@ func (b *Bitfinex) WsInsertSnapshot(p currency.Pair, assetType string, books []W return fmt.Errorf("bitfinex.go error - %s", err) } - b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{Pair: p, + b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: p, Asset: assetType, Exchange: b.GetName()} return nil @@ -549,7 +512,7 @@ func (b *Bitfinex) WsUpdateOrderbook(p currency.Pair, assetType string, book Web return err } - b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{Pair: p, + b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: p, Asset: assetType, Exchange: b.GetName()} @@ -569,7 +532,7 @@ func (b *Bitfinex) WsUpdateOrderbook(p currency.Pair, assetType string, book Web return err } - b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{Pair: p, + b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: p, Asset: assetType, Exchange: b.GetName()} @@ -590,7 +553,7 @@ func (b *Bitfinex) WsUpdateOrderbook(p currency.Pair, assetType string, book Web return err } - b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{Pair: p, + b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: p, Asset: assetType, Exchange: b.GetName()} @@ -610,7 +573,7 @@ func (b *Bitfinex) WsUpdateOrderbook(p currency.Pair, assetType string, book Web return err } - b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{Pair: p, + b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: p, Asset: assetType, Exchange: b.GetName()} @@ -620,14 +583,14 @@ func (b *Bitfinex) WsUpdateOrderbook(p currency.Pair, assetType string, book Web // GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() func (b *Bitfinex) GenerateDefaultSubscriptions() { var channels = []string{"book", "trades", "ticker"} - var subscriptions []exchange.WebsocketChannelSubscription + var subscriptions []wshandler.WebsocketChannelSubscription for i := range channels { for j := range b.EnabledPairs { params := make(map[string]interface{}) if channels[i] == "book" { params["prec"] = "P0" } - subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ Channel: channels[i], Currency: b.EnabledPairs[j], Params: params, @@ -638,7 +601,7 @@ func (b *Bitfinex) GenerateDefaultSubscriptions() { } // Subscribe sends a websocket message to receive data from the channel -func (b *Bitfinex) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { +func (b *Bitfinex) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { req := make(map[string]interface{}) req["event"] = "subscribe" req["channel"] = channelToSubscribe.Channel @@ -650,11 +613,11 @@ func (b *Bitfinex) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscri req[k] = v } } - return b.wsSend(req) + return b.WebsocketConn.SendMessage(req) } // Unsubscribe sends a websocket message to stop receiving data from the channel -func (b *Bitfinex) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { +func (b *Bitfinex) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { req := make(map[string]interface{}) req["event"] = "unsubscribe" req["channel"] = channelToSubscribe.Channel @@ -664,5 +627,5 @@ func (b *Bitfinex) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubsc req[k] = v } } - return b.wsSend(req) + return b.WebsocketConn.SendMessage(req) } diff --git a/exchanges/bitfinex/bitfinex_wrapper.go b/exchanges/bitfinex/bitfinex_wrapper.go index a3071ffe..31519a54 100644 --- a/exchanges/bitfinex/bitfinex_wrapper.go +++ b/exchanges/bitfinex/bitfinex_wrapper.go @@ -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" ) @@ -323,7 +324,7 @@ func (b *Bitfinex) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchang } // GetWebsocket returns a pointer to the exchange websocket -func (b *Bitfinex) GetWebsocket() (*exchange.Websocket, error) { +func (b *Bitfinex) GetWebsocket() (*wshandler.Websocket, error) { return b.Websocket, nil } @@ -457,20 +458,20 @@ func (b *Bitfinex) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) // SubscribeToWebsocketChannels appends to ChannelsToSubscribe // which lets websocket.manageSubscriptions handle subscribing -func (b *Bitfinex) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (b *Bitfinex) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { b.Websocket.SubscribeToChannels(channels) return nil } // UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe // which lets websocket.manageSubscriptions handle unsubscribing -func (b *Bitfinex) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { - b.Websocket.UnsubscribeToChannels(channels) +func (b *Bitfinex) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { + b.Websocket.RemoveSubscribedChannels(channels) return nil } // GetSubscriptions returns a copied list of subscriptions -func (b *Bitfinex) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) { +func (b *Bitfinex) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { return b.Websocket.GetSubscriptions(), nil } diff --git a/exchanges/bitflyer/bitflyer.go b/exchanges/bitflyer/bitflyer.go index b4e68583..c8fee189 100644 --- a/exchanges/bitflyer/bitflyer.go +++ b/exchanges/bitflyer/bitflyer.go @@ -14,6 +14,7 @@ import ( 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" ) @@ -99,7 +100,7 @@ func (b *Bitflyer) SetDefaults() { b.APIUrl = b.APIUrlDefault b.APIUrlSecondaryDefault = chainAnalysis b.APIUrlSecondary = b.APIUrlSecondaryDefault - b.WebsocketInit() + b.Websocket = wshandler.New() } // Setup takes in the supplied exchange configuration details and sets params diff --git a/exchanges/bitflyer/bitflyer_wrapper.go b/exchanges/bitflyer/bitflyer_wrapper.go index 6572223d..88b9c420 100644 --- a/exchanges/bitflyer/bitflyer_wrapper.go +++ b/exchanges/bitflyer/bitflyer_wrapper.go @@ -9,6 +9,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" ) @@ -219,7 +220,7 @@ func (b *Bitflyer) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchang } // GetWebsocket returns a pointer to the exchange websocket -func (b *Bitflyer) GetWebsocket() (*exchange.Websocket, error) { +func (b *Bitflyer) GetWebsocket() (*wshandler.Websocket, error) { return nil, common.ErrNotYetImplemented } @@ -245,18 +246,18 @@ func (b *Bitflyer) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error // SubscribeToWebsocketChannels appends to ChannelsToSubscribe // which lets websocket.manageSubscriptions handle subscribing -func (b *Bitflyer) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (b *Bitflyer) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { return common.ErrFunctionNotSupported } // UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe // which lets websocket.manageSubscriptions handle unsubscribing -func (b *Bitflyer) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (b *Bitflyer) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { return common.ErrFunctionNotSupported } // GetSubscriptions returns a copied list of subscriptions -func (b *Bitflyer) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) { +func (b *Bitflyer) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/bithumb/bithumb.go b/exchanges/bithumb/bithumb.go index f9c87f9d..2fa36249 100644 --- a/exchanges/bithumb/bithumb.go +++ b/exchanges/bithumb/bithumb.go @@ -17,6 +17,7 @@ import ( 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" ) @@ -81,7 +82,7 @@ func (b *Bithumb) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) b.APIUrlDefault = apiURL b.APIUrl = b.APIUrlDefault - b.WebsocketInit() + b.Websocket = wshandler.New() } // Setup takes in the supplied exchange configuration details and sets params diff --git a/exchanges/bithumb/bithumb_wrapper.go b/exchanges/bithumb/bithumb_wrapper.go index df519436..3ff457f5 100644 --- a/exchanges/bithumb/bithumb_wrapper.go +++ b/exchanges/bithumb/bithumb_wrapper.go @@ -13,6 +13,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" ) @@ -320,7 +321,7 @@ func (b *Bithumb) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange } // GetWebsocket returns a pointer to the exchange websocket -func (b *Bithumb) GetWebsocket() (*exchange.Websocket, error) { +func (b *Bithumb) GetWebsocket() (*wshandler.Websocket, error) { return nil, common.ErrFunctionNotSupported } @@ -423,18 +424,18 @@ func (b *Bithumb) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ( // SubscribeToWebsocketChannels appends to ChannelsToSubscribe // which lets websocket.manageSubscriptions handle subscribing -func (b *Bithumb) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (b *Bithumb) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { return common.ErrFunctionNotSupported } // UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe // which lets websocket.manageSubscriptions handle unsubscribing -func (b *Bithumb) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (b *Bithumb) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { return common.ErrFunctionNotSupported } // GetSubscriptions returns a copied list of subscriptions -func (b *Bithumb) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) { +func (b *Bithumb) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/bitmex/bitmex.go b/exchanges/bitmex/bitmex.go index 48232e2a..b1e57e7c 100644 --- a/exchanges/bitmex/bitmex.go +++ b/exchanges/bitmex/bitmex.go @@ -7,24 +7,22 @@ import ( "net/http" "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" ) // Bitmex is the overarching type across this package type Bitmex struct { exchange.Base - WebsocketConn *websocket.Conn - wsRequestMtx sync.Mutex + WebsocketConn *wshandler.WebsocketConnection } const ( @@ -135,13 +133,16 @@ func (b *Bitmex) SetDefaults() { b.APIUrlDefault = bitmexAPIURL b.APIUrl = b.APIUrlDefault b.SupportsAutoPairUpdating = true - b.WebsocketInit() - b.Websocket.Functionality = exchange.WebsocketTradeDataSupported | - exchange.WebsocketOrderbookSupported | - exchange.WebsocketSubscribeSupported | - exchange.WebsocketUnsubscribeSupported | - exchange.WebsocketAuthenticatedEndpointsSupported | - exchange.WebsocketAccountDataSupported + b.Websocket = wshandler.New() + b.Websocket.Functionality = wshandler.WebsocketTradeDataSupported | + wshandler.WebsocketOrderbookSupported | + wshandler.WebsocketSubscribeSupported | + wshandler.WebsocketUnsubscribeSupported | + wshandler.WebsocketAuthenticatedEndpointsSupported | + wshandler.WebsocketAccountDataSupported | + wshandler.WebsocketDeadMansSwitchSupported + b.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + b.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout } // Setup takes in the supplied exchange configuration details and sets params @@ -180,17 +181,26 @@ func (b *Bitmex) Setup(exch *config.ExchangeConfig) { if err != nil { log.Fatal(err) } - err = b.WebsocketSetup(b.WsConnector, + err = b.Websocket.Setup(b.WsConnector, b.Subscribe, b.Unsubscribe, exch.Name, exch.Websocket, exch.Verbose, bitmexWSURL, - exch.WebsocketURL) + exch.WebsocketURL, + exch.AuthenticatedWebsocketAPISupport) if err != nil { log.Fatal(err) } + b.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: b.Name, + URL: b.Websocket.GetWebsocketURL(), + ProxyURL: b.Websocket.GetProxyAddress(), + Verbose: b.Verbose, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } } } diff --git a/exchanges/bitmex/bitmex_test.go b/exchanges/bitmex/bitmex_test.go index 5f86a890..b2773b4b 100644 --- a/exchanges/bitmex/bitmex_test.go +++ b/exchanges/bitmex/bitmex_test.go @@ -12,6 +12,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 your own keys here for due diligence testing @@ -689,22 +690,26 @@ func TestWsAuth(t *testing.T) { b.SetDefaults() TestSetup(t) if !b.Websocket.IsEnabled() && !b.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() { - t.Skip(exchange.WebsocketNotEnabled) + t.Skip(wshandler.WebsocketNotEnabled) + } + b.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: b.Name, + URL: b.Websocket.GetWebsocketURL(), + Verbose: b.Verbose, + ResponseMaxLimit: exchange.DefaultWebsocketResponseMaxLimit, + ResponseCheckTimeout: exchange.DefaultWebsocketResponseCheckTimeout, } - var err error var dialer websocket.Dialer - b.WebsocketConn, _, err = dialer.Dial(b.Websocket.GetWebsocketURL(), - http.Header{}) + err := b.WebsocketConn.Dial(&dialer, http.Header{}) if err != nil { t.Fatal(err) } b.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() b.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() go b.wsHandleIncomingData() - defer b.WebsocketConn.Close() err = b.websocketSendAuth() if err != nil { - t.Error(err) + t.Fatal(err) } timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout) select { diff --git a/exchanges/bitmex/bitmex_websocket.go b/exchanges/bitmex/bitmex_websocket.go index 1e9040e2..bc4d34cc 100644 --- a/exchanges/bitmex/bitmex_websocket.go +++ b/exchanges/bitmex/bitmex_websocket.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "net/http" - "net/url" "strconv" "strings" "time" @@ -14,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" ) @@ -66,34 +66,21 @@ var ( // WsConnector initiates a new websocket connection func (b *Bitmex) WsConnector() error { if !b.Websocket.IsEnabled() || !b.IsEnabled() { - return errors.New(exchange.WebsocketNotEnabled) + return errors.New(wshandler.WebsocketNotEnabled) } - var dialer websocket.Dialer - var err error - - if b.Websocket.GetProxyAddress() != "" { - var proxy *url.URL - proxy, err = url.Parse(b.Websocket.GetProxyAddress()) - if err != nil { - return err - } - - dialer.Proxy = http.ProxyURL(proxy) - } - - b.WebsocketConn, _, err = dialer.Dial(b.Websocket.GetWebsocketURL(), nil) + err := b.WebsocketConn.Dial(&dialer, http.Header{}) if err != nil { return err } - _, p, err := b.WebsocketConn.ReadMessage() + p, err := b.WebsocketConn.ReadMessage() if err != nil { return err } - + b.Websocket.TrafficAlert <- struct{}{} var welcomeResp WebsocketWelcome - err = common.JSONDecode(p, &welcomeResp) + err = common.JSONDecode(p.Raw, &welcomeResp) if err != nil { return err } @@ -116,19 +103,6 @@ func (b *Bitmex) WsConnector() error { return nil } -func (b *Bitmex) wsReadData() (exchange.WebsocketResponse, error) { - _, resp, err := b.WebsocketConn.ReadMessage() - if err != nil { - return exchange.WebsocketResponse{}, err - } - - b.Websocket.TrafficAlert <- struct{}{} - - return exchange.WebsocketResponse{ - Raw: resp, - }, nil -} - // wsHandleIncomingData services incoming data from the websocket connection func (b *Bitmex) wsHandleIncomingData() { b.Websocket.Wg.Add(1) @@ -143,12 +117,12 @@ func (b *Bitmex) wsHandleIncomingData() { return default: - resp, err := b.wsReadData() + resp, err := b.WebsocketConn.ReadMessage() if err != nil { b.Websocket.DataHandler <- err return } - + b.Websocket.TrafficAlert <- struct{}{} message := string(resp.Raw) if common.StringContains(message, "pong") { pongChan <- 1 @@ -156,7 +130,7 @@ func (b *Bitmex) wsHandleIncomingData() { } if common.StringContains(message, "ping") { - err = b.wsSend("pong") + err = b.WebsocketConn.SendMessage("pong") if err != nil { b.Websocket.DataHandler <- err continue @@ -255,7 +229,7 @@ func (b *Bitmex) wsHandleIncomingData() { } // TODO: update this to support multiple asset types - b.Websocket.DataHandler <- exchange.TradeData{ + b.Websocket.DataHandler <- wshandler.TradeData{ Timestamp: timestamp, Price: trade.Price, Amount: float64(trade.Size), @@ -405,7 +379,7 @@ func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, currencyPai err) } snapshotloaded[currencyPair][assetType] = true - b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Pair: currencyPair, Asset: assetType, Exchange: b.GetName(), @@ -440,7 +414,7 @@ func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, currencyPai return err } - b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Pair: currencyPair, Asset: assetType, Exchange: b.GetName(), @@ -454,7 +428,7 @@ func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, currencyPai func (b *Bitmex) GenerateDefaultSubscriptions() { contracts := b.GetEnabledCurrencies() channels := []string{bitmexWSOrderbookL2, bitmexWSTrade} - subscriptions := []exchange.WebsocketChannelSubscription{ + subscriptions := []wshandler.WebsocketChannelSubscription{ { Channel: bitmexWSAnnouncement, }, @@ -462,7 +436,7 @@ func (b *Bitmex) GenerateDefaultSubscriptions() { for i := range channels { for j := range contracts { - subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ Channel: fmt.Sprintf("%v:%v", channels[i], contracts[j].String()), Currency: contracts[j], }) @@ -480,7 +454,7 @@ func (b *Bitmex) GenerateAuthenticatedSubscriptions() { channels := []string{bitmexWSExecution, bitmexWSPosition, } - subscriptions := []exchange.WebsocketChannelSubscription{ + subscriptions := []wshandler.WebsocketChannelSubscription{ { Channel: bitmexWSAffiliate, }, @@ -502,7 +476,7 @@ func (b *Bitmex) GenerateAuthenticatedSubscriptions() { } for i := range channels { for j := range contracts { - subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ Channel: fmt.Sprintf("%v:%v", channels[i], contracts[j].String()), Currency: contracts[j], }) @@ -512,21 +486,21 @@ func (b *Bitmex) GenerateAuthenticatedSubscriptions() { } // Subscribe subscribes to a websocket channel -func (b *Bitmex) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { +func (b *Bitmex) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { var subscriber WebsocketRequest subscriber.Command = "subscribe" subscriber.Arguments = append(subscriber.Arguments, channelToSubscribe.Channel) - return b.wsSend(subscriber) + return b.WebsocketConn.SendMessage(subscriber) } // Unsubscribe sends a websocket message to stop receiving data from the channel -func (b *Bitmex) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { +func (b *Bitmex) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { var subscriber WebsocketRequest subscriber.Command = "unsubscribe" subscriber.Arguments = append(subscriber.Arguments, channelToSubscribe.Params["args"], channelToSubscribe.Channel+":"+channelToSubscribe.Currency.String()) - return b.wsSend(subscriber) + return b.WebsocketConn.SendMessage(subscriber) } // WebsocketSendAuth sends an authenticated subscription @@ -545,20 +519,10 @@ func (b *Bitmex) websocketSendAuth() error { sendAuth.Command = "authKeyExpires" sendAuth.Arguments = append(sendAuth.Arguments, b.APIKey, timestamp, signature) - err := b.wsSend(sendAuth) + err := b.WebsocketConn.SendMessage(sendAuth) if err != nil { b.Websocket.SetCanUseAuthenticatedEndpoints(false) return err } return nil } - -// WsSend sends data to the websocket server -func (b *Bitmex) wsSend(data interface{}) error { - b.wsRequestMtx.Lock() - defer b.wsRequestMtx.Unlock() - if b.Verbose { - log.Debugf("%v sending message to websocket %v", b.Name, data) - } - return b.WebsocketConn.WriteJSON(data) -} diff --git a/exchanges/bitmex/bitmex_wrapper.go b/exchanges/bitmex/bitmex_wrapper.go index 6aa2f0c9..2716b0f4 100644 --- a/exchanges/bitmex/bitmex_wrapper.go +++ b/exchanges/bitmex/bitmex_wrapper.go @@ -12,6 +12,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" ) @@ -300,7 +301,7 @@ func (b *Bitmex) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange. } // GetWebsocket returns a pointer to the exchange websocket -func (b *Bitmex) GetWebsocket() (*exchange.Websocket, error) { +func (b *Bitmex) GetWebsocket() (*wshandler.Websocket, error) { return b.Websocket, nil } @@ -401,20 +402,20 @@ func (b *Bitmex) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([ // SubscribeToWebsocketChannels appends to ChannelsToSubscribe // which lets websocket.manageSubscriptions handle subscribing -func (b *Bitmex) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (b *Bitmex) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { b.Websocket.SubscribeToChannels(channels) return nil } // UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe // which lets websocket.manageSubscriptions handle unsubscribing -func (b *Bitmex) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { - b.Websocket.UnsubscribeToChannels(channels) +func (b *Bitmex) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { + b.Websocket.RemoveSubscribedChannels(channels) return nil } // GetSubscriptions returns a copied list of subscriptions -func (b *Bitmex) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) { +func (b *Bitmex) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { return b.Websocket.GetSubscriptions(), nil } diff --git a/exchanges/bitstamp/bitstamp.go b/exchanges/bitstamp/bitstamp.go index 868e16bf..a068f1fc 100644 --- a/exchanges/bitstamp/bitstamp.go +++ b/exchanges/bitstamp/bitstamp.go @@ -9,16 +9,15 @@ import ( "reflect" "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" ) @@ -64,8 +63,7 @@ const ( type Bitstamp struct { exchange.Base Balance Balances - WebsocketConn *websocket.Conn - wsRequestMtx sync.Mutex + WebsocketConn *wshandler.WebsocketConnection } // SetDefaults sets default for Bitstamp @@ -89,11 +87,13 @@ func (b *Bitstamp) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) b.APIUrlDefault = bitstampAPIURL b.APIUrl = b.APIUrlDefault - b.WebsocketInit() - b.Websocket.Functionality = exchange.WebsocketOrderbookSupported | - exchange.WebsocketTradeDataSupported | - exchange.WebsocketSubscribeSupported | - exchange.WebsocketUnsubscribeSupported + b.Websocket = wshandler.New() + b.Websocket.Functionality = wshandler.WebsocketOrderbookSupported | + wshandler.WebsocketTradeDataSupported | + wshandler.WebsocketSubscribeSupported | + wshandler.WebsocketUnsubscribeSupported + b.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + b.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout } // Setup sets configuration values to bitstamp @@ -138,17 +138,26 @@ func (b *Bitstamp) Setup(exch *config.ExchangeConfig) { if err != nil { log.Fatal(err) } - err = b.WebsocketSetup(b.WsConnect, + err = b.Websocket.Setup(b.WsConnect, b.Subscribe, b.Unsubscribe, exch.Name, exch.Websocket, exch.Verbose, bitstampWSURL, - exch.WebsocketURL) + exch.WebsocketURL, + exch.AuthenticatedWebsocketAPISupport) if err != nil { log.Fatal(err) } + b.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: b.Name, + URL: b.Websocket.GetWebsocketURL(), + ProxyURL: b.Websocket.GetProxyAddress(), + Verbose: b.Verbose, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } } } diff --git a/exchanges/bitstamp/bitstamp_websocket.go b/exchanges/bitstamp/bitstamp_websocket.go index 9aab11db..f51144b6 100644 --- a/exchanges/bitstamp/bitstamp_websocket.go +++ b/exchanges/bitstamp/bitstamp_websocket.go @@ -4,16 +4,15 @@ import ( "errors" "fmt" "net/http" - "net/url" "strconv" "time" "github.com/gorilla/websocket" "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/currency" - 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" ) @@ -24,26 +23,13 @@ const ( // WsConnect connects to a websocket feed func (b *Bitstamp) WsConnect() error { if !b.Websocket.IsEnabled() || !b.IsEnabled() { - return errors.New(exchange.WebsocketNotEnabled) + return errors.New(wshandler.WebsocketNotEnabled) } - var dialer websocket.Dialer - if b.Websocket.GetProxyAddress() != "" { - proxy, err := url.Parse(b.Websocket.GetProxyAddress()) - if err != nil { - return err - } - dialer.Proxy = http.ProxyURL(proxy) - } - - var err error - b.WebsocketConn, _, err = dialer.Dial(b.Websocket.GetWebsocketURL(), http.Header{}) + err := b.WebsocketConn.Dial(&dialer, http.Header{}) if err != nil { - return fmt.Errorf("%s Unable to connect to Websocket. Error: %s", - b.Name, - err) + return err } - if b.Verbose { log.Debugf("%s Connected to Websocket.\n", b.GetName()) } @@ -59,22 +45,6 @@ func (b *Bitstamp) WsConnect() error { return nil } -// WsReadData reads data coming from bitstamp websocket connection -func (b *Bitstamp) WsReadData() (exchange.WebsocketResponse, error) { - msgType, resp, err := b.WebsocketConn.ReadMessage() - - if err != nil { - return exchange.WebsocketResponse{}, err - } - - if b.Verbose { - log.Debugf("%s websocket raw response: %s", b.GetName(), resp) - } - - b.Websocket.TrafficAlert <- struct{}{} - return exchange.WebsocketResponse{Type: msgType, Raw: resp}, nil -} - // WsHandleData handles websocket data from WsReadData func (b *Bitstamp) WsHandleData() { b.Websocket.Wg.Add(1) @@ -89,12 +59,12 @@ func (b *Bitstamp) WsHandleData() { return default: - resp, err := b.WsReadData() + resp, err := b.WebsocketConn.ReadMessage() if err != nil { b.Websocket.DataHandler <- err return } - + b.Websocket.TrafficAlert <- struct{}{} wsResponse := websocketResponse{} err = common.JSONDecode(resp.Raw, &wsResponse) if err != nil { @@ -138,7 +108,7 @@ func (b *Bitstamp) WsHandleData() { currencyPair := common.SplitStrings(wsResponse.Channel, "_") p := currency.NewPairFromString(common.StringToUpper(currencyPair[2])) - b.Websocket.DataHandler <- exchange.TradeData{ + b.Websocket.DataHandler <- wshandler.TradeData{ Price: wsTradeTemp.Data.Price, Amount: wsTradeTemp.Data.Amount, CurrencyPair: p, @@ -153,10 +123,10 @@ func (b *Bitstamp) WsHandleData() { func (b *Bitstamp) generateDefaultSubscriptions() { var channels = []string{"live_trades_", "diff_order_book_"} enabledCurrencies := b.GetEnabledCurrencies() - var subscriptions []exchange.WebsocketChannelSubscription + var subscriptions []wshandler.WebsocketChannelSubscription for i := range channels { for j := range enabledCurrencies { - subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ Channel: fmt.Sprintf("%v%v", channels[i], enabledCurrencies[j].Lower().String()), }) } @@ -165,31 +135,25 @@ func (b *Bitstamp) generateDefaultSubscriptions() { } // Subscribe sends a websocket message to receive data from the channel -func (b *Bitstamp) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { - b.wsRequestMtx.Lock() - defer b.wsRequestMtx.Unlock() - +func (b *Bitstamp) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { req := websocketEventRequest{ Event: "bts:subscribe", Data: websocketData{ Channel: channelToSubscribe.Channel, }, } - return b.WebsocketConn.WriteJSON(req) + return b.WebsocketConn.SendMessage(req) } // Unsubscribe sends a websocket message to stop receiving data from the channel -func (b *Bitstamp) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { - b.wsRequestMtx.Lock() - defer b.wsRequestMtx.Unlock() - +func (b *Bitstamp) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { req := websocketEventRequest{ Event: "bts:unsubscribe", Data: websocketData{ Channel: channelToSubscribe.Channel, }, } - return b.WebsocketConn.WriteJSON(req) + return b.WebsocketConn.SendMessage(req) } func (b *Bitstamp) wsUpdateOrderbook(ob websocketOrderBook, p currency.Pair, assetType string) error { @@ -240,7 +204,7 @@ func (b *Bitstamp) wsUpdateOrderbook(ob websocketOrderBook, p currency.Pair, ass return err } - b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Pair: p, Asset: assetType, Exchange: b.GetName(), @@ -284,7 +248,7 @@ func (b *Bitstamp) seedOrderBook() error { return err } - b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Pair: p[x], Asset: ticker.Spot, Exchange: b.GetName(), diff --git a/exchanges/bitstamp/bitstamp_wrapper.go b/exchanges/bitstamp/bitstamp_wrapper.go index 2f1a2ceb..6f3b0a04 100644 --- a/exchanges/bitstamp/bitstamp_wrapper.go +++ b/exchanges/bitstamp/bitstamp_wrapper.go @@ -13,6 +13,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" ) @@ -305,7 +306,7 @@ func (b *Bitstamp) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchang } // GetWebsocket returns a pointer to the exchange websocket -func (b *Bitstamp) GetWebsocket() (*exchange.Websocket, error) { +func (b *Bitstamp) GetWebsocket() (*wshandler.Websocket, error) { return b.Websocket, nil } @@ -405,20 +406,20 @@ func (b *Bitstamp) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) // SubscribeToWebsocketChannels appends to ChannelsToSubscribe // which lets websocket.manageSubscriptions handle subscribing -func (b *Bitstamp) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (b *Bitstamp) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { b.Websocket.SubscribeToChannels(channels) return nil } // UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe // which lets websocket.manageSubscriptions handle unsubscribing -func (b *Bitstamp) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { - b.Websocket.UnsubscribeToChannels(channels) +func (b *Bitstamp) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { + b.Websocket.RemoveSubscribedChannels(channels) return nil } // GetSubscriptions returns a copied list of subscriptions -func (b *Bitstamp) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) { +func (b *Bitstamp) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { return b.Websocket.GetSubscriptions(), nil } diff --git a/exchanges/bittrex/bittrex.go b/exchanges/bittrex/bittrex.go index 68a835a7..2fc65896 100644 --- a/exchanges/bittrex/bittrex.go +++ b/exchanges/bittrex/bittrex.go @@ -14,6 +14,7 @@ import ( 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" ) @@ -85,7 +86,7 @@ func (b *Bittrex) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) b.APIUrlDefault = bittrexAPIURL b.APIUrl = b.APIUrlDefault - b.WebsocketInit() + b.Websocket = wshandler.New() } // Setup method sets current configuration details if enabled diff --git a/exchanges/bittrex/bittrex_wrapper.go b/exchanges/bittrex/bittrex_wrapper.go index cbb0ebea..4645b899 100644 --- a/exchanges/bittrex/bittrex_wrapper.go +++ b/exchanges/bittrex/bittrex_wrapper.go @@ -12,6 +12,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" ) @@ -294,7 +295,7 @@ func (b *Bittrex) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange } // GetWebsocket returns a pointer to the exchange websocket -func (b *Bittrex) GetWebsocket() (*exchange.Websocket, error) { +func (b *Bittrex) GetWebsocket() (*wshandler.Websocket, error) { return nil, common.ErrNotYetImplemented } @@ -400,18 +401,18 @@ func (b *Bittrex) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ( // SubscribeToWebsocketChannels appends to ChannelsToSubscribe // which lets websocket.manageSubscriptions handle subscribing -func (b *Bittrex) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (b *Bittrex) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { return common.ErrFunctionNotSupported } // UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe // which lets websocket.manageSubscriptions handle unsubscribing -func (b *Bittrex) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (b *Bittrex) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { return common.ErrFunctionNotSupported } // GetSubscriptions returns a copied list of subscriptions -func (b *Bittrex) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) { +func (b *Bittrex) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/btcmarkets/btcmarkets.go b/exchanges/btcmarkets/btcmarkets.go index b70e4c52..4d7294d9 100644 --- a/exchanges/btcmarkets/btcmarkets.go +++ b/exchanges/btcmarkets/btcmarkets.go @@ -14,6 +14,7 @@ import ( 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" ) @@ -74,7 +75,7 @@ func (b *BTCMarkets) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) b.APIUrlDefault = btcMarketsAPIURL b.APIUrl = b.APIUrlDefault - b.WebsocketInit() + b.Websocket = wshandler.New() } // Setup takes in an exchange configuration and sets all parameters diff --git a/exchanges/btcmarkets/btcmarkets_wrapper.go b/exchanges/btcmarkets/btcmarkets_wrapper.go index 003f6e14..d05caa93 100644 --- a/exchanges/btcmarkets/btcmarkets_wrapper.go +++ b/exchanges/btcmarkets/btcmarkets_wrapper.go @@ -13,6 +13,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" ) @@ -322,7 +323,7 @@ func (b *BTCMarkets) WithdrawFiatFundsToInternationalBank(withdrawRequest *excha } // GetWebsocket returns a pointer to the exchange websocket -func (b *BTCMarkets) GetWebsocket() (*exchange.Websocket, error) { +func (b *BTCMarkets) GetWebsocket() (*wshandler.Websocket, error) { return nil, common.ErrNotYetImplemented } @@ -463,18 +464,18 @@ func (b *BTCMarkets) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest // SubscribeToWebsocketChannels appends to ChannelsToSubscribe // which lets websocket.manageSubscriptions handle subscribing -func (b *BTCMarkets) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (b *BTCMarkets) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { return common.ErrFunctionNotSupported } // UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe // which lets websocket.manageSubscriptions handle unsubscribing -func (b *BTCMarkets) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (b *BTCMarkets) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { return common.ErrFunctionNotSupported } // GetSubscriptions returns a copied list of subscriptions -func (b *BTCMarkets) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) { +func (b *BTCMarkets) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/btse/btse.go b/exchanges/btse/btse.go index b71c7dd8..f066324f 100644 --- a/exchanges/btse/btse.go +++ b/exchanges/btse/btse.go @@ -6,24 +6,22 @@ import ( "net/http" "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" ) // BTSE is the overarching type across this package type BTSE struct { exchange.Base - WebsocketConn *websocket.Conn - wsRequestMtx sync.Mutex + WebsocketConn *wshandler.WebsocketConnection } const ( @@ -66,11 +64,13 @@ func (b *BTSE) SetDefaults() { b.APIUrl = b.APIUrlDefault b.SupportsAutoPairUpdating = true b.SupportsRESTTickerBatching = false - b.WebsocketInit() - b.Websocket.Functionality = exchange.WebsocketOrderbookSupported | - exchange.WebsocketTickerSupported | - exchange.WebsocketSubscribeSupported | - exchange.WebsocketUnsubscribeSupported + b.Websocket = wshandler.New() + b.Websocket.Functionality = wshandler.WebsocketOrderbookSupported | + wshandler.WebsocketTickerSupported | + wshandler.WebsocketSubscribeSupported | + wshandler.WebsocketUnsubscribeSupported + b.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + b.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout } // Setup takes in the supplied exchange configuration details and sets params @@ -109,17 +109,26 @@ func (b *BTSE) Setup(exch *config.ExchangeConfig) { if err != nil { log.Fatal(err) } - err = b.WebsocketSetup(b.WsConnect, + err = b.Websocket.Setup(b.WsConnect, b.Subscribe, b.Unsubscribe, exch.Name, exch.Websocket, exch.Verbose, btseWebsocket, - exch.WebsocketURL) + exch.WebsocketURL, + exch.AuthenticatedWebsocketAPISupport) if err != nil { log.Fatal(err) } + b.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: b.Name, + URL: b.Websocket.GetWebsocketURL(), + ProxyURL: b.Websocket.GetProxyAddress(), + Verbose: b.Verbose, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } } } diff --git a/exchanges/btse/btse_websocket.go b/exchanges/btse/btse_websocket.go index a044bca6..4f574f09 100644 --- a/exchanges/btse/btse_websocket.go +++ b/exchanges/btse/btse_websocket.go @@ -2,9 +2,7 @@ package btse import ( "errors" - "fmt" "net/http" - "net/url" "strconv" "strings" "time" @@ -12,8 +10,8 @@ import ( "github.com/gorilla/websocket" "github.com/thrasher-/gocryptotrader/common" "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" ) @@ -24,27 +22,12 @@ const ( // WsConnect connects the websocket client func (b *BTSE) WsConnect() error { if !b.Websocket.IsEnabled() || !b.IsEnabled() { - return errors.New(exchange.WebsocketNotEnabled) + return errors.New(wshandler.WebsocketNotEnabled) } - var dialer websocket.Dialer - - if b.Websocket.GetProxyAddress() != "" { - proxy, err := url.Parse(b.Websocket.GetProxyAddress()) - if err != nil { - return fmt.Errorf("%s websocket error - proxy address %s", - b.Name, err) - } - - dialer.Proxy = http.ProxyURL(proxy) - } - - var err error - b.WebsocketConn, _, err = dialer.Dial(b.Websocket.GetWebsocketURL(), - http.Header{}) + err := b.WebsocketConn.Dial(&dialer, http.Header{}) if err != nil { - return fmt.Errorf("%s websocket error - unable to connect %s", - b.Name, err) + return err } go b.WsHandleData() @@ -53,17 +36,6 @@ func (b *BTSE) WsConnect() error { return nil } -// WsReadData reads data from the websocket connection -func (b *BTSE) WsReadData() (exchange.WebsocketResponse, error) { - _, resp, err := b.WebsocketConn.ReadMessage() - if err != nil { - return exchange.WebsocketResponse{}, err - } - - b.Websocket.TrafficAlert <- struct{}{} - return exchange.WebsocketResponse{Raw: resp}, nil -} - // WsHandleData handles read data from websocket connection func (b *BTSE) WsHandleData() { b.Websocket.Wg.Add(1) @@ -78,11 +50,12 @@ func (b *BTSE) WsHandleData() { return default: - resp, err := b.WsReadData() + resp, err := b.WebsocketConn.ReadMessage() if err != nil { b.Websocket.DataHandler <- err return } + b.Websocket.TrafficAlert <- struct{}{} type MsgType struct { Type string `json:"type"` @@ -118,7 +91,7 @@ func (b *BTSE) WsHandleData() { continue } - b.Websocket.DataHandler <- exchange.TickerData{ + b.Websocket.DataHandler <- wshandler.TickerData{ Timestamp: time.Now(), Pair: currency.NewPairDelimiter(t.ProductID, "-"), AssetType: "SPOT", @@ -191,7 +164,7 @@ func (b *BTSE) wsProcessSnapshot(snapshot *websocketOrderbookSnapshot) error { return err } - b.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + b.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Pair: p, Asset: "SPOT", Exchange: b.GetName(), @@ -204,10 +177,10 @@ func (b *BTSE) wsProcessSnapshot(snapshot *websocketOrderbookSnapshot) error { func (b *BTSE) GenerateDefaultSubscriptions() { var channels = []string{"snapshot", "ticker"} enabledCurrencies := b.GetEnabledCurrencies() - var subscriptions []exchange.WebsocketChannelSubscription + var subscriptions []wshandler.WebsocketChannelSubscription for i := range channels { for j := range enabledCurrencies { - subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ Channel: channels[i], Currency: enabledCurrencies[j], }) @@ -217,7 +190,7 @@ func (b *BTSE) GenerateDefaultSubscriptions() { } // Subscribe sends a websocket message to receive data from the channel -func (b *BTSE) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { +func (b *BTSE) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { subscribe := websocketSubscribe{ Type: "subscribe", Channels: []websocketChannel{ @@ -227,11 +200,11 @@ func (b *BTSE) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscriptio }, }, } - return b.wsSend(subscribe) + return b.WebsocketConn.SendMessage(subscribe) } // Unsubscribe sends a websocket message to stop receiving data from the channel -func (b *BTSE) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { +func (b *BTSE) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { subscribe := websocketSubscribe{ Type: "unsubscribe", Channels: []websocketChannel{ @@ -241,19 +214,5 @@ func (b *BTSE) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscript }, }, } - return b.wsSend(subscribe) -} - -// WsSend sends data to the websocket server -func (b *BTSE) wsSend(data interface{}) error { - b.wsRequestMtx.Lock() - defer b.wsRequestMtx.Unlock() - if b.Verbose { - log.Debugf("%v sending message to websocket %v", b.Name, data) - } - json, err := common.JSONEncode(data) - if err != nil { - return err - } - return b.WebsocketConn.WriteMessage(websocket.TextMessage, json) + return b.WebsocketConn.SendMessage(subscribe) } diff --git a/exchanges/btse/btse_wrapper.go b/exchanges/btse/btse_wrapper.go index 6511289b..ec05e07d 100644 --- a/exchanges/btse/btse_wrapper.go +++ b/exchanges/btse/btse_wrapper.go @@ -12,6 +12,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" ) @@ -284,7 +285,7 @@ func (b *BTSE) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.Wi } // GetWebsocket returns a pointer to the exchange websocket -func (b *BTSE) GetWebsocket() (*exchange.Websocket, error) { +func (b *BTSE) GetWebsocket() (*wshandler.Websocket, error) { return b.Websocket, nil } @@ -361,20 +362,20 @@ func (b *BTSE) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { // SubscribeToWebsocketChannels appends to ChannelsToSubscribe // which lets websocket.manageSubscriptions handle subscribing -func (b *BTSE) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (b *BTSE) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { b.Websocket.SubscribeToChannels(channels) return nil } // UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe // which lets websocket.manageSubscriptions handle unsubscribing -func (b *BTSE) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { - b.Websocket.UnsubscribeToChannels(channels) +func (b *BTSE) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { + b.Websocket.RemoveSubscribedChannels(channels) return nil } // GetSubscriptions returns a copied list of subscriptions -func (b *BTSE) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) { +func (b *BTSE) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { return b.Websocket.GetSubscriptions(), nil } diff --git a/exchanges/coinbasepro/coinbasepro.go b/exchanges/coinbasepro/coinbasepro.go index d60df3a7..c4ef65fa 100644 --- a/exchanges/coinbasepro/coinbasepro.go +++ b/exchanges/coinbasepro/coinbasepro.go @@ -8,16 +8,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" ) @@ -61,8 +60,7 @@ const ( // CoinbasePro is the overarching type across the coinbasepro package type CoinbasePro struct { exchange.Base - WebsocketConn *websocket.Conn - wsRequestMtx sync.Mutex + WebsocketConn *wshandler.WebsocketConnection } // SetDefaults sets default values for the exchange @@ -88,12 +86,15 @@ func (c *CoinbasePro) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) c.APIUrlDefault = coinbaseproAPIURL c.APIUrl = c.APIUrlDefault - c.WebsocketInit() - c.Websocket.Functionality = exchange.WebsocketTickerSupported | - exchange.WebsocketOrderbookSupported | - exchange.WebsocketSubscribeSupported | - exchange.WebsocketUnsubscribeSupported | - exchange.WebsocketAuthenticatedEndpointsSupported + c.Websocket = wshandler.New() + c.Websocket.Functionality = wshandler.WebsocketTickerSupported | + wshandler.WebsocketOrderbookSupported | + wshandler.WebsocketSubscribeSupported | + wshandler.WebsocketUnsubscribeSupported | + wshandler.WebsocketAuthenticatedEndpointsSupported | + wshandler.WebsocketSequenceNumberSupported + c.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + c.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout } // Setup initialises the exchange parameters with the current configuration @@ -137,17 +138,26 @@ func (c *CoinbasePro) Setup(exch *config.ExchangeConfig) { if err != nil { log.Fatal(err) } - err = c.WebsocketSetup(c.WsConnect, + err = c.Websocket.Setup(c.WsConnect, c.Subscribe, c.Unsubscribe, exch.Name, exch.Websocket, exch.Verbose, coinbaseproWebsocketURL, - exch.WebsocketURL) + exch.WebsocketURL, + exch.AuthenticatedWebsocketAPISupport) if err != nil { log.Fatal(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, + } } } diff --git a/exchanges/coinbasepro/coinbasepro_test.go b/exchanges/coinbasepro/coinbasepro_test.go index 52048b90..3fb92a81 100644 --- a/exchanges/coinbasepro/coinbasepro_test.go +++ b/exchanges/coinbasepro/coinbasepro_test.go @@ -10,6 +10,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" ) var c CoinbasePro @@ -585,20 +586,24 @@ func TestWsAuth(t *testing.T) { c.SetDefaults() TestSetup(t) if !c.Websocket.IsEnabled() && !c.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() { - t.Skip(exchange.WebsocketNotEnabled) + t.Skip(wshandler.WebsocketNotEnabled) + } + c.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: c.Name, + URL: c.Websocket.GetWebsocketURL(), + Verbose: c.Verbose, + ResponseMaxLimit: exchange.DefaultWebsocketResponseMaxLimit, + ResponseCheckTimeout: exchange.DefaultWebsocketResponseCheckTimeout, } - var err error var dialer websocket.Dialer - c.WebsocketConn, _, err = dialer.Dial(c.Websocket.GetWebsocketURL(), - http.Header{}) + err := c.WebsocketConn.Dial(&dialer, http.Header{}) if err != nil { t.Fatal(err) } c.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() c.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() go c.WsHandleData() - defer c.WebsocketConn.Close() - err = c.Subscribe(exchange.WebsocketChannelSubscription{ + err = c.Subscribe(wshandler.WebsocketChannelSubscription{ Channel: "user", Currency: currency.NewPairFromString("BTC-USD"), }) diff --git a/exchanges/coinbasepro/coinbasepro_websocket.go b/exchanges/coinbasepro/coinbasepro_websocket.go index fe24470c..582cdaff 100644 --- a/exchanges/coinbasepro/coinbasepro_websocket.go +++ b/exchanges/coinbasepro/coinbasepro_websocket.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "net/http" - "net/url" "strconv" "time" @@ -13,7 +12,7 @@ import ( "github.com/thrasher-/gocryptotrader/currency" exchange "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/orderbook" - log "github.com/thrasher-/gocryptotrader/logger" + "github.com/thrasher-/gocryptotrader/exchanges/wshandler" ) const ( @@ -23,27 +22,12 @@ const ( // WsConnect initiates a websocket connection func (c *CoinbasePro) WsConnect() error { if !c.Websocket.IsEnabled() || !c.IsEnabled() { - return errors.New(exchange.WebsocketNotEnabled) + return errors.New(wshandler.WebsocketNotEnabled) } - var dialer websocket.Dialer - - if c.Websocket.GetProxyAddress() != "" { - proxy, err := url.Parse(c.Websocket.GetProxyAddress()) - if err != nil { - return fmt.Errorf("coinbasepro_websocket.go error - proxy address %s", - err) - } - - dialer.Proxy = http.ProxyURL(proxy) - } - - var err error - c.WebsocketConn, _, err = dialer.Dial(c.Websocket.GetWebsocketURL(), - http.Header{}) + err := c.WebsocketConn.Dial(&dialer, http.Header{}) if err != nil { - return fmt.Errorf("coinbasepro_websocket.go error - unable to connect to websocket %s", - err) + return err } c.GenerateDefaultSubscriptions() @@ -52,16 +36,6 @@ func (c *CoinbasePro) WsConnect() error { return nil } -// WsReadData reads data from the websocket connection -func (c *CoinbasePro) WsReadData() (exchange.WebsocketResponse, error) { - _, resp, err := c.WebsocketConn.ReadMessage() - if err != nil { - return exchange.WebsocketResponse{}, err - } - c.Websocket.TrafficAlert <- struct{}{} - return exchange.WebsocketResponse{Raw: resp}, nil -} - // WsHandleData handles read data from websocket connection func (c *CoinbasePro) WsHandleData() { c.Websocket.Wg.Add(1) @@ -75,11 +49,13 @@ func (c *CoinbasePro) WsHandleData() { case <-c.Websocket.ShutdownC: return default: - resp, err := c.WsReadData() + resp, err := c.WebsocketConn.ReadMessage() if err != nil { c.Websocket.DataHandler <- err return } + c.Websocket.TrafficAlert <- struct{}{} + type MsgType struct { Type string `json:"type"` Sequence int64 `json:"sequence"` @@ -109,7 +85,7 @@ func (c *CoinbasePro) WsHandleData() { continue } - c.Websocket.DataHandler <- exchange.TickerData{ + c.Websocket.DataHandler <- wshandler.TickerData{ Timestamp: ticker.Time, Pair: currency.NewPairFromString(ticker.ProductID), AssetType: "SPOT", @@ -240,7 +216,7 @@ func (c *CoinbasePro) ProcessSnapshot(snapshot *WebsocketOrderbookSnapshot) erro return err } - c.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + c.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Pair: pair, Asset: "SPOT", Exchange: c.GetName(), @@ -275,7 +251,7 @@ func (c *CoinbasePro) ProcessUpdate(update WebsocketL2Update) error { return err } - c.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + c.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Pair: p, Asset: "SPOT", Exchange: c.GetName(), @@ -288,14 +264,14 @@ func (c *CoinbasePro) ProcessUpdate(update WebsocketL2Update) error { func (c *CoinbasePro) GenerateDefaultSubscriptions() { var channels = []string{"heartbeat", "level2", "ticker", "user"} enabledCurrencies := c.GetEnabledCurrencies() - var subscriptions []exchange.WebsocketChannelSubscription + var subscriptions []wshandler.WebsocketChannelSubscription for i := range channels { if (channels[i] == "user" || channels[i] == "full") && !c.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { continue } for j := range enabledCurrencies { enabledCurrencies[j].Delimiter = "-" - subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ Channel: channels[i], Currency: enabledCurrencies[j], }) @@ -305,7 +281,7 @@ func (c *CoinbasePro) GenerateDefaultSubscriptions() { } // Subscribe sends a websocket message to receive data from the channel -func (c *CoinbasePro) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { +func (c *CoinbasePro) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { subscribe := WebsocketSubscribe{ Type: "subscribe", Channels: []WsChannels{ @@ -326,11 +302,11 @@ func (c *CoinbasePro) Subscribe(channelToSubscribe exchange.WebsocketChannelSubs subscribe.Passphrase = c.ClientID subscribe.Timestamp = n } - return c.wsSend(subscribe) + return c.WebsocketConn.SendMessage(subscribe) } // Unsubscribe sends a websocket message to stop receiving data from the channel -func (c *CoinbasePro) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { +func (c *CoinbasePro) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { subscribe := WebsocketSubscribe{ Type: "unsubscribe", Channels: []WsChannels{ @@ -342,19 +318,5 @@ func (c *CoinbasePro) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSu }, }, } - return c.wsSend(subscribe) -} - -// WsSend sends data to the websocket server -func (c *CoinbasePro) wsSend(data interface{}) error { - c.wsRequestMtx.Lock() - defer c.wsRequestMtx.Unlock() - if c.Verbose { - log.Debugf("%v sending message to websocket %v", c.Name, data) - } - json, err := common.JSONEncode(data) - if err != nil { - return err - } - return c.WebsocketConn.WriteMessage(websocket.TextMessage, json) + return c.WebsocketConn.SendMessage(subscribe) } diff --git a/exchanges/coinbasepro/coinbasepro_wrapper.go b/exchanges/coinbasepro/coinbasepro_wrapper.go index 611d40f1..91b0fa1a 100644 --- a/exchanges/coinbasepro/coinbasepro_wrapper.go +++ b/exchanges/coinbasepro/coinbasepro_wrapper.go @@ -12,6 +12,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" ) @@ -282,7 +283,7 @@ func (c *CoinbasePro) WithdrawFiatFundsToInternationalBank(withdrawRequest *exch } // GetWebsocket returns a pointer to the exchange websocket -func (c *CoinbasePro) GetWebsocket() (*exchange.Websocket, error) { +func (c *CoinbasePro) GetWebsocket() (*wshandler.Websocket, error) { return c.Websocket, nil } @@ -385,20 +386,20 @@ func (c *CoinbasePro) GetOrderHistory(getOrdersRequest *exchange.GetOrdersReques // SubscribeToWebsocketChannels appends to ChannelsToSubscribe // which lets websocket.manageSubscriptions handle subscribing -func (c *CoinbasePro) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (c *CoinbasePro) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { c.Websocket.SubscribeToChannels(channels) return nil } // UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe // which lets websocket.manageSubscriptions handle unsubscribing -func (c *CoinbasePro) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { - c.Websocket.UnsubscribeToChannels(channels) +func (c *CoinbasePro) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { + c.Websocket.RemoveSubscribedChannels(channels) return nil } // GetSubscriptions returns a copied list of subscriptions -func (c *CoinbasePro) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) { +func (c *CoinbasePro) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { return c.Websocket.GetSubscriptions(), nil } diff --git a/exchanges/coinut/coinut.go b/exchanges/coinut/coinut.go index de9a05f5..eb8049e2 100644 --- a/exchanges/coinut/coinut.go +++ b/exchanges/coinut/coinut.go @@ -6,16 +6,15 @@ import ( "errors" "fmt" "net/http" - "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" ) @@ -47,9 +46,8 @@ const ( // COINUT is the overarching type across the coinut package type COINUT struct { exchange.Base - WebsocketConn *websocket.Conn + WebsocketConn *wshandler.WebsocketConnection InstrumentMap map[string]int - wsRequestMtx sync.Mutex } // SetDefaults sets current default values @@ -76,15 +74,18 @@ func (c *COINUT) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) c.APIUrlDefault = coinutAPIURL c.APIUrl = c.APIUrlDefault - c.WebsocketInit() - c.Websocket.Functionality = exchange.WebsocketTickerSupported | - exchange.WebsocketOrderbookSupported | - exchange.WebsocketTradeDataSupported | - exchange.WebsocketSubscribeSupported | - exchange.WebsocketUnsubscribeSupported | - exchange.WebsocketAuthenticatedEndpointsSupported | - exchange.WebsocketSubmitOrderSupported | - exchange.WebsocketCancelOrderSupported + c.Websocket = wshandler.New() + c.Websocket.Functionality = wshandler.WebsocketTickerSupported | + wshandler.WebsocketOrderbookSupported | + wshandler.WebsocketTradeDataSupported | + wshandler.WebsocketSubscribeSupported | + wshandler.WebsocketUnsubscribeSupported | + wshandler.WebsocketAuthenticatedEndpointsSupported | + wshandler.WebsocketSubmitOrderSupported | + wshandler.WebsocketCancelOrderSupported | + wshandler.WebsocketMessageCorrelationSupported + c.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + c.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout } // Setup sets the current exchange configuration @@ -125,17 +126,27 @@ func (c *COINUT) Setup(exch *config.ExchangeConfig) { if err != nil { log.Fatal(err) } - err = c.WebsocketSetup(c.WsConnect, + err = c.Websocket.Setup(c.WsConnect, c.Subscribe, c.Unsubscribe, exch.Name, exch.Websocket, exch.Verbose, coinutWebsocketURL, - exch.WebsocketURL) + exch.WebsocketURL, + exch.AuthenticatedWebsocketAPISupport) if err != nil { log.Fatal(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, + RateLimit: coinutWebsocketRateLimit, + } } } diff --git a/exchanges/coinut/coinut_test.go b/exchanges/coinut/coinut_test.go index 553ea098..285b1b93 100644 --- a/exchanges/coinut/coinut_test.go +++ b/exchanges/coinut/coinut_test.go @@ -11,6 +11,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" ) var c COINUT @@ -55,12 +56,18 @@ func setupWSTestAuth(t *testing.T) { c.SetDefaults() TestSetup(t) if !c.Websocket.IsEnabled() && !c.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() { - t.Skip(exchange.WebsocketNotEnabled) + t.Skip(wshandler.WebsocketNotEnabled) + } + c.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: c.Name, + URL: coinutWebsocketURL, + Verbose: c.Verbose, + RateLimit: coinutWebsocketRateLimit, + ResponseMaxLimit: exchange.DefaultWebsocketResponseMaxLimit, + ResponseCheckTimeout: exchange.DefaultWebsocketResponseCheckTimeout, } - var err error var dialer websocket.Dialer - c.WebsocketConn, _, err = dialer.Dial(c.Websocket.GetWebsocketURL(), - http.Header{}) + err := c.WebsocketConn.Dial(&dialer, http.Header{}) if err != nil { t.Fatal(err) } @@ -72,17 +79,6 @@ func setupWSTestAuth(t *testing.T) { t.Error(err) } - timer := time.NewTimer(5 * time.Second) - select { - case resp := <-c.Websocket.DataHandler: - if resp.(WsLoginResponse).Username != clientID { - t.Fatal("Unsuccessful login") - } - case <-timer.C: - t.Fatal("Expected response") - } - timer.Stop() - time.Sleep(2 * time.Second) instrumentListByString = make(map[string]int64) instrumentListByString[currency.NewPair(currency.LTC, currency.BTC).String()] = 1 wsSetupRan = true @@ -448,28 +444,21 @@ func TestGetDepositAddress(t *testing.T) { } } -// TestWsAuthGetAccountBalance dials websocket, sends login request. +// TestWsAuthGetAccountBalance dials websocket, retrieves account balance func TestWsAuthGetAccountBalance(t *testing.T) { setupWSTestAuth(t) - err := c.wsGetAccountBalance() + _, err := c.wsGetAccountBalance() if err != nil { t.Error(err) } - timer := time.NewTimer(sharedtestvalues.WebsocketResponseExtendedTimeout) - select { - case resp := <-c.Websocket.DataHandler: - if resp.(WsUserBalanceResponse).Status[0] != "OK" { - t.Error("Expected successful response") - } - case <-timer.C: - t.Error("Expected response") - } - timer.Stop() } -// TestWsAuthSubmitOrders dials websocket, sends login request. -func TestWsAuthSubmitOrders(t *testing.T) { +// TestWsAuthSubmitOrder dials websocket, submit order +func TestWsAuthSubmitOrder(t *testing.T) { setupWSTestAuth(t) + if !canManipulateRealOrders { + t.Skip("API keys set, canManipulateRealOrders false, skipping test") + } order := WsSubmitOrderParameters{ Amount: 1, Currency: currency.NewPair(currency.LTC, currency.BTC), @@ -477,42 +466,64 @@ func TestWsAuthSubmitOrders(t *testing.T) { Price: 1, Side: exchange.BuyOrderSide, } - err := c.wsSubmitOrders([]WsSubmitOrderParameters{order, order}) + _, err := c.wsSubmitOrder(&order) if err != nil { t.Error(err) } - timer := time.NewTimer(sharedtestvalues.WebsocketResponseExtendedTimeout) - select { - case <-c.Websocket.DataHandler: - case <-timer.C: - t.Error("Expected response") - } - timer.Stop() } -// TestWsAuthCancelOrders dials websocket, sends login request. +// TestWsAuthCancelOrders dials websocket, submit orders +func TestWsAuthSubmitOrders(t *testing.T) { + setupWSTestAuth(t) + if !canManipulateRealOrders { + t.Skip("API keys set, canManipulateRealOrders false, skipping test") + } + order1 := WsSubmitOrderParameters{ + Amount: 1, + Currency: currency.NewPair(currency.LTC, currency.BTC), + OrderID: 1, + Price: 1, + Side: exchange.BuyOrderSide, + } + order2 := WsSubmitOrderParameters{ + Amount: 3, + Currency: currency.NewPair(currency.LTC, currency.BTC), + OrderID: 2, + Price: 2, + Side: exchange.BuyOrderSide, + } + _, err := c.wsSubmitOrders([]WsSubmitOrderParameters{order1, order2}) + if err != nil { + t.Error(err) + } +} + +// TestWsAuthCancelOrders dials websocket, cancels orders func TestWsAuthCancelOrders(t *testing.T) { setupWSTestAuth(t) + if !canManipulateRealOrders { + t.Skip("API keys set, canManipulateRealOrders false, skipping test") + } order := WsCancelOrderParameters{ Currency: currency.NewPair(currency.LTC, currency.BTC), OrderID: 1, } - err := c.wsCancelOrders([]WsCancelOrderParameters{order, order}) - if err != nil { - t.Error(err) + order2 := WsCancelOrderParameters{ + Currency: currency.NewPair(currency.LTC, currency.BTC), + OrderID: 2, } - timer := time.NewTimer(sharedtestvalues.WebsocketResponseExtendedTimeout) - select { - case <-c.Websocket.DataHandler: - case <-timer.C: - t.Error("Expected response") + _, errs := c.wsCancelOrders([]WsCancelOrderParameters{order, order2}) + if len(errs) > 0 { + t.Error(errs) } - timer.Stop() } -// TestWsAuthCancelOrder dials websocket, sends login request. +// TestWsAuthCancelOrder dials websocket, cancels order func TestWsAuthCancelOrder(t *testing.T) { setupWSTestAuth(t) + if !canManipulateRealOrders { + t.Skip("API keys set, canManipulateRealOrders false, skipping test") + } order := WsCancelOrderParameters{ Currency: currency.NewPair(currency.LTC, currency.BTC), OrderID: 1, @@ -521,27 +532,13 @@ func TestWsAuthCancelOrder(t *testing.T) { if err != nil { t.Error(err) } - timer := time.NewTimer(sharedtestvalues.WebsocketResponseExtendedTimeout) - select { - case <-c.Websocket.DataHandler: - case <-timer.C: - t.Error("Expected response") - } - timer.Stop() } -// TestWsAuthGetOpenOrders dials websocket, sends login request. +// TestWsAuthGetOpenOrders dials websocket, retrieves open orders func TestWsAuthGetOpenOrders(t *testing.T) { setupWSTestAuth(t) err := c.wsGetOpenOrders(currency.NewPair(currency.LTC, currency.BTC)) if err != nil { t.Error(err) } - timer := time.NewTimer(sharedtestvalues.WebsocketResponseExtendedTimeout) - select { - case <-c.Websocket.DataHandler: - case <-timer.C: - t.Error("Expected response") - } - timer.Stop() } diff --git a/exchanges/coinut/coinut_types.go b/exchanges/coinut/coinut_types.go index cd708998..5574428c 100644 --- a/exchanges/coinut/coinut_types.go +++ b/exchanges/coinut/coinut_types.go @@ -263,11 +263,12 @@ type wsRequest struct { SecType string `json:"sec_type,omitempty"` InstID int64 `json:"inst_id,omitempty"` TopN int64 `json:"top_n,omitempty"` - Subscribe bool `json:"subscribe"` - Nonce int64 `json:"nonce"` + Subscribe bool `json:"subscribe,omitempty"` + Nonce int64 `json:"nonce,omitempty"` } type wsResponse struct { + Nonce int64 `json:"nonce,omitempty"` Reply string `json:"reply"` } @@ -400,14 +401,14 @@ type WsCancelOrderParameters struct { OrderID int64 } -// WsCancelOrderRequest ws request +// WsCancelOrderRequest data required for cancelling an order type WsCancelOrderRequest struct { InstID int64 `json:"inst_id"` OrderID int64 `json:"order_id"` WsRequest } -// WsCancelOrderResponse ws response +// WsCancelOrderResponse contains cancelled order data type WsCancelOrderResponse struct { Nonce int64 `json:"nonce"` Reply string `json:"reply"` @@ -416,16 +417,20 @@ type WsCancelOrderResponse struct { Status []string `json:"status"` } -// WsCancelOrdersResponse ws response +// WsCancelOrdersResponse contains all cancelled order data type WsCancelOrdersResponse struct { - WsRequest - Entries []WsCancelOrdersResponseEntry `json:"entries"` + Nonce int64 `json:"nonce"` + Reply string `json:"reply"` + Results []WsCancelOrdersResponseData `json:"results"` + Status []string `json:"status"` + TransID int64 `json:"trans_id"` } -// WsCancelOrdersResponseEntry ws response entry -type WsCancelOrdersResponseEntry struct { - InstID int64 `json:"inst_id"` - OrderID int64 `json:"order_id"` +// WsCancelOrdersResponseData individual cancellation response data +type WsCancelOrdersResponseData struct { + InstID int64 `json:"inst_id"` + OrderID int64 `json:"order_id"` + Status string `json:"status"` } // WsGetOpenOrdersRequest ws request @@ -547,6 +552,25 @@ type WsOrderRejectedResponse struct { TransID int64 `json:"trans_id"` } +// WsStandardOrderResponse a standardised order +type WsStandardOrderResponse struct { + InstID int64 + OrderID int64 + ClientOrdID int64 + TransID int64 + Nonce int64 + Status []string + Qty float64 + OpenQty float64 + Price float64 + Side string + Reasons []string + Timestamp int64 + OrderType string + CommissionAmount float64 + CommissionCurrency currency.Pair +} + // WsUserOpenOrdersResponse ws response type WsUserOpenOrdersResponse struct { Nonce int64 `json:"nonce"` @@ -612,3 +636,25 @@ type WsNewOrderResponse struct { Reply string `json:"reply"` Status []string `json:"status"` } + +// WsGetAccountBalanceResponse contains values of each currency +type WsGetAccountBalanceResponse struct { + BCH string `json:"BCH"` + BTC string `json:"BTC"` + BTG string `json:"BTG"` + CAD string `json:"CAD"` + ETC string `json:"ETC"` + ETH string `json:"ETH"` + LCH string `json:"LCH"` + LTC string `json:"LTC"` + MYR string `json:"MYR"` + SGD string `json:"SGD"` + USD string `json:"USD"` + USDT string `json:"USDT"` + XMR string `json:"XMR"` + ZEC string `json:"ZEC"` + Nonce int64 `json:"nonce"` + Reply string `json:"reply"` + Status []string `json:"status"` + TransID int64 `json:"trans_id"` +} diff --git a/exchanges/coinut/coinut_websocket.go b/exchanges/coinut/coinut_websocket.go index bc247edf..f6d65068 100644 --- a/exchanges/coinut/coinut_websocket.go +++ b/exchanges/coinut/coinut_websocket.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "net/http" - "net/url" "strings" "time" @@ -13,11 +12,11 @@ import ( "github.com/thrasher-/gocryptotrader/currency" exchange "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/orderbook" - log "github.com/thrasher-/gocryptotrader/logger" + "github.com/thrasher-/gocryptotrader/exchanges/wshandler" ) const coinutWebsocketURL = "wss://wsapi.coinut.com" -const coinutWebsocketRateLimit = 30 * time.Millisecond +const coinutWebsocketRateLimit = 30 var nNonce map[int64]string var channels map[string]chan []byte @@ -33,32 +32,18 @@ var populatedList bool // WsConnect intiates a websocket connection func (c *COINUT) WsConnect() error { if !c.Websocket.IsEnabled() || !c.IsEnabled() { - return errors.New(exchange.WebsocketNotEnabled) + return errors.New(wshandler.WebsocketNotEnabled) } - - var Dialer websocket.Dialer - - if c.Websocket.GetProxyAddress() != "" { - proxy, err := url.Parse(c.Websocket.GetProxyAddress()) - if err != nil { - return err - } - - Dialer.Proxy = http.ProxyURL(proxy) - } - - var err error - c.WebsocketConn, _, err = Dialer.Dial(c.Websocket.GetWebsocketURL(), - http.Header{}) - + var dialer websocket.Dialer + err := c.WebsocketConn.Dial(&dialer, http.Header{}) if err != nil { return err } + go c.WsHandleData() if !populatedList { instrumentListByString = make(map[string]int64) instrumentListByCode = make(map[int64]string) - err = c.WsSetInstrumentList() if err != nil { return err @@ -72,21 +57,9 @@ func (c *COINUT) WsConnect() error { channels = make(map[string]chan []byte) channels["hb"] = make(chan []byte, 1) - go c.WsHandleData() return nil } -// WsReadData reads data from the websocket connection -func (c *COINUT) WsReadData() (exchange.WebsocketResponse, error) { - _, resp, err := c.WebsocketConn.ReadMessage() - if err != nil { - return exchange.WebsocketResponse{}, err - } - - c.Websocket.TrafficAlert <- struct{}{} - return exchange.WebsocketResponse{Raw: resp}, nil -} - // WsHandleData handles read data func (c *COINUT) WsHandleData() { c.Websocket.Wg.Add(1) @@ -101,11 +74,12 @@ func (c *COINUT) WsHandleData() { return default: - resp, err := c.WsReadData() + resp, err := c.WebsocketConn.ReadMessage() if err != nil { c.Websocket.DataHandler <- err return } + c.Websocket.TrafficAlert <- struct{}{} if strings.HasPrefix(string(resp.Raw), "[") { var incoming []wsResponse @@ -115,6 +89,10 @@ func (c *COINUT) WsHandleData() { continue } for i := range incoming { + if incoming[i].Nonce > 0 { + c.WebsocketConn.AddResponseWithID(incoming[i].Nonce, resp.Raw) + break + } var individualJSON []byte individualJSON, err = common.JSONEncode(incoming[i]) if err != nil { @@ -131,6 +109,7 @@ func (c *COINUT) WsHandleData() { c.Websocket.DataHandler <- err continue } + c.wsProcessResponse(resp.Raw) } @@ -146,15 +125,6 @@ func (c *COINUT) wsProcessResponse(resp []byte) { return } switch incoming.Reply { - case "login": - var login WsLoginResponse - err := common.JSONDecode(resp, &login) - if err != nil { - c.Websocket.DataHandler <- err - return - } - c.Websocket.SetCanUseAuthenticatedEndpoints(login.Username == c.ClientID) - c.Websocket.DataHandler <- login case "hb": channels["hb"] <- resp case "inst_tick": @@ -164,7 +134,7 @@ func (c *COINUT) wsProcessResponse(resp []byte) { c.Websocket.DataHandler <- err return } - c.Websocket.DataHandler <- exchange.TickerData{ + c.Websocket.DataHandler <- wshandler.TickerData{ Timestamp: time.Unix(0, ticker.Timestamp), Exchange: c.GetName(), AssetType: "SPOT", @@ -187,7 +157,7 @@ func (c *COINUT) wsProcessResponse(resp []byte) { return } currencyPair := instrumentListByCode[orderbooksnapshot.InstID] - c.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + c.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Exchange: c.GetName(), Asset: "SPOT", Pair: currency.NewPairFromString(currencyPair), @@ -205,7 +175,7 @@ func (c *COINUT) wsProcessResponse(resp []byte) { return } currencyPair := instrumentListByCode[orderbookUpdate.InstID] - c.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + c.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Exchange: c.GetName(), Asset: "SPOT", Pair: currency.NewPairFromString(currencyPair), @@ -226,7 +196,7 @@ func (c *COINUT) wsProcessResponse(resp []byte) { return } currencyPair := instrumentListByCode[tradeUpdate.InstID] - c.Websocket.DataHandler <- exchange.TradeData{ + c.Websocket.DataHandler <- wshandler.TradeData{ Timestamp: time.Unix(tradeUpdate.Timestamp, 0), CurrencyPair: currency.NewPairFromString(currencyPair), AssetType: "SPOT", @@ -234,78 +204,12 @@ func (c *COINUT) wsProcessResponse(resp []byte) { Price: tradeUpdate.Price, Side: tradeUpdate.Side, } - case "user_balance": - var userBalance WsUserBalanceResponse - err := common.JSONDecode(resp, &userBalance) - if err != nil { - c.Websocket.DataHandler <- err + default: + if incoming.Nonce > 0 { + c.WebsocketConn.AddResponseWithID(incoming.Nonce, resp) return } - c.Websocket.DataHandler <- userBalance - case "new_order": - var newOrder WsNewOrderResponse - err := common.JSONDecode(resp, &newOrder) - if err != nil { - c.Websocket.DataHandler <- err - return - } - c.Websocket.DataHandler <- newOrder - case "order_accepted": - var orderAccepted WsOrderAcceptedResponse - err := common.JSONDecode(resp, &orderAccepted) - if err != nil { - c.Websocket.DataHandler <- err - return - } - c.Websocket.DataHandler <- orderAccepted - case "order_filled": - var orderFilled WsOrderFilledResponse - err := common.JSONDecode(resp, &orderFilled) - if err != nil { - c.Websocket.DataHandler <- err - return - } - c.Websocket.DataHandler <- orderFilled - case "order_rejected": - var orderRejected WsOrderRejectedResponse - err := common.JSONDecode(resp, &orderRejected) - if err != nil { - c.Websocket.DataHandler <- err - return - } - c.Websocket.DataHandler <- orderRejected - case "user_open_orders": - var openOrders WsUserOpenOrdersResponse - err := common.JSONDecode(resp, &openOrders) - if err != nil { - c.Websocket.DataHandler <- err - return - } - c.Websocket.DataHandler <- openOrders - case "trade_history": - var tradeHistory WsTradeHistoryResponse - err := common.JSONDecode(resp, &tradeHistory) - if err != nil { - c.Websocket.DataHandler <- err - return - } - c.Websocket.DataHandler <- tradeHistory - case "cancel_orders": - var cancelOrders WsCancelOrdersResponse - err := common.JSONDecode(resp, &cancelOrders) - if err != nil { - c.Websocket.DataHandler <- err - return - } - c.Websocket.DataHandler <- cancelOrders - case "cancel_order": - var cancelOrder WsCancelOrderResponse - err := common.JSONDecode(resp, &cancelOrder) - if err != nil { - c.Websocket.DataHandler <- err - return - } - c.Websocket.DataHandler <- cancelOrder + c.Websocket.DataHandler <- fmt.Errorf("%v unhandled websocket response: %s", c.Name, resp) } } @@ -322,37 +226,27 @@ func (c *COINUT) GetNonce() int64 { // WsSetInstrumentList fetches instrument list and propagates a local cache func (c *COINUT) WsSetInstrumentList() error { - err := c.wsSend(wsRequest{ + request := wsRequest{ Request: "inst_list", SecType: "SPOT", - Nonce: c.GetNonce(), - }) + Nonce: c.WebsocketConn.GenerateMessageID(false), + } + resp, err := c.WebsocketConn.SendMessageReturnResponse(request.Nonce, request) if err != nil { return err } - - _, resp, err := c.WebsocketConn.ReadMessage() - if err != nil { - return err - } - - c.Websocket.TrafficAlert <- struct{}{} - var list WsInstrumentList err = common.JSONDecode(resp, &list) if err != nil { return err } - for currency, data := range list.Spot { instrumentListByString[currency] = data[0].InstID instrumentListByCode[data[0].InstID] = currency } - if len(instrumentListByString) == 0 || len(instrumentListByCode) == 0 { return errors.New("instrument lists failed to populate") } - return nil } @@ -409,11 +303,11 @@ func (c *COINUT) WsProcessOrderbookUpdate(ob *WsOrderbookUpdate) error { // GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() func (c *COINUT) GenerateDefaultSubscriptions() { var channels = []string{"inst_tick", "inst_order_book"} - var subscriptions []exchange.WebsocketChannelSubscription + var subscriptions []wshandler.WebsocketChannelSubscription enabledCurrencies := c.GetEnabledCurrencies() for i := range channels { for j := range enabledCurrencies { - subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ Channel: channels[i], Currency: enabledCurrencies[j], }) @@ -423,42 +317,37 @@ func (c *COINUT) GenerateDefaultSubscriptions() { } // Subscribe sends a websocket message to receive data from the channel -func (c *COINUT) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { +func (c *COINUT) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { subscribe := wsRequest{ Request: channelToSubscribe.Channel, InstID: instrumentListByString[channelToSubscribe.Currency.String()], Subscribe: true, - Nonce: c.GetNonce(), + Nonce: c.WebsocketConn.GenerateMessageID(false), } - return c.wsSend(subscribe) + return c.WebsocketConn.SendMessage(subscribe) } // Unsubscribe sends a websocket message to stop receiving data from the channel -func (c *COINUT) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { +func (c *COINUT) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { subscribe := wsRequest{ Request: channelToSubscribe.Channel, InstID: instrumentListByString[channelToSubscribe.Currency.String()], Subscribe: false, - Nonce: c.GetNonce(), + Nonce: c.WebsocketConn.GenerateMessageID(false), } - return c.wsSend(subscribe) -} - -// WsSend sends data to the websocket server -func (c *COINUT) wsSend(data interface{}) error { - c.wsRequestMtx.Lock() - defer c.wsRequestMtx.Unlock() - - json, err := common.JSONEncode(data) + resp, err := c.WebsocketConn.SendMessageReturnResponse(subscribe.Nonce, subscribe) if err != nil { return err } - if c.Verbose { - log.Debugf("%v sending message to websocket %v", c.Name, string(json)) + var response map[string]interface{} + err = common.JSONDecode(resp, &response) + if err != nil { + return err } - // Basic rate limiter - time.Sleep(coinutWebsocketRateLimit) - return c.WebsocketConn.WriteMessage(websocket.TextMessage, json) + if response["status"].([]interface{})[0] != "OK" { + return fmt.Errorf("%v unsubscribe failed for channel %v", c.Name, channelToSubscribe.Channel) + } + return nil } func (c *COINUT) wsAuthenticate() error { @@ -466,7 +355,7 @@ func (c *COINUT) wsAuthenticate() error { return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", c.Name) } timestamp := time.Now().Unix() - nonce := c.GetNonce() + nonce := c.WebsocketConn.GenerateMessageID(false) payload := fmt.Sprintf("%v|%v|%v", c.ClientID, timestamp, nonce) hmac := common.GetHMAC(common.HashSHA256, []byte(payload), []byte(c.APIKey)) loginRequest := struct { @@ -483,34 +372,54 @@ func (c *COINUT) wsAuthenticate() error { Timestamp: timestamp, } - err := c.wsSend(loginRequest) + resp, err := c.WebsocketConn.SendMessageReturnResponse(loginRequest.Nonce, loginRequest) if err != nil { - c.Websocket.SetCanUseAuthenticatedEndpoints(false) return err } + var response map[string]interface{} + err = common.JSONDecode(resp, &response) + if err != nil { + return err + } + if response["status"].([]interface{})[0] != "OK" { + c.Websocket.SetCanUseAuthenticatedEndpoints(false) + return fmt.Errorf("%v failed to authenticate", c.Name) + } + c.Websocket.SetCanUseAuthenticatedEndpoints(true) return nil - } -func (c *COINUT) wsGetAccountBalance() error { +func (c *COINUT) wsGetAccountBalance() (*WsGetAccountBalanceResponse, error) { if !c.Websocket.CanUseAuthenticatedEndpoints() { - return fmt.Errorf("%v not authorised to submit order", c.Name) + return nil, fmt.Errorf("%v not authorised to submit order", c.Name) } accBalance := wsRequest{ Request: "user_balance", - Nonce: c.GetNonce(), + Nonce: c.WebsocketConn.GenerateMessageID(false), } - return c.wsSend(accBalance) + resp, err := c.WebsocketConn.SendMessageReturnResponse(accBalance.Nonce, accBalance) + if err != nil { + return nil, err + } + var response WsGetAccountBalanceResponse + err = common.JSONDecode(resp, &response) + if err != nil { + return nil, err + } + if response.Status[0] != "OK" { + return &response, fmt.Errorf("%v get account balance failed", c.Name) + } + return &response, nil } -func (c *COINUT) wsSubmitOrder(order *WsSubmitOrderParameters) error { +func (c *COINUT) wsSubmitOrder(order *WsSubmitOrderParameters) (*WsStandardOrderResponse, error) { if !c.Websocket.CanUseAuthenticatedEndpoints() { - return fmt.Errorf("%v not authorised to submit order", c.Name) + return nil, fmt.Errorf("%v not authorised to submit order", c.Name) } currency := exchange.FormatExchangeCurrency(c.Name, order.Currency).String() var orderSubmissionRequest WsSubmitOrderRequest orderSubmissionRequest.Request = "new_order" - orderSubmissionRequest.Nonce = c.GetNonce() + orderSubmissionRequest.Nonce = c.WebsocketConn.GenerateMessageID(false) orderSubmissionRequest.InstID = instrumentListByString[currency] orderSubmissionRequest.Qty = order.Amount orderSubmissionRequest.Price = order.Price @@ -519,12 +428,100 @@ func (c *COINUT) wsSubmitOrder(order *WsSubmitOrderParameters) error { if order.OrderID > 0 { orderSubmissionRequest.OrderID = order.OrderID } - return c.wsSend(orderSubmissionRequest) + resp, err := c.WebsocketConn.SendMessageReturnResponse(orderSubmissionRequest.Nonce, orderSubmissionRequest) + if err != nil { + return nil, err + } + var standardOrder WsStandardOrderResponse + standardOrder, err = c.wsStandardiseOrderResponse(resp) + if err != nil { + return nil, err + } + if standardOrder.Status[0] != "OK" { + return &standardOrder, fmt.Errorf("%v order submission failed. %v", c.Name, standardOrder) + } + if len(standardOrder.Reasons) > 0 && standardOrder.Reasons[0] != "" { + return &standardOrder, fmt.Errorf("%v order submission failed. %v", c.Name, standardOrder.Reasons[0]) + } + return &standardOrder, nil } -func (c *COINUT) wsSubmitOrders(orders []WsSubmitOrderParameters) error { +func (c *COINUT) wsStandardiseOrderResponse(resp []byte) (WsStandardOrderResponse, error) { + var response WsStandardOrderResponse + var incoming wsResponse + err := common.JSONDecode(resp, &incoming) + if err != nil { + return response, err + } + switch incoming.Reply { + case "order_accepted": + var orderAccepted WsOrderAcceptedResponse + err := common.JSONDecode(resp, &orderAccepted) + if err != nil { + return response, err + } + response = WsStandardOrderResponse{ + InstID: orderAccepted.InstID, + Nonce: orderAccepted.Nonce, + OpenQty: orderAccepted.OpenQty, + OrderID: orderAccepted.OrderID, + OrderType: orderAccepted.Reply, + Price: orderAccepted.OrderPrice, + Qty: orderAccepted.Qty, + Side: orderAccepted.Side, + Status: orderAccepted.Status, + TransID: orderAccepted.TransID, + ClientOrdID: orderAccepted.ClientOrdID, + } + case "order_filled": + var orderFilled WsOrderFilledResponse + err := common.JSONDecode(resp, &orderFilled) + if err != nil { + return response, err + } + response = WsStandardOrderResponse{ + InstID: orderFilled.Order.InstID, + Nonce: orderFilled.Nonce, + OpenQty: orderFilled.Order.OpenQty, + OrderID: orderFilled.Order.OrderID, + OrderType: orderFilled.Reply, + Price: orderFilled.Order.Price, + Qty: orderFilled.Order.Qty, + Side: orderFilled.Order.Side, + Status: orderFilled.Status, + TransID: orderFilled.TransID, + ClientOrdID: orderFilled.Order.ClientOrdID, + } + case "order_rejected": + var orderRejected WsOrderRejectedResponse + err := common.JSONDecode(resp, &orderRejected) + if err != nil { + return response, err + } + response = WsStandardOrderResponse{ + InstID: orderRejected.InstID, + Nonce: orderRejected.Nonce, + OpenQty: orderRejected.OpenQty, + OrderID: orderRejected.OrderID, + OrderType: orderRejected.Reply, + Price: orderRejected.Price, + Qty: orderRejected.Qty, + Side: orderRejected.Side, + Status: orderRejected.Status, + TransID: orderRejected.TransID, + ClientOrdID: orderRejected.ClientOrdID, + Reasons: orderRejected.Reasons, + } + } + return response, nil +} + +func (c *COINUT) wsSubmitOrders(orders []WsSubmitOrderParameters) ([]WsStandardOrderResponse, []error) { + var errors []error + var ordersResponse []WsStandardOrderResponse if !c.Websocket.CanUseAuthenticatedEndpoints() { - return fmt.Errorf("%v not authorised to submit orders", c.Name) + errors = append(errors, fmt.Errorf("%v not authorised to submit orders", c.Name)) + return nil, errors } orderRequest := WsSubmitOrdersRequest{} for i := range orders { @@ -539,9 +536,48 @@ func (c *COINUT) wsSubmitOrders(orders []WsSubmitOrderParameters) error { }) } - orderRequest.Nonce = c.GetNonce() + orderRequest.Nonce = c.WebsocketConn.GenerateMessageID(false) orderRequest.Request = "new_orders" - return c.wsSend(orderRequest) + resp, err := c.WebsocketConn.SendMessageReturnResponse(orderRequest.Nonce, orderRequest) + if err != nil { + errors = append(errors, err) + return nil, errors + } + var incoming []interface{} + err = common.JSONDecode(resp, &incoming) + if err != nil { + errors = append(errors, err) + return nil, errors + } + for i := range incoming { + var individualJSON []byte + individualJSON, err = common.JSONEncode(incoming[i]) + if err != nil { + errors = append(errors, err) + continue + } + standardOrder, err := c.wsStandardiseOrderResponse(individualJSON) + if err != nil { + errors = append(errors, err) + continue + } + if standardOrder.Status[0] != "OK" { + errors = append(errors, fmt.Errorf("%v order submission failed. %v", c.Name, standardOrder)) + continue + } + if len(standardOrder.Reasons) > 0 && standardOrder.Reasons[0] != "" { + errors = append(errors, fmt.Errorf("%v order submission failed for currency %v and orderID %v, message %v ", + c.Name, + instrumentListByCode[standardOrder.InstID], + standardOrder.OrderID, + standardOrder.Reasons[0])) + + continue + } + ordersResponse = append(ordersResponse, standardOrder) + } + + return ordersResponse, errors } func (c *COINUT) wsGetOpenOrders(p currency.Pair) error { @@ -551,10 +587,24 @@ func (c *COINUT) wsGetOpenOrders(p currency.Pair) error { currency := exchange.FormatExchangeCurrency(c.Name, p).String() var openOrdersRequest WsGetOpenOrdersRequest openOrdersRequest.Request = "user_open_orders" - openOrdersRequest.Nonce = c.GetNonce() + openOrdersRequest.Nonce = c.WebsocketConn.GenerateMessageID(false) openOrdersRequest.InstID = instrumentListByString[currency] - return c.wsSend(openOrdersRequest) + resp, err := c.WebsocketConn.SendMessageReturnResponse(openOrdersRequest.Nonce, openOrdersRequest) + if err != nil { + return err + } + var response map[string]interface{} + err = common.JSONDecode(resp, &response) + if err != nil { + return err + } + if response["status"].([]interface{})[0] != "OK" { + return fmt.Errorf("%v get open orders failed for currency %v", + c.Name, + p) + } + return nil } func (c *COINUT) wsCancelOrder(cancellation WsCancelOrderParameters) error { @@ -566,14 +616,31 @@ func (c *COINUT) wsCancelOrder(cancellation WsCancelOrderParameters) error { cancellationRequest.Request = "cancel_order" cancellationRequest.InstID = instrumentListByString[currency] cancellationRequest.OrderID = cancellation.OrderID - cancellationRequest.Nonce = c.GetNonce() + cancellationRequest.Nonce = c.WebsocketConn.GenerateMessageID(false) - return c.wsSend(cancellationRequest) + resp, err := c.WebsocketConn.SendMessageReturnResponse(cancellationRequest.Nonce, cancellationRequest) + if err != nil { + return err + } + var response map[string]interface{} + err = common.JSONDecode(resp, &response) + if err != nil { + return err + } + if response["status"].([]interface{})[0] != "OK" { + return fmt.Errorf("%v order cancellation failed for currency %v and orderID %v, message %v", + c.Name, + cancellation.Currency, + cancellation.OrderID, + response["status"]) + } + return nil } -func (c *COINUT) wsCancelOrders(cancellations []WsCancelOrderParameters) error { +func (c *COINUT) wsCancelOrders(cancellations []WsCancelOrderParameters) (*WsCancelOrdersResponse, []error) { + var errors []error if !c.Websocket.CanUseAuthenticatedEndpoints() { - return fmt.Errorf("%v not authorised to cancel orders", c.Name) + return nil, errors } cancelOrderRequest := WsCancelOrdersRequest{} for i := range cancellations { @@ -585,8 +652,29 @@ func (c *COINUT) wsCancelOrders(cancellations []WsCancelOrderParameters) error { } cancelOrderRequest.Request = "cancel_orders" - cancelOrderRequest.Nonce = c.GetNonce() - return c.wsSend(cancelOrderRequest) + cancelOrderRequest.Nonce = c.WebsocketConn.GenerateMessageID(false) + resp, err := c.WebsocketConn.SendMessageReturnResponse(cancelOrderRequest.Nonce, cancelOrderRequest) + if err != nil { + return nil, []error{err} + } + var response WsCancelOrdersResponse + err = common.JSONDecode(resp, &response) + if err != nil { + return nil, []error{err} + } + if response.Status[0] != "OK" { + return &response, []error{err} + } + for i := range response.Results { + if response.Results[i].Status != "OK" { + errors = append(errors, fmt.Errorf("%v order cancellation failed for currency %v and orderID %v, message %v", + c.Name, + instrumentListByCode[response.Results[i].InstID], + response.Results[i].OrderID, + response.Results[i].Status)) + } + } + return &response, errors } func (c *COINUT) wsGetTradeHistory(p currency.Pair, start, limit int64) error { @@ -597,9 +685,23 @@ func (c *COINUT) wsGetTradeHistory(p currency.Pair, start, limit int64) error { var request WsTradeHistoryRequest request.Request = "trade_history" request.InstID = instrumentListByString[currency] - request.Nonce = c.GetNonce() + request.Nonce = c.WebsocketConn.GenerateMessageID(false) request.Start = start request.Limit = limit - return c.wsSend(request) + resp, err := c.WebsocketConn.SendMessageReturnResponse(request.Nonce, request) + if err != nil { + return err + } + var response map[string]interface{} + err = common.JSONDecode(resp, &response) + if err != nil { + return err + } + if response["status"].([]interface{})[0] != "OK" { + return fmt.Errorf("%v get trade history failed for %v", + c.Name, + request) + } + return nil } diff --git a/exchanges/coinut/coinut_wrapper.go b/exchanges/coinut/coinut_wrapper.go index 25c4b0f0..efcd1cc8 100644 --- a/exchanges/coinut/coinut_wrapper.go +++ b/exchanges/coinut/coinut_wrapper.go @@ -13,6 +13,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" ) @@ -377,7 +378,7 @@ func (c *COINUT) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange. } // GetWebsocket returns a pointer to the exchange websocket -func (c *COINUT) GetWebsocket() (*exchange.Websocket, error) { +func (c *COINUT) GetWebsocket() (*wshandler.Websocket, error) { return c.Websocket, nil } @@ -506,20 +507,20 @@ func (c *COINUT) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([ // SubscribeToWebsocketChannels appends to ChannelsToSubscribe // which lets websocket.manageSubscriptions handle subscribing -func (c *COINUT) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (c *COINUT) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { c.Websocket.SubscribeToChannels(channels) return nil } // UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe // which lets websocket.manageSubscriptions handle unsubscribing -func (c *COINUT) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { - c.Websocket.UnsubscribeToChannels(channels) +func (c *COINUT) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { + c.Websocket.RemoveSubscribedChannels(channels) return nil } // GetSubscriptions returns a copied list of subscriptions -func (c *COINUT) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) { +func (c *COINUT) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { return c.Websocket.GetSubscriptions(), nil } diff --git a/exchanges/exchange.go b/exchanges/exchange.go index 39e98306..6ec60e0d 100644 --- a/exchanges/exchange.go +++ b/exchanges/exchange.go @@ -16,6 +16,7 @@ import ( "github.com/thrasher-/gocryptotrader/exchanges/orderbook" "github.com/thrasher-/gocryptotrader/exchanges/request" "github.com/thrasher-/gocryptotrader/exchanges/ticker" + "github.com/thrasher-/gocryptotrader/exchanges/wshandler" log "github.com/thrasher-/gocryptotrader/logger" ) @@ -27,6 +28,10 @@ const ( ErrExchangeNotFound = "exchange not found in dataset" // DefaultHTTPTimeout is the default HTTP/HTTPS Timeout for exchange requests DefaultHTTPTimeout = time.Second * 15 + // DefaultWebsocketResponseCheckTimeout is the default delay in checking for an expected websocket response + DefaultWebsocketResponseCheckTimeout = time.Millisecond * 30 + // DefaultWebsocketResponseMaxLimit is the default max wait for an expected websocket response before a timeout + DefaultWebsocketResponseMaxLimit = time.Second * 7 ) // FeeType custom type for calculating fees based on method @@ -260,6 +265,8 @@ type Base struct { Enabled bool Verbose bool RESTPollingDelay time.Duration + WebsocketResponseCheckTimeout time.Duration + WebsocketResponseMaxLimit time.Duration AuthenticatedAPISupport bool AuthenticatedWebsocketAPISupport bool APIWithdrawPermissions uint32 @@ -283,7 +290,7 @@ type Base struct { APIUrlSecondaryDefault string RequestCurrencyPairFormat config.CurrencyPairFormatConfig ConfigCurrencyPairFormat config.CurrencyPairFormatConfig - Websocket *Websocket + Websocket *wshandler.Websocket *request.Requester } @@ -326,11 +333,11 @@ type IBotExchange interface { WithdrawCryptocurrencyFunds(withdrawRequest *WithdrawRequest) (string, error) WithdrawFiatFunds(withdrawRequest *WithdrawRequest) (string, error) WithdrawFiatFundsToInternationalBank(withdrawRequest *WithdrawRequest) (string, error) - GetWebsocket() (*Websocket, error) - SubscribeToWebsocketChannels(channels []WebsocketChannelSubscription) error - UnsubscribeToWebsocketChannels(channels []WebsocketChannelSubscription) error + GetWebsocket() (*wshandler.Websocket, error) + SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error + UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error AuthenticateWebsocket() error - GetSubscriptions() ([]WebsocketChannelSubscription, error) + GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) } // SupportsRESTTickerBatchUpdates returns whether or not the diff --git a/exchanges/exchange_test.go b/exchanges/exchange_test.go index 340e256a..c1615a00 100644 --- a/exchanges/exchange_test.go +++ b/exchanges/exchange_test.go @@ -11,6 +11,7 @@ import ( "github.com/thrasher-/gocryptotrader/currency" "github.com/thrasher-/gocryptotrader/exchanges/request" "github.com/thrasher-/gocryptotrader/exchanges/ticker" + "github.com/thrasher-/gocryptotrader/exchanges/wshandler" ) const ( @@ -79,7 +80,7 @@ func TestSetClientProxyAddress(t *testing.T) { newBase := Base{Name: "Testicles", Requester: requester} - newBase.WebsocketInit() + newBase.Websocket = wshandler.New() err := newBase.SetClientProxyAddress(":invalid") if err == nil { diff --git a/exchanges/exmo/exmo.go b/exchanges/exmo/exmo.go index 06b6db70..835e4a59 100644 --- a/exchanges/exmo/exmo.go +++ b/exchanges/exmo/exmo.go @@ -15,6 +15,7 @@ import ( 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" ) @@ -73,7 +74,7 @@ func (e *EXMO) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) e.APIUrlDefault = exmoAPIURL e.APIUrl = e.APIUrlDefault - e.WebsocketInit() + e.Websocket = wshandler.New() } // Setup takes in the supplied exchange configuration details and sets params diff --git a/exchanges/exmo/exmo_wrapper.go b/exchanges/exmo/exmo_wrapper.go index e75bb830..aa4c56ba 100644 --- a/exchanges/exmo/exmo_wrapper.go +++ b/exchanges/exmo/exmo_wrapper.go @@ -13,6 +13,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" ) @@ -313,7 +314,7 @@ func (e *EXMO) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.Wi } // GetWebsocket returns a pointer to the exchange websocket -func (e *EXMO) GetWebsocket() (*exchange.Websocket, error) { +func (e *EXMO) GetWebsocket() (*wshandler.Websocket, error) { return nil, common.ErrFunctionNotSupported } @@ -399,18 +400,18 @@ func (e *EXMO) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]e // SubscribeToWebsocketChannels appends to ChannelsToSubscribe // which lets websocket.manageSubscriptions handle subscribing -func (e *EXMO) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (e *EXMO) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { return common.ErrFunctionNotSupported } // UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe // which lets websocket.manageSubscriptions handle unsubscribing -func (e *EXMO) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (e *EXMO) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { return common.ErrFunctionNotSupported } // GetSubscriptions returns a copied list of subscriptions -func (e *EXMO) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) { +func (e *EXMO) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/gateio/gateio.go b/exchanges/gateio/gateio.go index 541b16d9..3f0f6b84 100644 --- a/exchanges/gateio/gateio.go +++ b/exchanges/gateio/gateio.go @@ -7,16 +7,15 @@ import ( "net/http" "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" ) @@ -48,9 +47,8 @@ const ( // Gateio is the overarching type across this package type Gateio struct { - WebsocketConn *websocket.Conn + WebsocketConn *wshandler.WebsocketConnection exchange.Base - wsRequestMtx sync.Mutex } // SetDefaults sets default values for the exchange @@ -76,14 +74,17 @@ func (g *Gateio) SetDefaults() { g.APIUrl = g.APIUrlDefault g.APIUrlSecondaryDefault = gateioMarketURL g.APIUrlSecondary = g.APIUrlSecondaryDefault - g.WebsocketInit() - g.Websocket.Functionality = exchange.WebsocketTickerSupported | - exchange.WebsocketTradeDataSupported | - exchange.WebsocketOrderbookSupported | - exchange.WebsocketKlineSupported | - exchange.WebsocketSubscribeSupported | - exchange.WebsocketUnsubscribeSupported | - exchange.WebsocketAuthenticatedEndpointsSupported + g.Websocket = wshandler.New() + g.Websocket.Functionality = wshandler.WebsocketTickerSupported | + wshandler.WebsocketTradeDataSupported | + wshandler.WebsocketOrderbookSupported | + wshandler.WebsocketKlineSupported | + wshandler.WebsocketSubscribeSupported | + wshandler.WebsocketUnsubscribeSupported | + wshandler.WebsocketAuthenticatedEndpointsSupported | + wshandler.WebsocketMessageCorrelationSupported + g.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + g.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout } // Setup sets user configuration @@ -125,17 +126,27 @@ func (g *Gateio) Setup(exch *config.ExchangeConfig) { if err != nil { log.Fatal(err) } - err = g.WebsocketSetup(g.WsConnect, + err = g.Websocket.Setup(g.WsConnect, g.Subscribe, g.Unsubscribe, exch.Name, exch.Websocket, exch.Verbose, gateioWebsocketEndpoint, - exch.WebsocketURL) + exch.WebsocketURL, + exch.AuthenticatedWebsocketAPISupport) if err != nil { log.Fatal(err) } + g.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: g.Name, + URL: g.Websocket.GetWebsocketURL(), + ProxyURL: g.Websocket.GetProxyAddress(), + Verbose: g.Verbose, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + RateLimit: gateioWebsocketRateLimit, + } } } diff --git a/exchanges/gateio/gateio_test.go b/exchanges/gateio/gateio_test.go index c65be299..89c19ef4 100644 --- a/exchanges/gateio/gateio_test.go +++ b/exchanges/gateio/gateio_test.go @@ -3,7 +3,6 @@ package gateio import ( "net/http" "testing" - "time" "github.com/gorilla/websocket" "github.com/thrasher-/gocryptotrader/common" @@ -11,6 +10,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 your own APIKEYS here for due diligence testing @@ -22,6 +22,7 @@ const ( ) var g Gateio +var wsSetupRan bool func TestSetDefaults(t *testing.T) { g.SetDefaults() @@ -496,47 +497,126 @@ func TestGetOrderInfo(t *testing.T) { } } -// TestWsAuth dials websocket, sends login request. -func TestWsAuth(t *testing.T) { +// TestWsGetBalance dials websocket, sends balance request. +func TestWsGetBalance(t *testing.T) { g.SetDefaults() TestSetup(t) if !g.Websocket.IsEnabled() && !g.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() { - t.Skip(exchange.WebsocketNotEnabled) + t.Skip(wshandler.WebsocketNotEnabled) + } + g.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: g.Name, + URL: gateioWebsocketEndpoint, + Verbose: g.Verbose, + RateLimit: gateioWebsocketRateLimit, + ResponseMaxLimit: exchange.DefaultWebsocketResponseMaxLimit, + ResponseCheckTimeout: exchange.DefaultWebsocketResponseCheckTimeout, } - var err error var dialer websocket.Dialer - g.WebsocketConn, _, err = dialer.Dial(g.Websocket.GetWebsocketURL(), - http.Header{}) + err := g.WebsocketConn.Dial(&dialer, http.Header{}) if err != nil { t.Fatal(err) } + go g.WsHandleData() g.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() g.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() - go g.WsHandleData() - defer g.WebsocketConn.Close() - err = g.wsServerSignIn() + resp, err := g.wsServerSignIn() + if err != nil { + t.Fatal(err) + } + if resp.Result.Status != "success" { + t.Fatal("Unsuccessful login") + } + _, err = g.wsGetBalance([]string{"EOS", "BTC"}) + if err != nil { + t.Error(err) + } +} + +// TestWsGetOrderInfo dials websocket, sends order info request. +func TestWsGetOrderInfo(t *testing.T) { + g.SetDefaults() + TestSetup(t) + if !g.Websocket.IsEnabled() && !g.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() { + t.Skip(wshandler.WebsocketNotEnabled) + } + g.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: g.Name, + URL: gateioWebsocketEndpoint, + Verbose: g.Verbose, + RateLimit: gateioWebsocketRateLimit, + ResponseMaxLimit: exchange.DefaultWebsocketResponseMaxLimit, + ResponseCheckTimeout: exchange.DefaultWebsocketResponseCheckTimeout, + } + var dialer websocket.Dialer + err := g.WebsocketConn.Dial(&dialer, http.Header{}) + if err != nil { + t.Fatal(err) + } + go g.WsHandleData() + g.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() + g.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() + resp, err := g.wsServerSignIn() + if err != nil { + t.Fatal(err) + } + if resp.Result.Status != "success" { + t.Fatal("Unsuccessful login") + } + _, err = g.wsGetOrderInfo("EOS_USDT", 0, 10) + if err != nil { + t.Error(err) + } +} + +func setupWSTestAuth(t *testing.T) { + if wsSetupRan { + return + } + g.SetDefaults() + TestSetup(t) + if !g.Websocket.IsEnabled() && !g.AuthenticatedWebsocketAPISupport { + t.Skip(wshandler.WebsocketNotEnabled) + } + g.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: g.Name, + URL: gateioWebsocketEndpoint, + Verbose: g.Verbose, + RateLimit: gateioWebsocketRateLimit, + ResponseMaxLimit: exchange.DefaultWebsocketResponseMaxLimit, + ResponseCheckTimeout: exchange.DefaultWebsocketResponseCheckTimeout, + } + var dialer websocket.Dialer + err := g.WebsocketConn.Dial(&dialer, http.Header{}) + if err != nil { + t.Fatal(err) + } + go g.WsHandleData() + g.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() + g.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() + wsSetupRan = true +} + +// TestWsSubscribe dials websocket, sends a subscribe request. +func TestWsSubscribe(t *testing.T) { + setupWSTestAuth(t) + err := g.Subscribe(wshandler.WebsocketChannelSubscription{ + Channel: "ticker.subscribe", + Currency: currency.NewPairWithDelimiter(currency.BTC.String(), currency.USDT.String(), "_"), + }) + if err != nil { + t.Error(err) + } +} + +// TestWsUnsubscribe dials websocket, sends an unsubscribe request. +func TestWsUnsubscribe(t *testing.T) { + setupWSTestAuth(t) + err := g.Unsubscribe(wshandler.WebsocketChannelSubscription{ + Channel: "ticker.subscribe", + Currency: currency.NewPairWithDelimiter(currency.BTC.String(), currency.USDT.String(), "_"), + }) if err != nil { t.Error(err) } - timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout) - select { - case resultString := <-g.Websocket.DataHandler: - if !common.StringContains(resultString.(string), "success") { - t.Error("Authentication failed") - } - case <-timer.C: - t.Error("Expected response") - } - timer.Stop() - err = g.wsGetBalance() - if err != nil { - t.Error(err) - } - timer = time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout) - select { - case <-g.Websocket.DataHandler: - case <-timer.C: - t.Error("Expected response") - } - timer.Stop() } diff --git a/exchanges/gateio/gateio_types.go b/exchanges/gateio/gateio_types.go index b55dbadf..1c80c29a 100644 --- a/exchanges/gateio/gateio_types.go +++ b/exchanges/gateio/gateio_types.go @@ -35,14 +35,6 @@ var ( TimeIntervalDay = TimeInterval(60 * 60 * 24) ) -// IDs for requests -const ( - IDGeneric = 0000 - IDSignIn = 1010 - IDBalance = 2000 - IDOrderQuery = 3001 -) - // MarketInfoResponse holds the market info data type MarketInfoResponse struct { Result string `json:"result"` @@ -478,3 +470,32 @@ type WebSocketOrderQueryRecords struct { FilledAmount string `json:"filledAmount"` FilledTotal string `json:"filledTotal"` } + +// WebsocketAuthenticationResponse contains the result of a login request +type WebsocketAuthenticationResponse struct { + Error string `json:"error"` + Result struct { + Status string `json:"status"` + } `json:"result"` + ID int64 `json:"id"` +} + +// wsGetBalanceRequest +type wsGetBalanceRequest struct { + ID int64 `json:"id"` + Method string `json:"method"` + Params []string `json:"params,omitempty"` +} + +// WsGetBalanceResponse stores WS GetBalance response +type WsGetBalanceResponse struct { + Error interface{} `json:"error"` + Result map[currency.Code]WsGetBalanceResponseData `json:"result,omitempty"` + ID int64 `json:"id"` +} + +// WsGetBalanceResponseData contains currency data +type WsGetBalanceResponseData struct { + Available float64 `json:"available,string"` + Freeze float64 `json:"freeze,string"` +} diff --git a/exchanges/gateio/gateio_websocket.go b/exchanges/gateio/gateio_websocket.go index 91ea4fc0..522c8127 100644 --- a/exchanges/gateio/gateio_websocket.go +++ b/exchanges/gateio/gateio_websocket.go @@ -1,11 +1,9 @@ package gateio import ( - "encoding/json" "errors" "fmt" "net/http" - "net/url" "strconv" "strings" "time" @@ -15,40 +13,28 @@ 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" ) const ( gateioWebsocketEndpoint = "wss://ws.gate.io/v3/" gatioWsMethodPing = "ping" - gateioWebsocketRateLimit = 120 * time.Millisecond + gateioWebsocketRateLimit = 120 ) // WsConnect initiates a websocket connection func (g *Gateio) WsConnect() error { if !g.Websocket.IsEnabled() || !g.IsEnabled() { - return errors.New(exchange.WebsocketNotEnabled) + return errors.New(wshandler.WebsocketNotEnabled) } - var dialer websocket.Dialer - if g.Websocket.GetProxyAddress() != "" { - proxy, err := url.Parse(g.Websocket.GetProxyAddress()) - if err != nil { - return err - } - - dialer.Proxy = http.ProxyURL(proxy) - } - - var err error - g.WebsocketConn, _, err = dialer.Dial(g.Websocket.GetWebsocketURL(), - http.Header{}) + err := g.WebsocketConn.Dial(&dialer, http.Header{}) if err != nil { return err } go g.WsHandleData() - - err = g.wsServerSignIn() + _, err = g.wsServerSignIn() if err != nil { log.Errorf("%v - authentication failed: %v", g.Name, err) } @@ -57,37 +43,33 @@ func (g *Gateio) WsConnect() error { return nil } -func (g *Gateio) wsServerSignIn() error { +func (g *Gateio) wsServerSignIn() (*WebsocketAuthenticationResponse, error) { if !g.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { - return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", g.Name) + return nil, fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", g.Name) } nonce := int(time.Now().Unix() * 1000) sigTemp := g.GenerateSignature(strconv.Itoa(nonce)) signature := common.Base64Encode(sigTemp) signinWsRequest := WebsocketRequest{ - ID: IDSignIn, + ID: g.WebsocketConn.GenerateMessageID(true), Method: "server.sign", Params: []interface{}{g.APIKey, signature, nonce}, } - err := g.wsSend(signinWsRequest) + resp, err := g.WebsocketConn.SendMessageReturnResponse(signinWsRequest.ID, signinWsRequest) if err != nil { g.Websocket.SetCanUseAuthenticatedEndpoints(false) - return err + return nil, err } - time.Sleep(time.Second * 2) // sleep to allow server to complete sign-on if further authenticated requests are sent prior to this they will fail - return nil -} - -// WsReadData reads from the websocket connection and returns the websocket -// response -func (g *Gateio) WsReadData() (exchange.WebsocketResponse, error) { - _, resp, err := g.WebsocketConn.ReadMessage() + var response WebsocketAuthenticationResponse + err = common.JSONDecode(resp, &response) if err != nil { - return exchange.WebsocketResponse{}, err + g.Websocket.SetCanUseAuthenticatedEndpoints(false) + return nil, err } - - g.Websocket.TrafficAlert <- struct{}{} - return exchange.WebsocketResponse{Raw: resp}, nil + if response.Result.Status == "success" { + g.Websocket.SetCanUseAuthenticatedEndpoints(true) + } + return &response, nil } // WsHandleData handles all the websocket data coming from the websocket @@ -105,25 +87,28 @@ func (g *Gateio) WsHandleData() { return default: - resp, err := g.WsReadData() + resp, err := g.WebsocketConn.ReadMessage() if err != nil { g.Websocket.DataHandler <- err - // Read data error messages can overwhelm and panic the application - time.Sleep(time.Second) - continue + return } - + g.Websocket.TrafficAlert <- struct{}{} var result WebsocketResponse err = common.JSONDecode(resp.Raw, &result) if err != nil { g.Websocket.DataHandler <- err continue } + + if result.ID > 0 { + g.WebsocketConn.AddResponseWithID(result.ID, resp.Raw) + continue + } + if result.Error.Code != 0 { if common.StringContains(result.Error.Message, "authentication") { g.Websocket.DataHandler <- fmt.Errorf("%v - authentication failed: %v", g.Name, err) g.Websocket.SetCanUseAuthenticatedEndpoints(false) - continue } g.Websocket.DataHandler <- fmt.Errorf("%v error %s", @@ -131,49 +116,6 @@ func (g *Gateio) WsHandleData() { continue } - switch result.ID { - case IDSignIn: - g.Websocket.SetCanUseAuthenticatedEndpoints(true) - g.Websocket.DataHandler <- string(result.Result) - case IDBalance: - var balance WebsocketBalance - var balanceInterface interface{} - err = json.Unmarshal(result.Result, &balanceInterface) - if err != nil { - g.Websocket.DataHandler <- err - } - var p WebsocketBalanceCurrency - switch x := balanceInterface.(type) { - case map[string]interface{}: - for xx := range x { - switch kk := x[xx].(type) { - case map[string]interface{}: - p = WebsocketBalanceCurrency{ - Currency: xx, - Available: kk["available"].(string), - Locked: kk["freeze"].(string), - } - balance.Currency = append(balance.Currency, p) - default: - break - } - } - default: - break - } - g.Websocket.DataHandler <- balance - case IDOrderQuery: - var orderQuery WebSocketOrderQueryResult - err = common.JSONDecode(result.Result, &orderQuery) - if err != nil { - g.Websocket.DataHandler <- err - continue - } - g.Websocket.DataHandler <- orderQuery - default: - break - } - switch { case common.StringContains(result.Method, "ticker"): var ticker WebsocketTicker @@ -190,7 +132,7 @@ func (g *Gateio) WsHandleData() { continue } - g.Websocket.DataHandler <- exchange.TickerData{ + g.Websocket.DataHandler <- wshandler.TickerData{ Timestamp: time.Now(), Pair: currency.NewPairFromString(c), AssetType: "SPOT", @@ -218,7 +160,7 @@ func (g *Gateio) WsHandleData() { } for _, trade := range trades { - g.Websocket.DataHandler <- exchange.TradeData{ + g.Websocket.DataHandler <- wshandler.TradeData{ Timestamp: time.Now(), CurrencyPair: currency.NewPairFromString(c), AssetType: "SPOT", @@ -310,7 +252,7 @@ func (g *Gateio) WsHandleData() { } } - g.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + g.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Pair: currency.NewPairFromString(c), Asset: "SPOT", Exchange: g.GetName(), @@ -330,7 +272,7 @@ func (g *Gateio) WsHandleData() { low, _ := strconv.ParseFloat(data[4].(string), 64) volume, _ := strconv.ParseFloat(data[5].(string), 64) - g.Websocket.DataHandler <- exchange.KlineData{ + g.Websocket.DataHandler <- wshandler.KlineData{ Timestamp: time.Now(), Pair: currency.NewPairFromString(data[7].(string)), AssetType: "SPOT", @@ -352,11 +294,11 @@ func (g *Gateio) GenerateAuthenticatedSubscriptions() { return } var channels = []string{"balance.subscribe", "order.subscribe"} - var subscriptions []exchange.WebsocketChannelSubscription + var subscriptions []wshandler.WebsocketChannelSubscription enabledCurrencies := g.GetEnabledCurrencies() for i := range channels { for j := range enabledCurrencies { - subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ Channel: channels[i], Currency: enabledCurrencies[j], }) @@ -368,7 +310,7 @@ func (g *Gateio) GenerateAuthenticatedSubscriptions() { // GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() func (g *Gateio) GenerateDefaultSubscriptions() { var channels = []string{"ticker.subscribe", "trades.subscribe", "depth.subscribe", "kline.subscribe"} - var subscriptions []exchange.WebsocketChannelSubscription + var subscriptions []wshandler.WebsocketChannelSubscription enabledCurrencies := g.GetEnabledCurrencies() for i := range channels { for j := range enabledCurrencies { @@ -379,7 +321,7 @@ func (g *Gateio) GenerateDefaultSubscriptions() { } else if strings.EqualFold(channels[i], "kline.subscribe") { params["interval"] = 1800 } - subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ Channel: channels[i], Currency: enabledCurrencies[j], Params: params, @@ -390,54 +332,84 @@ func (g *Gateio) GenerateDefaultSubscriptions() { } // Subscribe sends a websocket message to receive data from the channel -func (g *Gateio) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { +func (g *Gateio) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { params := []interface{}{channelToSubscribe.Currency.String()} for _, paramValue := range channelToSubscribe.Params { params = append(params, paramValue) } subscribe := WebsocketRequest{ - ID: IDGeneric, + ID: g.WebsocketConn.GenerateMessageID(true), Method: channelToSubscribe.Channel, Params: params, } - if strings.EqualFold(channelToSubscribe.Channel, "balance.subscribe") { - subscribe.ID = IDBalance + resp, err := g.WebsocketConn.SendMessageReturnResponse(subscribe.ID, subscribe) + if err != nil { + return err } - - return g.wsSend(subscribe) + var response WebsocketAuthenticationResponse + err = common.JSONDecode(resp, &response) + if err != nil { + return err + } + if response.Result.Status != "success" { + return fmt.Errorf("%v could not subscribe to %v", g.Name, channelToSubscribe.Channel) + } + return nil } // Unsubscribe sends a websocket message to stop receiving data from the channel -func (g *Gateio) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { +func (g *Gateio) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { unsbuscribeText := strings.Replace(channelToSubscribe.Channel, "subscribe", "unsubscribe", 1) subscribe := WebsocketRequest{ - ID: IDGeneric, + ID: g.WebsocketConn.GenerateMessageID(true), Method: unsbuscribeText, Params: []interface{}{channelToSubscribe.Currency.String(), 1800}, } - return g.wsSend(subscribe) + resp, err := g.WebsocketConn.SendMessageReturnResponse(subscribe.ID, subscribe) + if err != nil { + return err + } + var response WebsocketAuthenticationResponse + err = common.JSONDecode(resp, &response) + if err != nil { + return err + } + if response.Result.Status != "success" { + return fmt.Errorf("%v could not subscribe to %v", g.Name, channelToSubscribe.Channel) + } + return nil } -func (g *Gateio) wsGetBalance() error { +func (g *Gateio) wsGetBalance(currencies []string) (*WsGetBalanceResponse, error) { if !g.Websocket.CanUseAuthenticatedEndpoints() { - return fmt.Errorf("%v not authorised to get balance", g.Name) + return nil, fmt.Errorf("%v not authorised to get balance", g.Name) } - balanceWsRequest := WebsocketRequest{ - ID: IDBalance, + balanceWsRequest := wsGetBalanceRequest{ + ID: g.WebsocketConn.GenerateMessageID(false), Method: "balance.query", - Params: []interface{}{}, + Params: currencies, } - return g.wsSend(balanceWsRequest) + resp, err := g.WebsocketConn.SendMessageReturnResponse(balanceWsRequest.ID, balanceWsRequest) + if err != nil { + return nil, err + } + var balance WsGetBalanceResponse + err = common.JSONDecode(resp, &balance) + if err != nil { + return &balance, err + } + + return &balance, nil } -func (g *Gateio) wsGetOrderInfo(market string, offset, limit int) error { +func (g *Gateio) wsGetOrderInfo(market string, offset, limit int) (*WebSocketOrderQueryResult, error) { if !g.Websocket.CanUseAuthenticatedEndpoints() { - return fmt.Errorf("%v not authorised to get order info", g.Name) + return nil, fmt.Errorf("%v not authorised to get order info", g.Name) } order := WebsocketRequest{ - ID: IDOrderQuery, + ID: g.WebsocketConn.GenerateMessageID(true), Method: "order.query", Params: []interface{}{ market, @@ -445,17 +417,14 @@ func (g *Gateio) wsGetOrderInfo(market string, offset, limit int) error { limit, }, } - return g.wsSend(order) -} - -// WsSend sends data to the websocket server -func (g *Gateio) wsSend(data interface{}) error { - g.wsRequestMtx.Lock() - defer g.wsRequestMtx.Unlock() - if g.Verbose { - log.Debugf("%v sending message to websocket %v", g.Name, data) + resp, err := g.WebsocketConn.SendMessageReturnResponse(order.ID, order) + if err != nil { + return nil, err } - // Basic rate limiter - time.Sleep(gateioWebsocketRateLimit) - return g.WebsocketConn.WriteJSON(data) + var orderQuery WebSocketOrderQueryResult + err = common.JSONDecode(resp, &orderQuery) + if err != nil { + return &orderQuery, err + } + return &orderQuery, nil } diff --git a/exchanges/gateio/gateio_wrapper.go b/exchanges/gateio/gateio_wrapper.go index 266440da..04334750 100644 --- a/exchanges/gateio/gateio_wrapper.go +++ b/exchanges/gateio/gateio_wrapper.go @@ -13,6 +13,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" ) @@ -355,7 +356,7 @@ func (g *Gateio) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange. } // GetWebsocket returns a pointer to the exchange websocket -func (g *Gateio) GetWebsocket() (*exchange.Websocket, error) { +func (g *Gateio) GetWebsocket() (*wshandler.Websocket, error) { return g.Websocket, nil } @@ -448,24 +449,25 @@ func (g *Gateio) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([ // SubscribeToWebsocketChannels appends to ChannelsToSubscribe // which lets websocket.manageSubscriptions handle subscribing -func (g *Gateio) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (g *Gateio) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { g.Websocket.SubscribeToChannels(channels) return nil } // UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe // which lets websocket.manageSubscriptions handle unsubscribing -func (g *Gateio) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { - g.Websocket.UnsubscribeToChannels(channels) +func (g *Gateio) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { + g.Websocket.RemoveSubscribedChannels(channels) return nil } // GetSubscriptions returns a copied list of subscriptions -func (g *Gateio) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) { +func (g *Gateio) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { return g.Websocket.GetSubscriptions(), nil } // AuthenticateWebsocket sends an authentication message to the websocket func (g *Gateio) AuthenticateWebsocket() error { - return g.wsServerSignIn() + _, err := g.wsServerSignIn() + return err } diff --git a/exchanges/gemini/gemini.go b/exchanges/gemini/gemini.go index 22235e93..570add66 100644 --- a/exchanges/gemini/gemini.go +++ b/exchanges/gemini/gemini.go @@ -9,12 +9,12 @@ import ( "strings" "time" - "github.com/gorilla/websocket" "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" 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" ) @@ -67,7 +67,7 @@ var ( // AddSession, if sandbox test is needed append a new session with with the same // API keys and change the IsSandbox variable to true. type Gemini struct { - WebsocketConn *websocket.Conn + AuthenticatedWebsocketConn *wshandler.WebsocketConnection exchange.Base Role string RequiresHeartBeat bool @@ -121,10 +121,13 @@ func (g *Gemini) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) g.APIUrlDefault = geminiAPIURL g.APIUrl = g.APIUrlDefault - g.WebsocketInit() - g.Websocket.Functionality = exchange.WebsocketOrderbookSupported | - exchange.WebsocketTradeDataSupported | - exchange.WebsocketAuthenticatedEndpointsSupported + g.Websocket = wshandler.New() + g.Websocket.Functionality = wshandler.WebsocketOrderbookSupported | + wshandler.WebsocketTradeDataSupported | + wshandler.WebsocketAuthenticatedEndpointsSupported | + wshandler.WebsocketSequenceNumberSupported + g.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + g.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout } // Setup sets exchange configuration parameters @@ -169,17 +172,20 @@ func (g *Gemini) Setup(exch *config.ExchangeConfig) { if err != nil { log.Fatal(err) } - err = g.WebsocketSetup(g.WsConnect, + err = g.Websocket.Setup(g.WsConnect, nil, nil, exch.Name, exch.Websocket, exch.Verbose, g.WebsocketURL, - g.WebsocketURL) + g.WebsocketURL, + exch.AuthenticatedWebsocketAPISupport) if err != nil { log.Fatal(err) } + responseCheckTimeout = exch.WebsocketResponseCheckTimeout + responseMaxLimit = exch.WebsocketResponseMaxLimit } } diff --git a/exchanges/gemini/gemini_test.go b/exchanges/gemini/gemini_test.go index 66859b74..74af9ff8 100644 --- a/exchanges/gemini/gemini_test.go +++ b/exchanges/gemini/gemini_test.go @@ -11,6 +11,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 enter sandbox API keys & assigned roles for better testing procedures @@ -568,7 +569,7 @@ func TestWsAuth(t *testing.T) { g.WebsocketURL = geminiWebsocketSandboxEndpoint if !g.Websocket.IsEnabled() && !g.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() { - t.Skip(exchange.WebsocketNotEnabled) + t.Skip(wshandler.WebsocketNotEnabled) } var dialer websocket.Dialer go g.WsHandleData() diff --git a/exchanges/gemini/gemini_websocket.go b/exchanges/gemini/gemini_websocket.go index 40ad7a5f..dbe0dfdc 100644 --- a/exchanges/gemini/gemini_websocket.go +++ b/exchanges/gemini/gemini_websocket.go @@ -14,6 +14,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" ) @@ -27,11 +28,13 @@ const ( // Instantiates a communications channel between websocket connections var comms = make(chan ReadData, 1) +var responseMaxLimit time.Duration +var responseCheckTimeout time.Duration // WsConnect initiates a websocket connection func (g *Gemini) WsConnect() error { if !g.Websocket.IsEnabled() || !g.IsEnabled() { - return errors.New(exchange.WebsocketNotEnabled) + return errors.New(wshandler.WebsocketNotEnabled) } var dialer websocket.Dialer @@ -62,11 +65,18 @@ func (g *Gemini) WsSubscribe(dialer *websocket.Dialer) error { geminiWsMarketData, c.String(), val.Encode()) - conn, conStatus, err := dialer.Dial(endpoint, http.Header{}) - if err != nil { - return fmt.Errorf("%v %v %v Error: %v", endpoint, conStatus, conStatus.StatusCode, err) + connection := &wshandler.WebsocketConnection{ + ExchangeName: g.Name, + URL: endpoint, + Verbose: g.Verbose, + ResponseCheckTimeout: responseCheckTimeout, + ResponseMaxLimit: responseMaxLimit, } - go g.WsReadData(conn, c) + err := connection.Dial(dialer, http.Header{}) + if err != nil { + return fmt.Errorf("%v Websocket connection %v error. Error %v", g.Name, endpoint, err) + } + go g.WsReadData(connection, c) if len(enabledCurrencies)-1 == i { return nil } @@ -99,17 +109,22 @@ func (g *Gemini) WsSecureSubscribe(dialer *websocket.Dialer, url string) error { headers.Add("X-GEMINI-SIGNATURE", common.HexEncodeToString(hmac)) headers.Add("Cache-Control", "no-cache") - conn, conStatus, err := dialer.Dial(endpoint, headers) - if err != nil { - return fmt.Errorf("%v %v %v Error: %v", endpoint, conStatus, conStatus.StatusCode, err) + g.AuthenticatedWebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: g.Name, + URL: endpoint, + Verbose: g.Verbose, } - go g.WsReadData(conn, currency.Pair{}) + err = g.AuthenticatedWebsocketConn.Dial(dialer, headers) + if err != nil { + return fmt.Errorf("%v Websocket connection %v error. Error %v", g.Name, endpoint, err) + } + go g.WsReadData(g.AuthenticatedWebsocketConn, currency.Pair{}) return nil } // WsReadData reads from the websocket connection and returns the websocket // response -func (g *Gemini) WsReadData(ws *websocket.Conn, c currency.Pair) { +func (g *Gemini) WsReadData(ws *wshandler.WebsocketConnection, c currency.Pair) { g.Websocket.Wg.Add(1) defer g.Websocket.Wg.Done() for { @@ -117,13 +132,13 @@ func (g *Gemini) WsReadData(ws *websocket.Conn, c currency.Pair) { case <-g.Websocket.ShutdownC: return default: - _, resp, err := ws.ReadMessage() + resp, err := ws.ReadMessage() if err != nil { g.Websocket.DataHandler <- err return } g.Websocket.TrafficAlert <- struct{}{} - comms <- ReadData{Raw: resp, Currency: c} + comms <- ReadData{Raw: resp.Raw, Currency: c} } } } @@ -271,13 +286,13 @@ func (g *Gemini) wsProcessUpdate(result WsMarketUpdateResponse, pair currency.Pa return } - g.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{Pair: pair, + g.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: pair, Asset: "SPOT", Exchange: g.GetName()} } else { for _, event := range result.Events { if event.Type == "trade" { - g.Websocket.DataHandler <- exchange.TradeData{ + g.Websocket.DataHandler <- wshandler.TradeData{ Timestamp: time.Now(), CurrencyPair: pair, AssetType: "SPOT", @@ -316,7 +331,7 @@ func (g *Gemini) wsProcessUpdate(result WsMarketUpdateResponse, pair currency.Pa } } - g.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{Pair: pair, + g.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{Pair: pair, Asset: "SPOT", Exchange: g.GetName()} } diff --git a/exchanges/gemini/gemini_wrapper.go b/exchanges/gemini/gemini_wrapper.go index c56f8136..66b67a18 100644 --- a/exchanges/gemini/gemini_wrapper.go +++ b/exchanges/gemini/gemini_wrapper.go @@ -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" ) @@ -253,7 +254,7 @@ func (g *Gemini) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange. } // GetWebsocket returns a pointer to the exchange websocket -func (g *Gemini) GetWebsocket() (*exchange.Websocket, error) { +func (g *Gemini) GetWebsocket() (*wshandler.Websocket, error) { return g.Websocket, nil } @@ -360,18 +361,18 @@ func (g *Gemini) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([ // SubscribeToWebsocketChannels appends to ChannelsToSubscribe // which lets websocket.manageSubscriptions handle subscribing -func (g *Gemini) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (g *Gemini) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { return common.ErrFunctionNotSupported } // UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe // which lets websocket.manageSubscriptions handle unsubscribing -func (g *Gemini) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (g *Gemini) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { return common.ErrFunctionNotSupported } // GetSubscriptions returns a copied list of subscriptions -func (g *Gemini) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) { +func (g *Gemini) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/hitbtc/hitbtc.go b/exchanges/hitbtc/hitbtc.go index 6eaeb53e..3dd85770 100644 --- a/exchanges/hitbtc/hitbtc.go +++ b/exchanges/hitbtc/hitbtc.go @@ -7,16 +7,15 @@ import ( "net/http" "net/url" "strconv" - "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" ) @@ -54,8 +53,7 @@ const ( // HitBTC is the overarching type across the hitbtc package type HitBTC struct { exchange.Base - WebsocketConn *websocket.Conn - wsRequestMtx sync.Mutex + WebsocketConn *wshandler.WebsocketConnection } // SetDefaults sets default settings for hitbtc @@ -80,14 +78,17 @@ func (h *HitBTC) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) h.APIUrlDefault = apiURL h.APIUrl = h.APIUrlDefault - h.WebsocketInit() - h.Websocket.Functionality = exchange.WebsocketTickerSupported | - exchange.WebsocketOrderbookSupported | - exchange.WebsocketSubscribeSupported | - exchange.WebsocketUnsubscribeSupported | - exchange.WebsocketAuthenticatedEndpointsSupported | - exchange.WebsocketSubmitOrderSupported | - exchange.WebsocketCancelOrderSupported + h.Websocket = wshandler.New() + h.Websocket.Functionality = wshandler.WebsocketTickerSupported | + wshandler.WebsocketOrderbookSupported | + wshandler.WebsocketSubscribeSupported | + wshandler.WebsocketUnsubscribeSupported | + wshandler.WebsocketAuthenticatedEndpointsSupported | + wshandler.WebsocketSubmitOrderSupported | + wshandler.WebsocketCancelOrderSupported | + wshandler.WebsocketMessageCorrelationSupported + h.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + h.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout } // Setup sets user exchange configuration settings @@ -128,17 +129,27 @@ func (h *HitBTC) 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, hitbtcWebsocketAddress, - exch.WebsocketURL) + exch.WebsocketURL, + exch.AuthenticatedWebsocketAPISupport) if err != nil { log.Fatal(err) } + h.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: h.Name, + URL: h.Websocket.GetWebsocketURL(), + ProxyURL: h.Websocket.GetProxyAddress(), + Verbose: h.Verbose, + RateLimit: rateLimit, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } } } diff --git a/exchanges/hitbtc/hitbtc_test.go b/exchanges/hitbtc/hitbtc_test.go index 9a6624eb..9dc57797 100644 --- a/exchanges/hitbtc/hitbtc_test.go +++ b/exchanges/hitbtc/hitbtc_test.go @@ -11,9 +11,11 @@ import ( "github.com/thrasher-/gocryptotrader/currency" exchange "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/sharedtestvalues" + "github.com/thrasher-/gocryptotrader/exchanges/wshandler" ) var h HitBTC +var wsSetupRan bool // Please supply your own APIKEYS here for due diligence testing const ( @@ -102,7 +104,7 @@ func TestGetFee(t *testing.T) { var feeBuilder = setFeeBuilder() if areTestAPIKeysSet() { // CryptocurrencyTradeFee Basic - if resp, err := h.GetFee(feeBuilder); resp != float64(0.001) || err != nil { + if resp, err := h.GetFee(feeBuilder); resp != float64(0.002) || err != nil { t.Error(err) t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.002), resp) } @@ -111,7 +113,7 @@ func TestGetFee(t *testing.T) { feeBuilder = setFeeBuilder() feeBuilder.Amount = 1000 feeBuilder.PurchasePrice = 1000 - if resp, err := h.GetFee(feeBuilder); resp != float64(1000) || err != nil { + if resp, err := h.GetFee(feeBuilder); resp != float64(2000) || err != nil { t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(2000), resp) t.Error(err) } @@ -119,7 +121,7 @@ func TestGetFee(t *testing.T) { // CryptocurrencyTradeFee IsMaker feeBuilder = setFeeBuilder() feeBuilder.IsMaker = true - if resp, err := h.GetFee(feeBuilder); resp != float64(-0.0001) || err != nil { + if resp, err := h.GetFee(feeBuilder); resp != float64(0.001) || err != nil { t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.001), resp) t.Error(err) } @@ -127,7 +129,7 @@ func TestGetFee(t *testing.T) { // CryptocurrencyTradeFee Negative purchase price feeBuilder = setFeeBuilder() feeBuilder.PurchasePrice = -1000 - if resp, err := h.GetFee(feeBuilder); resp != float64(-1) || err != nil { + if resp, err := h.GetFee(feeBuilder); resp != float64(0) || err != nil { t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp) t.Error(err) } @@ -135,7 +137,7 @@ func TestGetFee(t *testing.T) { // CryptocurrencyWithdrawalFee Basic feeBuilder = setFeeBuilder() feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee - if resp, err := h.GetFee(feeBuilder); resp != float64(0.009580) || err != nil { + if resp, err := h.GetFee(feeBuilder); resp != float64(0.042800) || err != nil { t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.042800), resp) t.Error(err) } @@ -387,16 +389,25 @@ func TestGetDepositAddress(t *testing.T) { } } func setupWsAuth(t *testing.T) { + if wsSetupRan { + return + } 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 h.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() h.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() - h.WebsocketConn, _, err = dialer.Dial(hitbtcWebsocketAddress, http.Header{}) + h.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: h.Name, + URL: hitbtcWebsocketAddress, + Verbose: h.Verbose, + ResponseMaxLimit: exchange.DefaultWebsocketResponseMaxLimit, + ResponseCheckTimeout: exchange.DefaultWebsocketResponseCheckTimeout, + } + var dialer websocket.Dialer + err := h.WebsocketConn.Dial(&dialer, http.Header{}) if err != nil { t.Fatal(err) } @@ -409,84 +420,86 @@ func setupWsAuth(t *testing.T) { case <-timer.C: } timer.Stop() + wsSetupRan = true } // TestWsCancelOrder dials websocket, sends cancel request. func TestWsCancelOrder(t *testing.T) { setupWsAuth(t) - err := h.wsCancelOrder("ImNotARealOrderID") + if !canManipulateRealOrders { + t.Skip("canManipulateRealOrders false, skipping test") + } + _, err := h.wsCancelOrder("ImNotARealOrderID") if err != nil { t.Fatal(err) } - timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout) - select { - case <-h.Websocket.DataHandler: - case <-timer.C: - t.Error("Expecting response") - } - timer.Stop() } // TestWsPlaceOrder dials websocket, sends order submission. func TestWsPlaceOrder(t *testing.T) { setupWsAuth(t) - err := h.wsPlaceOrder(currency.NewPair(currency.LTC, currency.BTC), exchange.BuyOrderSide.ToString(), 1, 1) + if !canManipulateRealOrders { + t.Skip("canManipulateRealOrders false, skipping test") + } + _, err := h.wsPlaceOrder(currency.NewPair(currency.LTC, currency.BTC), exchange.BuyOrderSide.ToString(), 1, 1) if err != nil { t.Fatal(err) } - timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout) - select { - case <-h.Websocket.DataHandler: - case <-timer.C: - t.Error("Expecting response") - } - timer.Stop() } // TestWsReplaceOrder dials websocket, sends replace order request. func TestWsReplaceOrder(t *testing.T) { setupWsAuth(t) - err := h.wsReplaceOrder("ImNotARealOrderID", 1, 1) + if !canManipulateRealOrders { + t.Skip("canManipulateRealOrders false, skipping test") + } + _, err := h.wsReplaceOrder("ImNotARealOrderID", 1, 1) if err != nil { t.Fatal(err) } - timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout) - select { - case <-h.Websocket.DataHandler: - case <-timer.C: - t.Error("Expecting response") - } - timer.Stop() } // TestWsGetActiveOrders dials websocket, sends get active orders request. func TestWsGetActiveOrders(t *testing.T) { setupWsAuth(t) - err := h.wsGetActiveOrders() + _, err := h.wsGetActiveOrders() if err != nil { t.Fatal(err) } - timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout) - select { - case <-h.Websocket.DataHandler: - case <-timer.C: - t.Error("Expecting response") - } - timer.Stop() } // TestWsGetTradingBalance dials websocket, sends get trading balance request. func TestWsGetTradingBalance(t *testing.T) { setupWsAuth(t) - err := h.wsGetTradingBalance() + _, err := h.wsGetTradingBalance() + if err != nil { + t.Fatal(err) + } +} + +// TestWsGetTradingBalance dials websocket, sends get trading balance request. +func TestWsGetTrades(t *testing.T) { + setupWsAuth(t) + _, err := h.wsGetTrades(currency.NewPair(currency.ETH, currency.BTC), 1000, "ASC", "id") + if err != nil { + t.Fatal(err) + } +} + +// TestWsGetTradingBalance dials websocket, sends get trading balance request. +func TestWsGetSymbols(t *testing.T) { + setupWsAuth(t) + _, err := h.wsGetSymbols(currency.NewPair(currency.ETH, currency.BTC)) + if err != nil { + t.Fatal(err) + } +} + +// TestWsGetTradingBalance dials websocket, sends get trading balance request. +func TestSsGetCurrencies(t *testing.T) { + setupWsAuth(t) + _, err := h.wsGetCurrencies(currency.BTC) if err != nil { t.Fatal(err) } - timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout) - select { - case <-h.Websocket.DataHandler: - case <-timer.C: - t.Error("Expecting response") - } - timer.Stop() } diff --git a/exchanges/hitbtc/hitbtc_types.go b/exchanges/hitbtc/hitbtc_types.go index 61ba6ff5..a94390ec 100644 --- a/exchanges/hitbtc/hitbtc_types.go +++ b/exchanges/hitbtc/hitbtc_types.go @@ -299,13 +299,16 @@ type LendingHistory struct { } type capture struct { - Method string `json:"method,omitempty"` - Result interface{} `json:"result"` - Error struct { - Code int `json:"code"` - Message string `json:"message"` - } `json:"error"` - ID int64 `json:"id,omitempty"` + Method string `json:"method,omitempty"` + Result interface{} `json:"result"` + Error ResponseError `json:"error,omitempty"` + ID int64 `json:"id,omitempty"` +} + +// ResponseError contains error codes from JSON responses +type ResponseError struct { + Code int `json:"code"` + Message string `json:"message"` } // WsRequest defines a request obj for the JSON-RPC and gets a websocket @@ -393,12 +396,13 @@ type WsLoginData struct { // WsActiveOrdersResponse Active order response for auth subscription to reports type WsActiveOrdersResponse struct { Params []WsActiveOrdersResponseData `json:"params"` + Error ResponseError `json:"error,omitempty"` } // WsActiveOrdersResponseData Active order data for WsActiveOrdersResponse type WsActiveOrdersResponseData struct { ID string `json:"id"` - ClientOrderID string `json:"clientOrderId"` + ClientOrderID string `json:"clientOrderId,omitempty"` Symbol currency.Pair `json:"symbol"` Side string `json:"side"` Status string `json:"status"` @@ -416,12 +420,13 @@ type WsActiveOrdersResponseData struct { // WsReportResponse report response for auth subscription to reports type WsReportResponse struct { Params WsReportResponseData `json:"params"` + Error ResponseError `json:"error,omitempty"` } // WsReportResponseData Report data for WsReportResponse type WsReportResponseData struct { ID string `json:"id"` - ClientOrderID string `json:"clientOrderId"` + ClientOrderID string `json:"clientOrderId,omitempty"` Symbol currency.Pair `json:"symbol"` Side string `json:"side"` Status string `json:"status"` @@ -449,7 +454,7 @@ type WsSubmitOrderRequest struct { // WsSubmitOrderRequestData WS request data type WsSubmitOrderRequestData struct { - ClientOrderID string `json:"clientOrderId"` + ClientOrderID int64 `json:"clientOrderId,string,omitempty"` Symbol currency.Pair `json:"symbol"` Side string `json:"side"` Price float64 `json:"price,string"` @@ -460,6 +465,7 @@ type WsSubmitOrderRequestData struct { type WsSubmitOrderSuccessResponse struct { Result WsSubmitOrderSuccessResponseData `json:"result"` ID int64 `json:"id"` + Error ResponseError `json:"error,omitempty"` } // WsSubmitOrderSuccessResponseData WS response data @@ -482,7 +488,7 @@ type WsSubmitOrderSuccessResponseData struct { // WsSubmitOrderErrorResponse WS error response type WsSubmitOrderErrorResponse struct { - Error WsSubmitOrderErrorResponseData `json:"error"` + Error WsSubmitOrderErrorResponseData `json:"error,omitempty"` ID int64 `json:"id"` } @@ -497,12 +503,13 @@ type WsSubmitOrderErrorResponseData struct { type WsCancelOrderResponse struct { Result WsCancelOrderResponseData `json:"result"` ID int64 `json:"id"` + Error ResponseError `json:"error,omitempty"` } // WsCancelOrderResponseData WS response data type WsCancelOrderResponseData struct { ID string `json:"id"` - ClientOrderID string `json:"clientOrderId"` + ClientOrderID string `json:"clientOrderId,omitempty"` Symbol currency.Pair `json:"symbol"` Side string `json:"side"` Status string `json:"status"` @@ -521,12 +528,13 @@ type WsCancelOrderResponseData struct { type WsReplaceOrderResponse struct { Result WsReplaceOrderResponseData `json:"result"` ID int64 `json:"id"` + Error ResponseError `json:"error,omitempty"` } // WsReplaceOrderResponseData WS response data type WsReplaceOrderResponseData struct { ID string `json:"id"` - ClientOrderID string `json:"clientOrderId"` + ClientOrderID string `json:"clientOrderId,omitempty"` Symbol currency.Pair `json:"symbol"` Side string `json:"side"` Status string `json:"status"` @@ -546,12 +554,13 @@ type WsReplaceOrderResponseData struct { type WsGetActiveOrdersResponse struct { Result []WsGetActiveOrdersResponseData `json:"result"` ID int64 `json:"id"` + Error ResponseError `json:"error,omitempty"` } // WsGetActiveOrdersResponseData WS response data type WsGetActiveOrdersResponseData struct { ID string `json:"id"` - ClientOrderID string `json:"clientOrderId"` + ClientOrderID string `json:"clientOrderId,omitempty"` Symbol currency.Pair `json:"symbol"` Side string `json:"side"` Status string `json:"status"` @@ -571,6 +580,7 @@ type WsGetActiveOrdersResponseData struct { type WsGetTradingBalanceResponse struct { Result []WsGetTradingBalanceResponseData `json:"result"` ID int64 `json:"id"` + Error ResponseError `json:"error,omitempty"` } // WsGetTradingBalanceResponseData WS response data @@ -606,3 +616,106 @@ type WsReplaceOrderRequestData struct { Quantity float64 `json:"quantity,string,omitempty"` Price float64 `json:"price,string,omitempty"` } + +// WsGetCurrenciesRequest gets currencies +type WsGetCurrenciesRequest struct { + Method string `json:"method"` + Params WsGetCurrenciesRequestParameters `json:"params"` + ID int64 `json:"id"` +} + +// WsGetCurrenciesRequestParameters parameters +type WsGetCurrenciesRequestParameters struct { + Currency currency.Code `json:"currency"` +} + +// WsGetCurrenciesResponse currency response +type WsGetCurrenciesResponse struct { + Result WsGetCurrenciesResponseData `json:"result"` + ID int64 `json:"id"` + Error ResponseError `json:"error,omitempty"` +} + +// WsGetCurrenciesResponseData currency response data +type WsGetCurrenciesResponseData struct { + ID currency.Code `json:"id"` + FullName string `json:"fullName"` + Crypto bool `json:"crypto"` + PayinEnabled bool `json:"payinEnabled"` + PayinPaymentID bool `json:"payinPaymentId"` + PayinConfirmations int64 `json:"payinConfirmations"` + PayoutEnabled bool `json:"payoutEnabled"` + PayoutIsPaymentID bool `json:"payoutIsPaymentId"` + TransferEnabled bool `json:"transferEnabled"` + Delisted bool `json:"delisted"` + PayoutFee string `json:"payoutFee"` +} + +// WsGetSymbolsRequest request data +type WsGetSymbolsRequest struct { + Method string `json:"method"` + Params WsGetSymbolsRequestParameters `json:"params"` + ID int64 `json:"id"` +} + +// WsGetSymbolsRequestParameters request parameters +type WsGetSymbolsRequestParameters struct { + Symbol currency.Pair `json:"symbol"` +} + +// WsGetSymbolsResponse symbol response +type WsGetSymbolsResponse struct { + Result WsGetSymbolsResponseData `json:"result"` + ID int64 `json:"id"` + Error ResponseError `json:"error,omitempty"` +} + +// WsGetSymbolsResponseData symbol response data +type WsGetSymbolsResponseData struct { + ID currency.Pair `json:"id"` + BaseCurrency currency.Code `json:"baseCurrency"` + QuoteCurrency currency.Code `json:"quoteCurrency"` + QuantityIncrement float64 `json:"quantityIncrement,string"` + TickSize float64 `json:"tickSize,string"` + TakeLiquidityRate float64 `json:"takeLiquidityRate,string"` + ProvideLiquidityRate float64 `json:"provideLiquidityRate,string"` + FeeCurrency currency.Code `json:"feeCurrency"` +} + +// WsGetTradesRequest trade request +type WsGetTradesRequest struct { + Method string `json:"method"` + Params WsGetTradesRequestParameters `json:"params"` + ID int64 `json:"id"` +} + +// WsGetTradesRequestParameters trade request params +type WsGetTradesRequestParameters struct { + Symbol currency.Pair `json:"symbol"` + Limit int64 `json:"limit"` + Sort string `json:"sort"` + By string `json:"by"` +} + +// WsGetTradesResponse response +type WsGetTradesResponse struct { + Jsonrpc string `json:"jsonrpc"` + Result WsGetTradesResponseData `json:"result"` + ID int64 `json:"id"` + Error ResponseError `json:"error,omitempty"` +} + +// WsGetTradesResponseData trade response data +type WsGetTradesResponseData struct { + Data []WsGetTradesResponseTrades `json:"data"` + Symbol string `json:"symbol"` +} + +// WsGetTradesResponseTrades trade response +type WsGetTradesResponseTrades struct { + ID int64 `json:"id"` + Price float64 `json:"price,string"` + Quantity float64 `json:"quantity,string"` + Side string `json:"side"` + Timestamp time.Time `json:"timestamp"` +} diff --git a/exchanges/hitbtc/hitbtc_websocket.go b/exchanges/hitbtc/hitbtc_websocket.go index 511e4bf1..af390310 100644 --- a/exchanges/hitbtc/hitbtc_websocket.go +++ b/exchanges/hitbtc/hitbtc_websocket.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "net/http" - "net/url" "strings" "time" @@ -14,12 +13,14 @@ import ( exchange "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/nonce" "github.com/thrasher-/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-/gocryptotrader/exchanges/wshandler" log "github.com/thrasher-/gocryptotrader/logger" ) const ( hitbtcWebsocketAddress = "wss://api.hitbtc.com/api/2/ws" rpcVersion = "2.0" + rateLimit = 20 ) var requestID nonce.Nonce @@ -27,26 +28,13 @@ var requestID nonce.Nonce // WsConnect starts a new connection with the websocket API func (h *HitBTC) 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) - } - - var err error - h.WebsocketConn, _, err = dialer.Dial(hitbtcWebsocketAddress, http.Header{}) + err := h.WebsocketConn.Dial(&dialer, http.Header{}) if err != nil { return err } - go h.WsHandleData() err = h.wsLogin() if err != nil { @@ -58,17 +46,6 @@ func (h *HitBTC) WsConnect() error { return nil } -// WsReadData reads from the websocket connection -func (h *HitBTC) WsReadData() (exchange.WebsocketResponse, error) { - _, resp, err := h.WebsocketConn.ReadMessage() - if err != nil { - return exchange.WebsocketResponse{}, err - } - - h.Websocket.TrafficAlert <- struct{}{} - return exchange.WebsocketResponse{Raw: resp}, nil -} - // WsHandleData handles websocket data func (h *HitBTC) WsHandleData() { h.Websocket.Wg.Add(1) @@ -83,11 +60,12 @@ func (h *HitBTC) WsHandleData() { return default: - resp, err := h.WsReadData() + resp, err := h.WebsocketConn.ReadMessage() if err != nil { h.Websocket.DataHandler <- err return } + h.Websocket.TrafficAlert <- struct{}{} var init capture err = common.JSONDecode(resp.Raw, &init) @@ -95,11 +73,14 @@ func (h *HitBTC) WsHandleData() { h.Websocket.DataHandler <- err continue } - + if init.Error.Code == 1002 { + h.Websocket.SetCanUseAuthenticatedEndpoints(false) + } + if init.ID > 0 { + h.WebsocketConn.AddResponseWithID(init.ID, resp.Raw) + continue + } if init.Error.Message != "" || init.Error.Code != 0 { - if init.Error.Code == 1002 { - h.Websocket.SetCanUseAuthenticatedEndpoints(false) - } h.Websocket.DataHandler <- fmt.Errorf("hitbtc.go error - Code: %d, Message: %s", init.Error.Code, init.Error.Message) @@ -117,7 +98,7 @@ func (h *HitBTC) WsHandleData() { } } -func (h *HitBTC) handleSubscriptionUpdates(resp exchange.WebsocketResponse, init capture) { +func (h *HitBTC) handleSubscriptionUpdates(resp wshandler.WebsocketResponse, init capture) { switch init.Method { case "ticker": var ticker WsTicker @@ -131,7 +112,7 @@ func (h *HitBTC) handleSubscriptionUpdates(resp exchange.WebsocketResponse, init h.Websocket.DataHandler <- err return } - h.Websocket.DataHandler <- exchange.TickerData{ + h.Websocket.DataHandler <- wshandler.TickerData{ Exchange: h.GetName(), AssetType: "SPOT", Pair: currency.NewPairFromString(ticker.Params.Symbol), @@ -187,7 +168,7 @@ func (h *HitBTC) handleSubscriptionUpdates(resp exchange.WebsocketResponse, init } } -func (h *HitBTC) handleCommandResponses(resp exchange.WebsocketResponse, init capture) { +func (h *HitBTC) handleCommandResponses(resp wshandler.WebsocketResponse, init capture) { switch resultType := init.Result.(type) { case map[string]interface{}: switch resultType["reportType"].(string) { @@ -266,7 +247,7 @@ func (h *HitBTC) WsProcessOrderbookSnapshot(ob WsOrderbook) error { return err } - h.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + h.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Exchange: h.GetName(), Asset: "SPOT", Pair: p, @@ -297,7 +278,7 @@ func (h *HitBTC) WsProcessOrderbookUpdate(ob WsOrderbook) error { return err } - h.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + h.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Exchange: h.GetName(), Asset: "SPOT", Pair: p, @@ -308,9 +289,9 @@ func (h *HitBTC) WsProcessOrderbookUpdate(ob WsOrderbook) error { // GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() func (h *HitBTC) GenerateDefaultSubscriptions() { var channels = []string{"subscribeTicker", "subscribeOrderbook", "subscribeTrades", "subscribeCandles"} - var subscriptions []exchange.WebsocketChannelSubscription + var subscriptions []wshandler.WebsocketChannelSubscription if h.Websocket.CanUseAuthenticatedEndpoints() { - subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ Channel: "subscribeReports", }) } @@ -318,7 +299,7 @@ func (h *HitBTC) GenerateDefaultSubscriptions() { for i := range channels { for j := range enabledCurrencies { enabledCurrencies[j].Delimiter = "" - subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ Channel: channels[i], Currency: enabledCurrencies[j], }) @@ -328,7 +309,7 @@ func (h *HitBTC) GenerateDefaultSubscriptions() { } // Subscribe sends a websocket message to receive data from the channel -func (h *HitBTC) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { +func (h *HitBTC) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { subscribe := WsNotification{ Method: channelToSubscribe.Channel, } @@ -350,11 +331,11 @@ func (h *HitBTC) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscript } } - return h.wsSend(subscribe) + return h.WebsocketConn.SendMessage(subscribe) } // Unsubscribe sends a websocket message to stop receiving data from the channel -func (h *HitBTC) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { +func (h *HitBTC) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { unsubscribeChannel := strings.Replace(channelToSubscribe.Channel, "subscribe", "unsubscribe", 1) subscribe := WsNotification{ JSONRPCVersion: rpcVersion, @@ -376,21 +357,7 @@ func (h *HitBTC) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscri } } - return h.wsSend(subscribe) -} - -// WsSend sends data to the websocket server -func (h *HitBTC) wsSend(data interface{}) error { - h.wsRequestMtx.Lock() - defer h.wsRequestMtx.Unlock() - json, err := common.JSONEncode(data) - if err != nil { - return err - } - if h.Verbose { - log.Debugf("%v sending message to websocket %v", h.Name, string(json)) - } - return h.WebsocketConn.WriteMessage(websocket.TextMessage, json) + return h.WebsocketConn.SendMessage(subscribe) } // Unsubscribe sends a websocket message to stop receiving data from the channel @@ -411,7 +378,7 @@ func (h *HitBTC) wsLogin() error { }, } - err := h.wsSend(request) + err := h.WebsocketConn.SendMessage(request) if err != nil { h.Websocket.SetCanUseAuthenticatedEndpoints(false) return err @@ -420,43 +387,68 @@ func (h *HitBTC) wsLogin() error { } // wsPlaceOrder sends a websocket message to submit an order -func (h *HitBTC) wsPlaceOrder(pair currency.Pair, side string, price, quantity float64) error { +func (h *HitBTC) wsPlaceOrder(pair currency.Pair, side string, price, quantity float64) (*WsSubmitOrderSuccessResponse, error) { if !h.Websocket.CanUseAuthenticatedEndpoints() { - return fmt.Errorf("%v not authenticated, cannot place order", h.Name) + return nil, fmt.Errorf("%v not authenticated, cannot place order", h.Name) } + id := h.WebsocketConn.GenerateMessageID(false) request := WsSubmitOrderRequest{ Method: "newOrder", Params: WsSubmitOrderRequestData{ - ClientOrderID: fmt.Sprintf("%v", time.Now().Unix()), + ClientOrderID: id, Symbol: pair, Side: common.StringToLower(side), Price: price, Quantity: quantity, }, - ID: int64(requestID.GetInc()), + ID: id, } - return h.wsSend(request) + resp, err := h.WebsocketConn.SendMessageReturnResponse(id, request) + if err != nil { + return nil, fmt.Errorf("%v %v", h.Name, err) + } + var response WsSubmitOrderSuccessResponse + err = common.JSONDecode(resp, &response) + if err != nil { + return nil, fmt.Errorf("%v %v", h.Name, err) + } + if response.Error.Code > 0 || response.Error.Message != "" { + return &response, fmt.Errorf("%v Error:%v Message:%v", h.Name, response.Error.Code, response.Error.Message) + } + return &response, nil } // wsCancelOrder sends a websocket message to cancel an order -func (h *HitBTC) wsCancelOrder(clientOrderID string) error { +func (h *HitBTC) wsCancelOrder(clientOrderID string) (*WsCancelOrderResponse, error) { if !h.Websocket.CanUseAuthenticatedEndpoints() { - return fmt.Errorf("%v not authenticated, cannot place order", h.Name) + return nil, fmt.Errorf("%v not authenticated, cannot place order", h.Name) } request := WsCancelOrderRequest{ Method: "cancelOrder", Params: WsCancelOrderRequestData{ ClientOrderID: clientOrderID, }, - ID: int64(requestID.GetInc()), + ID: h.WebsocketConn.GenerateMessageID(false), } - return h.wsSend(request) + resp, err := h.WebsocketConn.SendMessageReturnResponse(request.ID, request) + if err != nil { + return nil, fmt.Errorf("%v %v", h.Name, err) + } + var response WsCancelOrderResponse + err = common.JSONDecode(resp, &response) + if err != nil { + return nil, fmt.Errorf("%v %v", h.Name, err) + } + if response.Error.Code > 0 || response.Error.Message != "" { + return &response, fmt.Errorf("%v Error:%v Message:%v", h.Name, response.Error.Code, response.Error.Message) + } + return &response, nil } // wsReplaceOrder sends a websocket message to replace an order -func (h *HitBTC) wsReplaceOrder(clientOrderID string, quantity, price float64) error { +func (h *HitBTC) wsReplaceOrder(clientOrderID string, quantity, price float64) (*WsReplaceOrderResponse, error) { if !h.Websocket.CanUseAuthenticatedEndpoints() { - return fmt.Errorf("%v not authenticated, cannot place order", h.Name) + return nil, fmt.Errorf("%v not authenticated, cannot place order", h.Name) } request := WsReplaceOrderRequest{ Method: "cancelReplaceOrder", @@ -466,33 +458,144 @@ func (h *HitBTC) wsReplaceOrder(clientOrderID string, quantity, price float64) e Quantity: quantity, Price: price, }, - ID: int64(requestID.GetInc()), + ID: h.WebsocketConn.GenerateMessageID(false), } - return h.wsSend(request) + resp, err := h.WebsocketConn.SendMessageReturnResponse(request.ID, request) + if err != nil { + return nil, fmt.Errorf("%v %v", h.Name, err) + } + var response WsReplaceOrderResponse + err = common.JSONDecode(resp, &response) + if err != nil { + return nil, fmt.Errorf("%v %v", h.Name, err) + } + if response.Error.Code > 0 || response.Error.Message != "" { + return &response, fmt.Errorf("%v Error:%v Message:%v", h.Name, response.Error.Code, response.Error.Message) + } + return &response, nil } // wsGetActiveOrders sends a websocket message to get all active orders -func (h *HitBTC) wsGetActiveOrders() error { +func (h *HitBTC) wsGetActiveOrders() (*WsActiveOrdersResponse, error) { if !h.Websocket.CanUseAuthenticatedEndpoints() { - return fmt.Errorf("%v not authenticated, cannot place order", h.Name) + return nil, fmt.Errorf("%v not authenticated, cannot place order", h.Name) } request := WsReplaceOrderRequest{ Method: "getOrders", Params: WsReplaceOrderRequestData{}, - ID: int64(requestID.GetInc()), + ID: h.WebsocketConn.GenerateMessageID(false), } - return h.wsSend(request) + resp, err := h.WebsocketConn.SendMessageReturnResponse(request.ID, request) + if err != nil { + return nil, fmt.Errorf("%v %v", h.Name, err) + } + var response WsActiveOrdersResponse + err = common.JSONDecode(resp, &response) + if err != nil { + return nil, fmt.Errorf("%v %v", h.Name, err) + } + if response.Error.Code > 0 || response.Error.Message != "" { + return &response, fmt.Errorf("%v Error:%v Message:%v", h.Name, response.Error.Code, response.Error.Message) + } + return &response, nil } // wsGetTradingBalance sends a websocket message to get trading balance -func (h *HitBTC) wsGetTradingBalance() error { +func (h *HitBTC) wsGetTradingBalance() (*WsGetTradingBalanceResponse, error) { if !h.Websocket.CanUseAuthenticatedEndpoints() { - return fmt.Errorf("%v not authenticated, cannot place order", h.Name) + return nil, fmt.Errorf("%v not authenticated, cannot place order", h.Name) } request := WsReplaceOrderRequest{ Method: "getTradingBalance", Params: WsReplaceOrderRequestData{}, - ID: int64(requestID.GetInc()), + ID: h.WebsocketConn.GenerateMessageID(false), } - return h.wsSend(request) + resp, err := h.WebsocketConn.SendMessageReturnResponse(request.ID, request) + if err != nil { + return nil, fmt.Errorf("%v %v", h.Name, err) + } + var response WsGetTradingBalanceResponse + err = common.JSONDecode(resp, &response) + if err != nil { + return nil, fmt.Errorf("%v %v", h.Name, err) + } + if response.Error.Code > 0 || response.Error.Message != "" { + return &response, fmt.Errorf("%v Error:%v Message:%v", h.Name, response.Error.Code, response.Error.Message) + } + return &response, nil +} + +// wsGetCurrencies sends a websocket message to get trading balance +func (h *HitBTC) wsGetCurrencies(currencyItem currency.Code) (*WsGetCurrenciesResponse, error) { + request := WsGetCurrenciesRequest{ + Method: "getCurrency", + Params: WsGetCurrenciesRequestParameters{ + Currency: currencyItem, + }, + ID: h.WebsocketConn.GenerateMessageID(false), + } + resp, err := h.WebsocketConn.SendMessageReturnResponse(request.ID, request) + if err != nil { + return nil, fmt.Errorf("%v %v", h.Name, err) + } + var response WsGetCurrenciesResponse + err = common.JSONDecode(resp, &response) + if err != nil { + return nil, fmt.Errorf("%v %v", h.Name, err) + } + if response.Error.Code > 0 || response.Error.Message != "" { + return &response, fmt.Errorf("%v Error:%v Message:%v", h.Name, response.Error.Code, response.Error.Message) + } + return &response, nil +} + +// wsGetSymbols sends a websocket message to get trading balance +func (h *HitBTC) wsGetSymbols(currencyItem currency.Pair) (*WsGetSymbolsResponse, error) { + request := WsGetSymbolsRequest{ + Method: "getSymbol", + Params: WsGetSymbolsRequestParameters{ + Symbol: currencyItem, + }, + ID: h.WebsocketConn.GenerateMessageID(false), + } + resp, err := h.WebsocketConn.SendMessageReturnResponse(request.ID, request) + if err != nil { + return nil, fmt.Errorf("%v %v", h.Name, err) + } + var response WsGetSymbolsResponse + err = common.JSONDecode(resp, &response) + if err != nil { + return nil, fmt.Errorf("%v %v", h.Name, err) + } + if response.Error.Code > 0 || response.Error.Message != "" { + return &response, fmt.Errorf("%v Error:%v Message:%v", h.Name, response.Error.Code, response.Error.Message) + } + return &response, nil +} + +// wsGetSymbols sends a websocket message to get trading balance +func (h *HitBTC) wsGetTrades(currencyItem currency.Pair, limit int64, sort, by string) (*WsGetTradesResponse, error) { + request := WsGetTradesRequest{ + Method: "getTrades", + Params: WsGetTradesRequestParameters{ + Symbol: currencyItem, + Limit: limit, + Sort: sort, + By: by, + }, + ID: h.WebsocketConn.GenerateMessageID(false), + } + resp, err := h.WebsocketConn.SendMessageReturnResponse(request.ID, request) + if err != nil { + return nil, fmt.Errorf("%v %v", h.Name, err) + } + var response WsGetTradesResponse + err = common.JSONDecode(resp, &response) + if err != nil { + return nil, fmt.Errorf("%v %v", h.Name, err) + } + if response.Error.Code > 0 || response.Error.Message != "" { + return &response, fmt.Errorf("%v Error:%v Message:%v", h.Name, response.Error.Code, response.Error.Message) + } + return &response, nil } diff --git a/exchanges/hitbtc/hitbtc_wrapper.go b/exchanges/hitbtc/hitbtc_wrapper.go index 276084ce..ed009209 100644 --- a/exchanges/hitbtc/hitbtc_wrapper.go +++ b/exchanges/hitbtc/hitbtc_wrapper.go @@ -12,7 +12,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" ) @@ -285,7 +285,7 @@ func (h *HitBTC) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange. } // GetWebsocket returns a pointer to the exchange websocket -func (h *HitBTC) GetWebsocket() (*exchange.Websocket, error) { +func (h *HitBTC) GetWebsocket() (*wshandler.Websocket, error) { return h.Websocket, nil } @@ -376,20 +376,20 @@ func (h *HitBTC) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([ // SubscribeToWebsocketChannels appends to ChannelsToSubscribe // which lets websocket.manageSubscriptions handle subscribing -func (h *HitBTC) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (h *HitBTC) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { h.Websocket.SubscribeToChannels(channels) return nil } // UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe // which lets websocket.manageSubscriptions handle unsubscribing -func (h *HitBTC) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { - h.Websocket.UnsubscribeToChannels(channels) +func (h *HitBTC) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { + h.Websocket.RemoveSubscribedChannels(channels) return nil } // GetSubscriptions returns a copied list of subscriptions -func (h *HitBTC) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) { +func (h *HitBTC) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { return h.Websocket.GetSubscriptions(), nil } diff --git a/exchanges/huobi/huobi.go b/exchanges/huobi/huobi.go index 483f284f..55839162 100644 --- a/exchanges/huobi/huobi.go +++ b/exchanges/huobi/huobi.go @@ -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, + } } } diff --git a/exchanges/huobi/huobi_test.go b/exchanges/huobi/huobi_test.go index fea3413f..645ff0b0 100644 --- a/exchanges/huobi/huobi_test.go +++ b/exchanges/huobi/huobi_test.go @@ -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() } diff --git a/exchanges/huobi/huobi_types.go b/exchanges/huobi/huobi_types.go index feb83195..c9918271 100644 --- a/exchanges/huobi/huobi_types.go +++ b/exchanges/huobi/huobi_types.go @@ -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"` +} diff --git a/exchanges/huobi/huobi_websocket.go b/exchanges/huobi/huobi_websocket.go index 691f45b9..4be1e331 100644 --- a/exchanges/huobi/huobi_websocket.go +++ b/exchanges/huobi/huobi_websocket.go @@ -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 } diff --git a/exchanges/huobi/huobi_wrapper.go b/exchanges/huobi/huobi_wrapper.go index e88e7fb3..90370630 100644 --- a/exchanges/huobi/huobi_wrapper.go +++ b/exchanges/huobi/huobi_wrapper.go @@ -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 } diff --git a/exchanges/huobihadax/huobihadax.go b/exchanges/huobihadax/huobihadax.go index 35d8d0a8..53b9d9bb 100644 --- a/exchanges/huobihadax/huobihadax.go +++ b/exchanges/huobihadax/huobihadax.go @@ -8,16 +8,15 @@ import ( "net/http" "net/url" "strconv" - "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" ) @@ -63,10 +62,9 @@ const ( // HUOBIHADAX is the overarching type across this package type HUOBIHADAX struct { - WebsocketConn *websocket.Conn - AuthenticatedWebsocketConn *websocket.Conn + WebsocketConn *wshandler.WebsocketConnection + AuthenticatedWebsocketConn *wshandler.WebsocketConnection exchange.Base - wsRequestMtx sync.Mutex } // SetDefaults sets default values for the exchange @@ -91,14 +89,18 @@ func (h *HUOBIHADAX) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) h.APIUrlDefault = huobihadaxAPIURL h.APIUrl = h.APIUrlDefault - h.WebsocketInit() - h.Websocket.Functionality = exchange.WebsocketKlineSupported | - exchange.WebsocketTradeDataSupported | - exchange.WebsocketOrderbookSupported | - exchange.WebsocketSubscribeSupported | - exchange.WebsocketUnsubscribeSupported | - exchange.WebsocketAuthenticatedEndpointsSupported | - exchange.WebsocketAccountDataSupported + h.Websocket = wshandler.New() + h.Websocket.Functionality = wshandler.WebsocketKlineSupported | + wshandler.WebsocketTradeDataSupported | + wshandler.WebsocketOrderbookSupported | + wshandler.WebsocketSubscribeSupported | + wshandler.WebsocketUnsubscribeSupported | + wshandler.WebsocketAuthenticatedEndpointsSupported | + wshandler.WebsocketAccountDataSupported | + wshandler.WebsocketMessageCorrelationSupported + h.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + h.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout + } // Setup sets user configuration @@ -140,17 +142,36 @@ func (h *HUOBIHADAX) 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, HuobiHadaxSocketIOAddress, - exch.WebsocketURL) + exch.WebsocketURL, + exch.AuthenticatedWebsocketAPISupport) if err != nil { log.Fatal(err) } + h.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: h.Name, + URL: HuobiHadaxSocketIOAddress, + 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, + } } } diff --git a/exchanges/huobihadax/huobihadax_test.go b/exchanges/huobihadax/huobihadax_test.go index c6d16bb8..634e2e6b 100644 --- a/exchanges/huobihadax/huobihadax_test.go +++ b/exchanges/huobihadax/huobihadax_test.go @@ -4,7 +4,6 @@ import ( "fmt" "strconv" "testing" - "time" "github.com/gorilla/websocket" "github.com/thrasher-/gocryptotrader/common" @@ -12,6 +11,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 your own APIKEYS here for due diligence testing @@ -52,37 +52,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: HuobiHadaxSocketIOAddress, + 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 } @@ -636,46 +635,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() } diff --git a/exchanges/huobihadax/huobihadax_types.go b/exchanges/huobihadax/huobihadax_types.go index 787c9b14..264f3fab 100644 --- a/exchanges/huobihadax/huobihadax_types.go +++ b/exchanges/huobihadax/huobihadax_types.go @@ -340,6 +340,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 @@ -357,6 +358,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 @@ -369,6 +371,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 @@ -381,6 +384,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 @@ -395,6 +399,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 @@ -405,7 +410,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 @@ -523,3 +528,8 @@ type WsAuthenticatedOrderDetailResponse struct { WsAuthenticatedDataResponse Data WsAuthenticatedOrdersListResponseData `json:"data"` } + +// WsPong sent for pong messages +type WsPong struct { + Pong int64 `json:"pong"` +} diff --git a/exchanges/huobihadax/huobihadax_websocket.go b/exchanges/huobihadax/huobihadax_websocket.go index 79e41a9d..37cb43d8 100644 --- a/exchanges/huobihadax/huobihadax_websocket.go +++ b/exchanges/huobihadax/huobihadax_websocket.go @@ -1,11 +1,8 @@ package huobihadax 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 *HUOBIHADAX) 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 @@ -76,7 +66,6 @@ func (h *HUOBIHADAX) WsConnect() error { if err != nil { log.Errorf("%v - authentication failed: %v", h.Name, err) } - go h.WsHandleData() h.GenerateDefaultSubscriptions() @@ -84,11 +73,9 @@ func (h *HUOBIHADAX) WsConnect() error { } func (h *HUOBIHADAX) wsDial(dialer *websocket.Dialer) error { - var err error - var conStatus *http.Response - h.WebsocketConn, conStatus, err = dialer.Dial(HuobiHadaxSocketIOAddress, http.Header{}) + err := h.WebsocketConn.Dial(dialer, http.Header{}) if err != nil { - return fmt.Errorf("%v %v %v Error: %v", HuobiHadaxSocketIOAddress, conStatus, conStatus.StatusCode, err) + return err } go h.wsMultiConnectionFunnel(h.WebsocketConn, HuobiHadaxSocketIOAddress) return nil @@ -98,18 +85,16 @@ func (h *HUOBIHADAX) 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 *HUOBIHADAX) wsMultiConnectionFunnel(ws *websocket.Conn, url string) { +func (h *HUOBIHADAX) wsMultiConnectionFunnel(ws *wshandler.WebsocketConnection, url string) { h.Websocket.Wg.Add(1) defer h.Websocket.Wg.Done() for { @@ -117,29 +102,13 @@ func (h *HUOBIHADAX) 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} } } } @@ -173,31 +142,26 @@ func (h *HUOBIHADAX) 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 +194,6 @@ func (h *HUOBIHADAX) 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 +216,7 @@ func (h *HUOBIHADAX) 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 +241,7 @@ func (h *HUOBIHADAX) 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 +260,7 @@ func (h *HUOBIHADAX) 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 +297,7 @@ func (h *HUOBIHADAX) 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 +309,10 @@ func (h *HUOBIHADAX) WsProcessOrderbook(ob *WsDepth, symbol string) error { // GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() func (h *HUOBIHADAX) 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 +321,7 @@ func (h *HUOBIHADAX) 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 +331,21 @@ func (h *HUOBIHADAX) GenerateDefaultSubscriptions() { } // Subscribe sends a websocket message to receive data from the channel -func (h *HUOBIHADAX) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { +func (h *HUOBIHADAX) 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 *HUOBIHADAX) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { +func (h *HUOBIHADAX) 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) -} - -// WsSend sends data to the websocket server -func (h *HUOBIHADAX) 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) + return h.WebsocketConn.SendMessage(WsRequest{Unsubscribe: channelToSubscribe.Channel}) } func (h *HUOBIHADAX) wsLogin() error { @@ -438,25 +363,14 @@ func (h *HUOBIHADAX) 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 } - return nil -} -func (h *HUOBIHADAX) 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) + time.Sleep(loginDelay) + return nil } func (h *HUOBIHADAX) wsGenerateSignature(timestamp, endpoint string) []byte { @@ -483,12 +397,12 @@ func (h *HUOBIHADAX) wsAuthenticatedSubscribe(operation, endpoint, topic string) } hmac := h.wsGenerateSignature(timestamp, endpoint) request.Signature = common.Base64Encode(hmac) - return h.wsAuthenticatedSend(request) + return h.AuthenticatedWebsocketConn.SendMessage(request) } -func (h *HUOBIHADAX) wsGetAccountsList(pair currency.Pair) error { +func (h *HUOBIHADAX) 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 +416,19 @@ func (h *HUOBIHADAX) 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 *HUOBIHADAX) wsGetOrdersList(accountID int64, pair currency.Pair) error { +func (h *HUOBIHADAX) 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 +444,19 @@ func (h *HUOBIHADAX) 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 *HUOBIHADAX) wsGetOrderDetails(orderID string) error { +func (h *HUOBIHADAX) 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 +470,12 @@ func (h *HUOBIHADAX) 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 } diff --git a/exchanges/huobihadax/huobihadax_wrapper.go b/exchanges/huobihadax/huobihadax_wrapper.go index fd7057cf..f41766c4 100644 --- a/exchanges/huobihadax/huobihadax_wrapper.go +++ b/exchanges/huobihadax/huobihadax_wrapper.go @@ -13,6 +13,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" ) @@ -341,7 +342,7 @@ func (h *HUOBIHADAX) WithdrawFiatFundsToInternationalBank(withdrawRequest *excha } // GetWebsocket returns a pointer to the exchange websocket -func (h *HUOBIHADAX) GetWebsocket() (*exchange.Websocket, error) { +func (h *HUOBIHADAX) GetWebsocket() (*wshandler.Websocket, error) { return h.Websocket, nil } @@ -451,20 +452,20 @@ func (h *HUOBIHADAX) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest // SubscribeToWebsocketChannels appends to ChannelsToSubscribe // which lets websocket.manageSubscriptions handle subscribing -func (h *HUOBIHADAX) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (h *HUOBIHADAX) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { h.Websocket.SubscribeToChannels(channels) return nil } // UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe // which lets websocket.manageSubscriptions handle unsubscribing -func (h *HUOBIHADAX) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { - h.Websocket.UnsubscribeToChannels(channels) +func (h *HUOBIHADAX) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { + h.Websocket.RemoveSubscribedChannels(channels) return nil } // GetSubscriptions returns a copied list of subscriptions -func (h *HUOBIHADAX) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) { +func (h *HUOBIHADAX) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { return h.Websocket.GetSubscriptions(), nil } diff --git a/exchanges/itbit/itbit.go b/exchanges/itbit/itbit.go index d1bbf83f..76b6514b 100644 --- a/exchanges/itbit/itbit.go +++ b/exchanges/itbit/itbit.go @@ -16,6 +16,7 @@ import ( 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" ) @@ -65,7 +66,7 @@ func (i *ItBit) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) i.APIUrlDefault = itbitAPIURL i.APIUrl = i.APIUrlDefault - i.WebsocketInit() + i.Websocket = wshandler.New() } // Setup sets the exchange parameters from exchange config diff --git a/exchanges/itbit/itbit_wrapper.go b/exchanges/itbit/itbit_wrapper.go index be36d7f5..138d31d7 100644 --- a/exchanges/itbit/itbit_wrapper.go +++ b/exchanges/itbit/itbit_wrapper.go @@ -13,6 +13,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" ) @@ -295,7 +296,7 @@ func (i *ItBit) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.W } // GetWebsocket returns a pointer to the exchange websocket -func (i *ItBit) GetWebsocket() (*exchange.Websocket, error) { +func (i *ItBit) GetWebsocket() (*wshandler.Websocket, error) { return nil, common.ErrFunctionNotSupported } @@ -409,18 +410,18 @@ func (i *ItBit) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([] // SubscribeToWebsocketChannels appends to ChannelsToSubscribe // which lets websocket.manageSubscriptions handle subscribing -func (i *ItBit) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (i *ItBit) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { return common.ErrFunctionNotSupported } // UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe // which lets websocket.manageSubscriptions handle unsubscribing -func (i *ItBit) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (i *ItBit) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { return common.ErrFunctionNotSupported } // GetSubscriptions returns a copied list of subscriptions -func (i *ItBit) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) { +func (i *ItBit) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/kraken/kraken.go b/exchanges/kraken/kraken.go index 0350a490..e581d0c7 100644 --- a/exchanges/kraken/kraken.go +++ b/exchanges/kraken/kraken.go @@ -10,13 +10,13 @@ import ( "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" ) @@ -58,7 +58,7 @@ const ( // Kraken is the overarching type across the alphapoint package type Kraken struct { exchange.Base - WebsocketConn *websocket.Conn + WebsocketConn *wshandler.WebsocketConnection CryptoFee, FiatFee float64 wsRequestMtx sync.Mutex } @@ -89,14 +89,17 @@ func (k *Kraken) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) k.APIUrlDefault = krakenAPIURL k.APIUrl = k.APIUrlDefault - k.WebsocketInit() + k.Websocket = wshandler.New() k.WebsocketURL = krakenWSURL - k.Websocket.Functionality = exchange.WebsocketTickerSupported | - exchange.WebsocketTradeDataSupported | - exchange.WebsocketKlineSupported | - exchange.WebsocketOrderbookSupported | - exchange.WebsocketSubscribeSupported | - exchange.WebsocketUnsubscribeSupported + k.Websocket.Functionality = wshandler.WebsocketTickerSupported | + wshandler.WebsocketTradeDataSupported | + wshandler.WebsocketKlineSupported | + wshandler.WebsocketOrderbookSupported | + wshandler.WebsocketSubscribeSupported | + wshandler.WebsocketUnsubscribeSupported | + wshandler.WebsocketMessageCorrelationSupported + k.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + k.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout } @@ -137,17 +140,27 @@ func (k *Kraken) Setup(exch *config.ExchangeConfig) { if err != nil { log.Fatal(err) } - err = k.WebsocketSetup(k.WsConnect, + err = k.Websocket.Setup(k.WsConnect, k.Subscribe, k.Unsubscribe, exch.Name, exch.Websocket, exch.Verbose, krakenWSURL, - exch.WebsocketURL) + exch.WebsocketURL, + exch.AuthenticatedWebsocketAPISupport) if err != nil { log.Fatal(err) } + k.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: k.Name, + URL: k.Websocket.GetWebsocketURL(), + ProxyURL: k.Websocket.GetProxyAddress(), + Verbose: k.Verbose, + RateLimit: krakenWsRateLimit, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } } } diff --git a/exchanges/kraken/kraken_test.go b/exchanges/kraken/kraken_test.go index 06740c05..f580fe85 100644 --- a/exchanges/kraken/kraken_test.go +++ b/exchanges/kraken/kraken_test.go @@ -2,17 +2,21 @@ package kraken import ( "fmt" + "net/http" "strings" "testing" + "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/sharedtestvalues" + "github.com/thrasher-/gocryptotrader/exchanges/wshandler" ) var k Kraken +var wsSetupRan bool // Please add your own APIkeys to do correct due diligence testing. const ( @@ -743,3 +747,42 @@ func TestOrderBookOutOfOrder(t *testing.T) { t.Error("Expected out of order orderbook error") } } + +func setupWsTests(t *testing.T) { + if wsSetupRan { + return + } + TestSetDefaults(t) + TestSetup(t) + if !k.Websocket.IsEnabled() && !k.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() { + t.Skip(wshandler.WebsocketNotEnabled) + } + k.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() + k.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() + k.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: k.Name, + URL: krakenWSURL, + Verbose: k.Verbose, + ResponseMaxLimit: exchange.DefaultWebsocketResponseMaxLimit, + ResponseCheckTimeout: exchange.DefaultWebsocketResponseCheckTimeout, + } + var dialer websocket.Dialer + err := k.WebsocketConn.Dial(&dialer, http.Header{}) + if err != nil { + t.Fatal(err) + } + go k.WsHandleData() + wsSetupRan = true +} + +// TestWebsocketSubscribe tests returning a message with an id +func TestWebsocketSubscribe(t *testing.T) { + setupWsTests(t) + err := k.Subscribe(wshandler.WebsocketChannelSubscription{ + Channel: defaultSubscribedChannels[0], + Currency: currency.NewPairWithDelimiter("XBT", "USD", "/"), + }) + if err != nil { + t.Error(err) + } +} diff --git a/exchanges/kraken/kraken_types.go b/exchanges/kraken/kraken_types.go index f72c4ecf..e05f860a 100644 --- a/exchanges/kraken/kraken_types.go +++ b/exchanges/kraken/kraken_types.go @@ -395,9 +395,14 @@ type WebsocketSubscriptionEventRequest struct { Subscription WebsocketSubscriptionData `json:"subscription,omitempty"` } +// WebsocketBaseEventRequest Just has an "event" property +type WebsocketBaseEventRequest struct { + Event string `json:"event"` // eg "unsubscribe" +} + // WebsocketUnsubscribeByChannelIDEventRequest handles WS unsubscribe events type WebsocketUnsubscribeByChannelIDEventRequest struct { - Event string `json:"event"` // unsubscribe + WebsocketBaseEventRequest RequestID int64 `json:"reqid,omitempty"` // Optional, client originated ID reflected in response message. Pairs []string `json:"pair,omitempty"` // Array of currency pairs (pair1,pair2,pair3). ChannelID int64 `json:"channelID,omitempty"` @@ -412,7 +417,7 @@ type WebsocketSubscriptionData struct { // WebsocketEventResponse holds all data response types type WebsocketEventResponse struct { - Event string `json:"event"` + WebsocketBaseEventRequest Status string `json:"status"` Pair currency.Pair `json:"pair,omitempty"` RequestID int64 `json:"reqid,omitempty"` // Optional, client originated ID reflected in response message. diff --git a/exchanges/kraken/kraken_websocket.go b/exchanges/kraken/kraken_websocket.go index 12f14cd0..836c8ed0 100644 --- a/exchanges/kraken/kraken_websocket.go +++ b/exchanges/kraken/kraken_websocket.go @@ -1,14 +1,10 @@ package kraken import ( - "bytes" - "compress/flate" "errors" "fmt" - "io/ioutil" "math" "net/http" - "net/url" "sort" "strconv" "sync" @@ -17,8 +13,8 @@ import ( "github.com/gorilla/websocket" "github.com/thrasher-/gocryptotrader/common" "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" ) @@ -44,9 +40,8 @@ const ( krakenWsSpread = "spread" krakenWsOrderbook = "book" // Only supported asset type - krakenWsAssetType = "SPOT" orderbookBufferLimit = 3 - krakenWsRateLimit = 50 * time.Millisecond + krakenWsRateLimit = 50 ) // orderbookMutex Ensures if two entries arrive at once, only one can be processed at a time @@ -66,53 +61,15 @@ var subscribeToDefaultChannels = true // Format [[ticker,but-t4u],[orderbook,nce-btt]] var defaultSubscribedChannels = []string{krakenWsTicker, krakenWsTrade, krakenWsOrderbook, krakenWsOHLC, krakenWsSpread} -// writeToWebsocket sends a message to the websocket endpoint -func (k *Kraken) writeToWebsocket(message []byte) error { - k.wsRequestMtx.Lock() - defer k.wsRequestMtx.Unlock() - if k.Verbose { - log.Debugf("%v Sending message to WS: %v", - k.Name, - string(message)) - } - // Really basic WS rate limit - time.Sleep(krakenWsRateLimit) - return k.WebsocketConn.WriteMessage(websocket.TextMessage, message) -} - // WsConnect initiates a websocket connection func (k *Kraken) WsConnect() error { if !k.Websocket.IsEnabled() || !k.IsEnabled() { - return errors.New(exchange.WebsocketNotEnabled) + return errors.New(wshandler.WebsocketNotEnabled) } - var dialer websocket.Dialer - if k.Websocket.GetProxyAddress() != "" { - proxy, err := url.Parse(k.Websocket.GetProxyAddress()) - if err != nil { - return err - } - - dialer.Proxy = http.ProxyURL(proxy) - } - - var err error - if k.Verbose { - log.Debugf("%v Attempting to connect to %v", - k.Name, - k.Websocket.GetWebsocketURL()) - } - k.WebsocketConn, _, err = dialer.Dial(k.Websocket.GetWebsocketURL(), - http.Header{}) + err := k.WebsocketConn.Dial(&dialer, http.Header{}) if err != nil { - return fmt.Errorf("%s Unable to connect to Websocket. Error: %s", - k.Name, - err) - } - if k.Verbose { - log.Debugf("%v Successful connection to %v", - k.Name, - k.Websocket.GetWebsocketURL()) + return err } go k.WsHandleData() go k.wsPingHandler() @@ -123,35 +80,6 @@ func (k *Kraken) WsConnect() error { return nil } -// WsReadData reads data from the websocket connection -func (k *Kraken) WsReadData() (exchange.WebsocketResponse, error) { - mType, resp, err := k.WebsocketConn.ReadMessage() - if err != nil { - return exchange.WebsocketResponse{}, err - } - k.Websocket.TrafficAlert <- struct{}{} - var standardMessage []byte - switch mType { - case websocket.TextMessage: - standardMessage = resp - - case websocket.BinaryMessage: - reader := flate.NewReader(bytes.NewReader(resp)) - standardMessage, err = ioutil.ReadAll(reader) - reader.Close() - if err != nil { - return exchange.WebsocketResponse{}, err - } - } - if k.Verbose { - log.Debugf("%v Websocket message received: %v", - k.Name, - string(standardMessage)) - } - - return exchange.WebsocketResponse{Raw: standardMessage}, nil -} - // wsPingHandler sends a message "ping" every 27 to maintain the connection to the websocket func (k *Kraken) wsPingHandler() { k.Websocket.Wg.Add(1) @@ -163,14 +91,13 @@ func (k *Kraken) wsPingHandler() { select { case <-k.Websocket.ShutdownC: return - case <-ticker.C: - pingEvent := fmt.Sprintf("{\"event\":\"%v\"}", krakenWsPing) + pingEvent := WebsocketBaseEventRequest{Event: krakenWsPing} if k.Verbose { log.Debugf("%v sending ping", k.Name) } - err := k.writeToWebsocket([]byte(pingEvent)) + err := k.WebsocketConn.SendMessage(pingEvent) if err != nil { k.Websocket.DataHandler <- err } @@ -190,18 +117,19 @@ func (k *Kraken) WsHandleData() { case <-k.Websocket.ShutdownC: return default: - resp, err := k.WsReadData() + resp, err := k.WebsocketConn.ReadMessage() if err != nil { k.Websocket.DataHandler <- fmt.Errorf("%v WsHandleData: %v", k.Name, err) return } + k.Websocket.TrafficAlert <- struct{}{} // event response handling var eventResponse WebsocketEventResponse err = common.JSONDecode(resp.Raw, &eventResponse) if err == nil && eventResponse.Event != "" { - k.WsHandleEventResponse(&eventResponse) + k.WsHandleEventResponse(&eventResponse, resp.Raw) continue } // Data response handling @@ -259,7 +187,7 @@ func (k *Kraken) WsHandleDataResponse(response WebsocketDataResponse) { } // WsHandleEventResponse classifies the WS response and sends to appropriate handler -func (k *Kraken) WsHandleEventResponse(response *WebsocketEventResponse) { +func (k *Kraken) WsHandleEventResponse(response *WebsocketEventResponse, rawResponse []byte) { switch response.Event { case krakenWsHeartbeat: if k.Verbose { @@ -285,19 +213,9 @@ func (k *Kraken) WsHandleEventResponse(response *WebsocketEventResponse) { k.Name, krakenWSSupportedVersion, response.WebsocketStatusResponse.Version) } case krakenWsSubscriptionStatus: - if k.Verbose { - log.Debugf("%v Websocket subscription status data received", - k.Name) - } + k.WebsocketConn.AddResponseWithID(response.RequestID, rawResponse) if response.Status != "subscribed" { - if response.RequestID > 0 { - k.Websocket.DataHandler <- fmt.Errorf("%v requestID: '%v'. Error: %v", - k.Name, - response.RequestID, - response.WebsocketErrorResponse.ErrorMessage) - } else { - k.Websocket.DataHandler <- fmt.Errorf(response.WebsocketErrorResponse.ErrorMessage) - } + k.Websocket.DataHandler <- fmt.Errorf("%v %v %v", k.Name, response.RequestID, response.WebsocketErrorResponse.ErrorMessage) return } addNewSubscriptionChannelData(response) @@ -358,10 +276,10 @@ func (k *Kraken) wsProcessTickers(channelData *WebsocketChannelData, data interf lowPrice, _ := strconv.ParseFloat(lowData[0].(string), 64) quantity, _ := strconv.ParseFloat(volumeData[0].(string), 64) - k.Websocket.DataHandler <- exchange.TickerData{ + k.Websocket.DataHandler <- wshandler.TickerData{ Timestamp: time.Now(), Exchange: k.Name, - AssetType: krakenWsAssetType, + AssetType: orderbook.Spot, Pair: channelData.Pair, ClosePrice: closePrice, OpenPrice: openPrice, @@ -403,8 +321,8 @@ func (k *Kraken) wsProcessTrades(channelData *WebsocketChannelData, data interfa price, _ := strconv.ParseFloat(trade[0].(string), 64) amount, _ := strconv.ParseFloat(trade[1].(string), 64) - k.Websocket.DataHandler <- exchange.TradeData{ - AssetType: krakenWsAssetType, + k.Websocket.DataHandler <- wshandler.TradeData{ + AssetType: orderbook.Spot, CurrencyPair: channelData.Pair, EventTime: time.Now().Unix(), Exchange: k.Name, @@ -432,7 +350,7 @@ func (k *Kraken) wsProcessOrderBook(channelData *WebsocketChannelData, data inte if len(orderbookBuffer[channelData.ChannelID]) >= orderbookBufferLimit { err := k.wsProcessOrderBookUpdate(channelData) if err != nil { - subscriptionToRemove := exchange.WebsocketChannelSubscription{ + subscriptionToRemove := wshandler.WebsocketChannelSubscription{ Channel: krakenWsOrderbook, Currency: channelData.Pair, } @@ -447,7 +365,7 @@ func (k *Kraken) wsProcessOrderBook(channelData *WebsocketChannelData, data inte func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, obData map[string]interface{}) { ob := orderbook.Base{ Pair: channelData.Pair, - AssetType: krakenWsAssetType, + AssetType: orderbook.Spot, } // Kraken ob data is timestamped per price, GCT orderbook data is timestamped per entry // Using the highest last update time, we can attempt to respect both within a reasonable degree @@ -495,9 +413,9 @@ func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, ob return } - k.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + k.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Exchange: k.Name, - Asset: krakenWsAssetType, + Asset: orderbook.Spot, Pair: channelData.Pair, } @@ -509,7 +427,7 @@ func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, ob func (k *Kraken) wsProcessOrderBookBuffer(channelData *WebsocketChannelData, obData map[string]interface{}) { ob := orderbook.Base{ - AssetType: krakenWsAssetType, + AssetType: orderbook.Spot, ExchangeName: k.Name, Pair: channelData.Pair, } @@ -618,9 +536,9 @@ func (k *Kraken) wsProcessOrderBookUpdate(channelData *WebsocketChannelData) err return err } - k.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + k.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Exchange: k.Name, - Asset: krakenWsAssetType, + Asset: orderbook.Spot, Pair: channelData.Pair, } // Reset the buffer @@ -754,8 +672,8 @@ func (k *Kraken) wsProcessCandles(channelData *WebsocketChannelData, data interf closePrice, _ := strconv.ParseFloat(candleData[5].(string), 64) volume, _ := strconv.ParseFloat(candleData[7].(string), 64) - k.Websocket.DataHandler <- exchange.KlineData{ - AssetType: krakenWsAssetType, + k.Websocket.DataHandler <- wshandler.KlineData{ + AssetType: orderbook.Spot, Pair: channelData.Pair, Timestamp: time.Now(), Exchange: k.Name, @@ -774,11 +692,11 @@ func (k *Kraken) wsProcessCandles(channelData *WebsocketChannelData, data interf // GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() func (k *Kraken) GenerateDefaultSubscriptions() { enabledCurrencies := k.GetEnabledCurrencies() - var subscriptions []exchange.WebsocketChannelSubscription + var subscriptions []wshandler.WebsocketChannelSubscription for i := range defaultSubscribedChannels { for j := range enabledCurrencies { enabledCurrencies[j].Delimiter = "/" - subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ Channel: defaultSubscribedChannels[i], Currency: enabledCurrencies[j], }) @@ -788,39 +706,29 @@ func (k *Kraken) GenerateDefaultSubscriptions() { } // Subscribe sends a websocket message to receive data from the channel -func (k *Kraken) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { +func (k *Kraken) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { resp := WebsocketSubscriptionEventRequest{ Event: krakenWsSubscribe, Pairs: []string{channelToSubscribe.Currency.String()}, Subscription: WebsocketSubscriptionData{ Name: channelToSubscribe.Channel, }, + RequestID: k.WebsocketConn.GenerateMessageID(true), } - json, err := common.JSONEncode(resp) - if err != nil { - if k.Verbose { - log.Debugf("%v subscribe error: %v", k.Name, err) - } - return err - } - return k.writeToWebsocket(json) + _, err := k.WebsocketConn.SendMessageReturnResponse(resp.RequestID, resp) + return err } // Unsubscribe sends a websocket message to stop receiving data from the channel -func (k *Kraken) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { +func (k *Kraken) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { resp := WebsocketSubscriptionEventRequest{ Event: krakenWsUnsubscribe, Pairs: []string{channelToSubscribe.Currency.String()}, Subscription: WebsocketSubscriptionData{ Name: channelToSubscribe.Channel, }, + RequestID: k.WebsocketConn.GenerateMessageID(true), } - json, err := common.JSONEncode(resp) - if err != nil { - if k.Verbose { - log.Debugf("%v unsubscribe error: %v", k.Name, err) - } - return err - } - return k.writeToWebsocket(json) + _, err := k.WebsocketConn.SendMessageReturnResponse(resp.RequestID, resp) + return err } diff --git a/exchanges/kraken/kraken_wrapper.go b/exchanges/kraken/kraken_wrapper.go index c864a980..7a069cf0 100644 --- a/exchanges/kraken/kraken_wrapper.go +++ b/exchanges/kraken/kraken_wrapper.go @@ -12,6 +12,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" ) @@ -305,7 +306,7 @@ func (k *Kraken) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange. } // GetWebsocket returns a pointer to the exchange websocket -func (k *Kraken) GetWebsocket() (*exchange.Websocket, error) { +func (k *Kraken) GetWebsocket() (*wshandler.Websocket, error) { return k.Websocket, nil } @@ -397,20 +398,20 @@ func (k *Kraken) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([ // SubscribeToWebsocketChannels appends to ChannelsToSubscribe // which lets websocket.manageSubscriptions handle subscribing -func (k *Kraken) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (k *Kraken) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { k.Websocket.SubscribeToChannels(channels) return nil } // UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe // which lets websocket.manageSubscriptions handle unsubscribing -func (k *Kraken) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { - k.Websocket.UnsubscribeToChannels(channels) +func (k *Kraken) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { + k.Websocket.RemoveSubscribedChannels(channels) return nil } // GetSubscriptions returns a copied list of subscriptions -func (k *Kraken) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) { +func (k *Kraken) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { return k.Websocket.GetSubscriptions(), nil } diff --git a/exchanges/lakebtc/lakebtc.go b/exchanges/lakebtc/lakebtc.go index a220b46a..ddef7ea7 100644 --- a/exchanges/lakebtc/lakebtc.go +++ b/exchanges/lakebtc/lakebtc.go @@ -14,6 +14,7 @@ import ( 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" ) @@ -65,7 +66,7 @@ func (l *LakeBTC) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) l.APIUrlDefault = lakeBTCAPIURL l.APIUrl = l.APIUrlDefault - l.WebsocketInit() + l.Websocket = wshandler.New() } // Setup sets exchange configuration profile diff --git a/exchanges/lakebtc/lakebtc_wrapper.go b/exchanges/lakebtc/lakebtc_wrapper.go index 987f077c..27a56314 100644 --- a/exchanges/lakebtc/lakebtc_wrapper.go +++ b/exchanges/lakebtc/lakebtc_wrapper.go @@ -13,6 +13,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" ) @@ -266,7 +267,7 @@ func (l *LakeBTC) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange } // GetWebsocket returns a pointer to the exchange websocket -func (l *LakeBTC) GetWebsocket() (*exchange.Websocket, error) { +func (l *LakeBTC) GetWebsocket() (*wshandler.Websocket, error) { // Documents are too vague to implement return nil, common.ErrFunctionNotSupported } @@ -351,18 +352,18 @@ func (l *LakeBTC) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ( // SubscribeToWebsocketChannels appends to ChannelsToSubscribe // which lets websocket.manageSubscriptions handle subscribing -func (l *LakeBTC) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (l *LakeBTC) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { return common.ErrFunctionNotSupported } // UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe // which lets websocket.manageSubscriptions handle unsubscribing -func (l *LakeBTC) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (l *LakeBTC) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { return common.ErrFunctionNotSupported } // GetSubscriptions returns a copied list of subscriptions -func (l *LakeBTC) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) { +func (l *LakeBTC) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/localbitcoins/localbitcoins.go b/exchanges/localbitcoins/localbitcoins.go index 2aaaef85..9a6eaaa5 100644 --- a/exchanges/localbitcoins/localbitcoins.go +++ b/exchanges/localbitcoins/localbitcoins.go @@ -13,6 +13,7 @@ import ( "github.com/thrasher-/gocryptotrader/config" exchange "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/request" + "github.com/thrasher-/gocryptotrader/exchanges/wshandler" log "github.com/thrasher-/gocryptotrader/logger" ) @@ -134,7 +135,7 @@ func (l *LocalBitcoins) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) l.APIUrlDefault = localbitcoinsAPIURL l.APIUrl = l.APIUrlDefault - l.WebsocketInit() + l.Websocket = wshandler.New() } // Setup sets exchange configuration parameters diff --git a/exchanges/localbitcoins/localbitcoins_wrapper.go b/exchanges/localbitcoins/localbitcoins_wrapper.go index aa869a6f..71e246a9 100644 --- a/exchanges/localbitcoins/localbitcoins_wrapper.go +++ b/exchanges/localbitcoins/localbitcoins_wrapper.go @@ -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" ) @@ -296,7 +297,7 @@ func (l *LocalBitcoins) WithdrawFiatFundsToInternationalBank(withdrawRequest *ex } // GetWebsocket returns a pointer to the exchange websocket -func (l *LocalBitcoins) GetWebsocket() (*exchange.Websocket, error) { +func (l *LocalBitcoins) GetWebsocket() (*wshandler.Websocket, error) { return nil, common.ErrFunctionNotSupported } @@ -430,18 +431,18 @@ func (l *LocalBitcoins) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequ // SubscribeToWebsocketChannels appends to ChannelsToSubscribe // which lets websocket.manageSubscriptions handle subscribing -func (l *LocalBitcoins) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (l *LocalBitcoins) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { return common.ErrFunctionNotSupported } // UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe // which lets websocket.manageSubscriptions handle unsubscribing -func (l *LocalBitcoins) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (l *LocalBitcoins) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { return common.ErrFunctionNotSupported } // GetSubscriptions returns a copied list of subscriptions -func (l *LocalBitcoins) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) { +func (l *LocalBitcoins) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/okcoin/okcoin.go b/exchanges/okcoin/okcoin.go index 763942b3..499897f6 100644 --- a/exchanges/okcoin/okcoin.go +++ b/exchanges/okcoin/okcoin.go @@ -8,6 +8,7 @@ import ( "github.com/thrasher-/gocryptotrader/exchanges/okgroup" "github.com/thrasher-/gocryptotrader/exchanges/request" "github.com/thrasher-/gocryptotrader/exchanges/ticker" + "github.com/thrasher-/gocryptotrader/exchanges/wshandler" ) const ( @@ -48,14 +49,17 @@ func (o *OKCoin) SetDefaults() { o.APIUrlDefault = okCoinAPIURL o.APIUrl = okCoinAPIURL o.AssetTypes = []string{ticker.Spot} - o.WebsocketInit() + o.Websocket = wshandler.New() o.WebsocketURL = okCoinWebsocketURL o.APIVersion = okCoinAPIVersion - o.Websocket.Functionality = exchange.WebsocketTickerSupported | - exchange.WebsocketTradeDataSupported | - exchange.WebsocketKlineSupported | - exchange.WebsocketOrderbookSupported | - exchange.WebsocketSubscribeSupported | - exchange.WebsocketUnsubscribeSupported | - exchange.WebsocketAuthenticatedEndpointsSupported + o.Websocket.Functionality = wshandler.WebsocketTickerSupported | + wshandler.WebsocketTradeDataSupported | + wshandler.WebsocketKlineSupported | + wshandler.WebsocketOrderbookSupported | + wshandler.WebsocketSubscribeSupported | + wshandler.WebsocketUnsubscribeSupported | + wshandler.WebsocketAuthenticatedEndpointsSupported | + wshandler.WebsocketMessageCorrelationSupported + o.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + o.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout } diff --git a/exchanges/okcoin/okcoin_test.go b/exchanges/okcoin/okcoin_test.go index 28d18f8f..4329f1a8 100644 --- a/exchanges/okcoin/okcoin_test.go +++ b/exchanges/okcoin/okcoin_test.go @@ -14,6 +14,7 @@ import ( exchange "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/okgroup" "github.com/thrasher-/gocryptotrader/exchanges/sharedtestvalues" + "github.com/thrasher-/gocryptotrader/exchanges/wshandler" ) // Please supply you own test keys here for due diligence testing. @@ -804,25 +805,27 @@ func TestGetMarginTransactionDetails(t *testing.T) { func TestSendWsMessages(t *testing.T) { TestSetDefaults(t) if !o.Websocket.IsEnabled() && !o.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() { - t.Skip(exchange.WebsocketNotEnabled) + t.Skip(wshandler.WebsocketNotEnabled) + } + var ok bool + o.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: o.Name, + URL: o.Websocket.GetWebsocketURL(), + Verbose: o.Verbose, + ResponseMaxLimit: exchange.DefaultWebsocketResponseMaxLimit, + ResponseCheckTimeout: exchange.DefaultWebsocketResponseCheckTimeout, } var dialer websocket.Dialer - var err error - var ok bool - o.WebsocketConn, _, err = dialer.Dial(o.Websocket.GetWebsocketURL(), - http.Header{}) + err := o.WebsocketConn.Dial(&dialer, http.Header{}) if err != nil { - t.Fatalf("%s Unable to connect to Websocket. Error: %s", - o.Name, - err) + t.Fatal(err) } - defer o.WebsocketConn.Close() wg := sync.WaitGroup{} wg.Add(1) go o.WsHandleData(&wg) wg.Wait() - subscription := exchange.WebsocketChannelSubscription{ + subscription := wshandler.WebsocketChannelSubscription{ Channel: "badChannel", } o.Subscribe(subscription) diff --git a/exchanges/okex/okex.go b/exchanges/okex/okex.go index 428665d7..517afa22 100644 --- a/exchanges/okex/okex.go +++ b/exchanges/okex/okex.go @@ -11,6 +11,7 @@ import ( "github.com/thrasher-/gocryptotrader/exchanges/okgroup" "github.com/thrasher-/gocryptotrader/exchanges/request" "github.com/thrasher-/gocryptotrader/exchanges/ticker" + "github.com/thrasher-/gocryptotrader/exchanges/wshandler" ) const ( @@ -73,16 +74,20 @@ func (o *OKEX) SetDefaults() { o.APIUrlDefault = okExAPIURL o.APIUrl = okExAPIURL o.AssetTypes = []string{ticker.Spot} - o.WebsocketInit() + o.Websocket = wshandler.New() o.APIVersion = okExAPIVersion o.WebsocketURL = OkExWebsocketURL - o.Websocket.Functionality = exchange.WebsocketTickerSupported | - exchange.WebsocketTradeDataSupported | - exchange.WebsocketKlineSupported | - exchange.WebsocketOrderbookSupported | - exchange.WebsocketSubscribeSupported | - exchange.WebsocketUnsubscribeSupported | - exchange.WebsocketAuthenticatedEndpointsSupported + o.Websocket.Functionality = wshandler.WebsocketTickerSupported | + wshandler.WebsocketTradeDataSupported | + wshandler.WebsocketKlineSupported | + wshandler.WebsocketOrderbookSupported | + wshandler.WebsocketSubscribeSupported | + wshandler.WebsocketUnsubscribeSupported | + wshandler.WebsocketAuthenticatedEndpointsSupported | + wshandler.WebsocketMessageCorrelationSupported + o.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + o.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout + } // GetFuturesPostions Get the information of all holding positions in futures trading. diff --git a/exchanges/okex/okex_test.go b/exchanges/okex/okex_test.go index 5431252a..69c6eb85 100644 --- a/exchanges/okex/okex_test.go +++ b/exchanges/okex/okex_test.go @@ -15,6 +15,7 @@ import ( exchange "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/okgroup" "github.com/thrasher-/gocryptotrader/exchanges/sharedtestvalues" + "github.com/thrasher-/gocryptotrader/exchanges/wshandler" ) // Please supply you own test keys here for due diligence testing. @@ -1569,25 +1570,27 @@ func TestGetETTSettlementPriceHistory(t *testing.T) { func TestSendWsMessages(t *testing.T) { TestSetDefaults(t) if !o.Websocket.IsEnabled() && !o.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() { - t.Skip(exchange.WebsocketNotEnabled) + t.Skip(wshandler.WebsocketNotEnabled) + } + var ok bool + o.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: o.Name, + URL: o.Websocket.GetWebsocketURL(), + Verbose: o.Verbose, + ResponseMaxLimit: exchange.DefaultWebsocketResponseMaxLimit, + ResponseCheckTimeout: exchange.DefaultWebsocketResponseCheckTimeout, } var dialer websocket.Dialer - var err error - var ok bool - o.WebsocketConn, _, err = dialer.Dial(o.Websocket.GetWebsocketURL(), - http.Header{}) + err := o.WebsocketConn.Dial(&dialer, http.Header{}) if err != nil { - t.Fatalf("%s Unable to connect to Websocket. Error: %s", - o.Name, - err) + t.Fatal(err) } - defer o.WebsocketConn.Close() wg := sync.WaitGroup{} wg.Add(1) go o.WsHandleData(&wg) wg.Wait() - subscription := exchange.WebsocketChannelSubscription{ + subscription := wshandler.WebsocketChannelSubscription{ Channel: "badChannel", } o.Subscribe(subscription) diff --git a/exchanges/okgroup/okgroup.go b/exchanges/okgroup/okgroup.go index 20097be1..a9ce3524 100644 --- a/exchanges/okgroup/okgroup.go +++ b/exchanges/okgroup/okgroup.go @@ -10,14 +10,13 @@ import ( "reflect" "strconv" "strings" - "sync" "time" "github.com/google/go-querystring/query" - "github.com/gorilla/websocket" "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" exchange "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/wshandler" log "github.com/thrasher-/gocryptotrader/logger" ) @@ -87,8 +86,7 @@ var errMissValue = errors.New("warning - resp value is missing from exchange") type OKGroup struct { exchange.Base ExchangeName string - WebsocketConn *websocket.Conn - wsRequestMtx sync.Mutex + WebsocketConn *wshandler.WebsocketConnection // Spot and contract market error codes as per https://www.okex.com/rest_request.html ErrorCodes map[string]error // Stores for corresponding variable checks @@ -141,17 +139,27 @@ func (o *OKGroup) Setup(exch *config.ExchangeConfig) { if err != nil { log.Fatal(err) } - err = o.WebsocketSetup(o.WsConnect, + err = o.Websocket.Setup(o.WsConnect, o.Subscribe, o.Unsubscribe, exch.Name, exch.Websocket, exch.Verbose, o.WebsocketURL, - exch.WebsocketURL) + exch.WebsocketURL, + exch.AuthenticatedWebsocketAPISupport) if err != nil { log.Fatal(err) } + o.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: o.Name, + URL: o.Websocket.GetWebsocketURL(), + ProxyURL: o.Websocket.GetProxyAddress(), + Verbose: o.Verbose, + RateLimit: okGroupWsRateLimit, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } } } diff --git a/exchanges/okgroup/okgroup_websocket.go b/exchanges/okgroup/okgroup_websocket.go index 87c19489..905adbb9 100644 --- a/exchanges/okgroup/okgroup_websocket.go +++ b/exchanges/okgroup/okgroup_websocket.go @@ -1,34 +1,27 @@ package okgroup import ( - "bytes" - "compress/flate" "errors" "fmt" "hash/crc32" - "io/ioutil" "net/http" - "net/url" "sort" "strconv" "strings" "sync" "time" - "github.com/thrasher-/gocryptotrader/currency" - "github.com/gorilla/websocket" "github.com/thrasher-/gocryptotrader/common" + "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" ) // List of all websocket channels to subscribe to const ( - // If a checksum fails, then resubscribing to the channel fails, fatal after these attempts - okGroupWsResubscribeFailureLimit = 3 - okGroupWsResubscribeDelayInSeconds = 3 // Orderbook events okGroupWsOrderbookUpdate = "update" okGroupWsOrderbookPartial = "partial" @@ -144,50 +137,22 @@ const ( okGroupWsFuturesPosition = okGroupWsFuturesSubsection + okGroupWsPosition okGroupWsFuturesOrder = okGroupWsFuturesSubsection + okGroupWsOrder - okGroupWsRateLimit = 30 * time.Millisecond + okGroupWsRateLimit = 30 ) // orderbookMutex Ensures if two entries arrive at once, only one can be processed at a time var orderbookMutex sync.Mutex var defaultSubscribedChannels = []string{okGroupWsSpotDepth, okGroupWsSpotCandle300s, okGroupWsSpotTicker, okGroupWsSpotTrade} -// writeToWebsocket sends a message to the websocket endpoint -func (o *OKGroup) writeToWebsocket(message string) error { - o.wsRequestMtx.Lock() - defer o.wsRequestMtx.Unlock() - if o.Verbose { - log.Debugf("%v sending message to WS: %v", o.Name, message) - } - // Really basic WS rate limit - time.Sleep(okGroupWsRateLimit) - return o.WebsocketConn.WriteMessage(websocket.TextMessage, []byte(message)) -} - // WsConnect initiates a websocket connection func (o *OKGroup) WsConnect() error { if !o.Websocket.IsEnabled() || !o.IsEnabled() { - return errors.New(exchange.WebsocketNotEnabled) + return errors.New(wshandler.WebsocketNotEnabled) } - var dialer websocket.Dialer - if o.Websocket.GetProxyAddress() != "" { - proxy, err := url.Parse(o.Websocket.GetProxyAddress()) - if err != nil { - return err - } - dialer.Proxy = http.ProxyURL(proxy) - } - - var err error - if o.Verbose { - log.Debugf("Attempting to connect to %v", o.Websocket.GetWebsocketURL()) - } - o.WebsocketConn, _, err = dialer.Dial(o.Websocket.GetWebsocketURL(), - http.Header{}) + err := o.WebsocketConn.Dial(&dialer, http.Header{}) if err != nil { - return fmt.Errorf("%s Unable to connect to Websocket. Error: %s", - o.Name, - err) + return err } if o.Verbose { log.Debugf("Successful connection to %v", @@ -210,34 +175,6 @@ func (o *OKGroup) WsConnect() error { return nil } -// WsReadData reads data from the websocket connection -func (o *OKGroup) WsReadData() (exchange.WebsocketResponse, error) { - mType, resp, err := o.WebsocketConn.ReadMessage() - if err != nil { - return exchange.WebsocketResponse{}, err - } - - o.Websocket.TrafficAlert <- struct{}{} - var standardMessage []byte - switch mType { - case websocket.TextMessage: - standardMessage = resp - - case websocket.BinaryMessage: - reader := flate.NewReader(bytes.NewReader(resp)) - standardMessage, err = ioutil.ReadAll(reader) - reader.Close() - if err != nil { - return exchange.WebsocketResponse{}, err - } - } - if o.Verbose { - log.Debugf("%v Websocket message received: %v", o.Name, string(standardMessage)) - } - - return exchange.WebsocketResponse{Raw: standardMessage}, nil -} - // wsPingHandler sends a message "ping" every 27 to maintain the connection to the websocket func (o *OKGroup) wsPingHandler(wg *sync.WaitGroup) { o.Websocket.Wg.Add(1) @@ -254,7 +191,7 @@ func (o *OKGroup) wsPingHandler(wg *sync.WaitGroup) { return case <-ticker.C: - err := o.writeToWebsocket("ping") + err := o.WebsocketConn.SendMessage("ping") if o.Verbose { log.Debugf("%v sending ping", o.GetName()) } @@ -280,11 +217,12 @@ func (o *OKGroup) WsHandleData(wg *sync.WaitGroup) { return default: - resp, err := o.WsReadData() + resp, err := o.WebsocketConn.ReadMessage() if err != nil { - time.Sleep(time.Second) o.Websocket.DataHandler <- err + return } + o.Websocket.TrafficAlert <- struct{}{} var dataResponse WebsocketDataResponse err = common.JSONDecode(resp.Raw, &dataResponse) if err == nil && dataResponse.Table != "" { @@ -326,16 +264,11 @@ func (o *OKGroup) WsLogin() error { signPath := "/users/self/verify" hmac := common.GetHMAC(common.HashSHA256, []byte(fmt.Sprintf("%v", unixTime)+http.MethodGet+signPath), []byte(o.APISecret)) base64 := common.Base64Encode(hmac) - resp := WebsocketEventRequest{ + request := WebsocketEventRequest{ Operation: "login", Arguments: []string{o.APIKey, o.ClientID, fmt.Sprintf("%v", unixTime), base64}, } - json, err := common.JSONEncode(resp) - if err != nil { - o.Websocket.SetCanUseAuthenticatedEndpoints(false) - return err - } - err = o.writeToWebsocket(string(json)) + err := o.WebsocketConn.SendMessage(request) if err != nil { o.Websocket.SetCanUseAuthenticatedEndpoints(false) return err @@ -384,20 +317,14 @@ func (o *OKGroup) WsHandleDataResponse(response *WebsocketDataResponse) { case okGroupWsCandle60s, okGroupWsCandle180s, okGroupWsCandle300s, okGroupWsCandle900s, okGroupWsCandle1800s, okGroupWsCandle3600s, okGroupWsCandle7200s, okGroupWsCandle14400s, okGroupWsCandle21600s, okGroupWsCandle43200s, okGroupWsCandle86400s, okGroupWsCandle604900s: - if o.Verbose { - log.Debugf("%v Websocket candle data received", o.GetName()) - } o.wsProcessCandles(response) case okGroupWsDepth, okGroupWsDepth5: - if o.Verbose { - log.Debugf("%v Websocket orderbook data received", o.GetName()) - } // Locking, orderbooks cannot be processed out of order orderbookMutex.Lock() err := o.WsProcessOrderBook(response) if err != nil { pair := currency.NewPairDelimiter(response.Data[0].InstrumentID, "-") - channelToResubscribe := exchange.WebsocketChannelSubscription{ + channelToResubscribe := wshandler.WebsocketChannelSubscription{ Channel: response.Table, Currency: pair, } @@ -405,14 +332,8 @@ func (o *OKGroup) WsHandleDataResponse(response *WebsocketDataResponse) { } orderbookMutex.Unlock() case okGroupWsTicker: - if o.Verbose { - log.Debugf("%v Websocket ticker data received", o.GetName()) - } o.wsProcessTickers(response) case okGroupWsTrade: - if o.Verbose { - log.Debugf("%v Websocket trade data received", o.GetName()) - } o.wsProcessTrades(response) default: logDataResponse(response) @@ -435,7 +356,7 @@ func logDataResponse(response *WebsocketDataResponse) { func (o *OKGroup) wsProcessTickers(response *WebsocketDataResponse) { for i := range response.Data { instrument := currency.NewPairDelimiter(response.Data[i].InstrumentID, "-") - o.Websocket.DataHandler <- exchange.TickerData{ + o.Websocket.DataHandler <- wshandler.TickerData{ Timestamp: response.Data[i].Timestamp, Exchange: o.GetName(), AssetType: o.GetAssetTypeFromTableName(response.Table), @@ -451,7 +372,7 @@ func (o *OKGroup) wsProcessTickers(response *WebsocketDataResponse) { func (o *OKGroup) wsProcessTrades(response *WebsocketDataResponse) { for i := range response.Data { instrument := currency.NewPairDelimiter(response.Data[i].InstrumentID, "-") - o.Websocket.DataHandler <- exchange.TradeData{ + o.Websocket.DataHandler <- wshandler.TradeData{ Amount: response.Data[i].Size, AssetType: o.GetAssetTypeFromTableName(response.Table), CurrencyPair: instrument, @@ -480,7 +401,7 @@ func (o *OKGroup) wsProcessCandles(response *WebsocketDataResponse) { candleInterval = response.Table[candleIndex+len(okGroupWsCandle) : secondIndex] } - klineData := exchange.KlineData{ + klineData := wshandler.KlineData{ AssetType: o.GetAssetTypeFromTableName(response.Table), Pair: instrument, Exchange: o.GetName(), @@ -548,7 +469,7 @@ func (o *OKGroup) WsProcessPartialOrderBook(wsEventData *WebsocketDataWrapper, i if err != nil { return err } - o.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + o.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Exchange: o.GetName(), Asset: o.GetAssetTypeFromTableName(tableName), Pair: instrument, @@ -591,7 +512,7 @@ func (o *OKGroup) WsProcessUpdateOrderbook(wsEventData *WebsocketDataWrapper, in if err != nil { log.Error(err) } - o.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + o.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Exchange: o.GetName(), Asset: o.GetAssetTypeFromTableName(tableName), Pair: instrument, @@ -694,14 +615,14 @@ func (o *OKGroup) CalculateUpdateOrderbookChecksum(orderbookData *orderbook.Base // GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() func (o *OKGroup) GenerateDefaultSubscriptions() { enabledCurrencies := o.GetEnabledCurrencies() - var subscriptions []exchange.WebsocketChannelSubscription + var subscriptions []wshandler.WebsocketChannelSubscription if o.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { defaultSubscribedChannels = append(defaultSubscribedChannels, okGroupWsSpotMarginAccount, okGroupWsSpotAccount, okGroupWsSpotOrder) } for i := range defaultSubscribedChannels { for j := range enabledCurrencies { enabledCurrencies[j].Delimiter = "-" - subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ Channel: defaultSubscribedChannels[i], Currency: enabledCurrencies[j], }) @@ -711,37 +632,23 @@ func (o *OKGroup) GenerateDefaultSubscriptions() { } // Subscribe sends a websocket message to receive data from the channel -func (o *OKGroup) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { - resp := WebsocketEventRequest{ +func (o *OKGroup) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { + request := WebsocketEventRequest{ Operation: "subscribe", Arguments: []string{fmt.Sprintf("%v:%v", channelToSubscribe.Channel, channelToSubscribe.Currency.String())}, } if strings.EqualFold(channelToSubscribe.Channel, okGroupWsSpotAccount) { - resp.Arguments = []string{fmt.Sprintf("%v:%v", channelToSubscribe.Channel, channelToSubscribe.Currency.Base.String())} + request.Arguments = []string{fmt.Sprintf("%v:%v", channelToSubscribe.Channel, channelToSubscribe.Currency.Base.String())} } - json, err := common.JSONEncode(resp) - if err != nil { - if o.Verbose { - log.Debugf("%v subscribe error: %v", o.Name, err) - } - return err - } - return o.writeToWebsocket(string(json)) + return o.WebsocketConn.SendMessage(request) } // Unsubscribe sends a websocket message to stop receiving data from the channel -func (o *OKGroup) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { - resp := WebsocketEventRequest{ +func (o *OKGroup) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { + request := WebsocketEventRequest{ Operation: "unsubscribe", Arguments: []string{fmt.Sprintf("%v:%v", channelToSubscribe.Channel, channelToSubscribe.Currency.String())}, } - json, err := common.JSONEncode(resp) - if err != nil { - if o.Verbose { - log.Debugf("%v unsubscribe error: %v", o.Name, err) - } - return err - } - return o.writeToWebsocket(string(json)) + return o.WebsocketConn.SendMessage(request) } diff --git a/exchanges/okgroup/okgroup_wrapper.go b/exchanges/okgroup/okgroup_wrapper.go index 7354ea1a..7f4765b8 100644 --- a/exchanges/okgroup/okgroup_wrapper.go +++ b/exchanges/okgroup/okgroup_wrapper.go @@ -11,6 +11,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" ) @@ -414,7 +415,7 @@ func (o *OKGroup) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ( } // GetWebsocket returns a pointer to the exchange websocket -func (o *OKGroup) GetWebsocket() (*exchange.Websocket, error) { +func (o *OKGroup) GetWebsocket() (*wshandler.Websocket, error) { return o.Websocket, nil } @@ -434,20 +435,20 @@ func (o *OKGroup) GetWithdrawCapabilities() uint32 { // SubscribeToWebsocketChannels appends to ChannelsToSubscribe // which lets websocket.manageSubscriptions handle subscribing -func (o *OKGroup) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (o *OKGroup) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { o.Websocket.SubscribeToChannels(channels) return nil } // UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe // which lets websocket.manageSubscriptions handle unsubscribing -func (o *OKGroup) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { - o.Websocket.UnsubscribeToChannels(channels) +func (o *OKGroup) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { + o.Websocket.RemoveSubscribedChannels(channels) return nil } // GetSubscriptions returns a copied list of subscriptions -func (o *OKGroup) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) { +func (o *OKGroup) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { return o.Websocket.GetSubscriptions(), nil } diff --git a/exchanges/poloniex/poloniex.go b/exchanges/poloniex/poloniex.go index 61d31ddb..edd03bc8 100644 --- a/exchanges/poloniex/poloniex.go +++ b/exchanges/poloniex/poloniex.go @@ -8,16 +8,15 @@ import ( "net/http" "net/url" "strconv" - "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" ) @@ -62,8 +61,7 @@ const ( // Poloniex is the overarching type across the poloniex package type Poloniex struct { exchange.Base - WebsocketConn *websocket.Conn - wsRequestMtx sync.Mutex + WebsocketConn *wshandler.WebsocketConnection } // SetDefaults sets default settings for poloniex @@ -88,13 +86,15 @@ func (p *Poloniex) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) p.APIUrlDefault = poloniexAPIURL p.APIUrl = p.APIUrlDefault - p.WebsocketInit() - p.Websocket.Functionality = exchange.WebsocketTradeDataSupported | - exchange.WebsocketOrderbookSupported | - exchange.WebsocketTickerSupported | - exchange.WebsocketSubscribeSupported | - exchange.WebsocketUnsubscribeSupported | - exchange.WebsocketAuthenticatedEndpointsSupported + p.Websocket = wshandler.New() + p.Websocket.Functionality = wshandler.WebsocketTradeDataSupported | + wshandler.WebsocketOrderbookSupported | + wshandler.WebsocketTickerSupported | + wshandler.WebsocketSubscribeSupported | + wshandler.WebsocketUnsubscribeSupported | + wshandler.WebsocketAuthenticatedEndpointsSupported + p.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + p.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout } // Setup sets user exchange configuration settings @@ -135,17 +135,26 @@ func (p *Poloniex) Setup(exch *config.ExchangeConfig) { if err != nil { log.Fatal(err) } - err = p.WebsocketSetup(p.WsConnect, + err = p.Websocket.Setup(p.WsConnect, p.Subscribe, p.Unsubscribe, exch.Name, exch.Websocket, exch.Verbose, poloniexWebsocketAddress, - exch.WebsocketURL) + exch.WebsocketURL, + exch.AuthenticatedWebsocketAPISupport) if err != nil { log.Fatal(err) } + p.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: p.Name, + URL: p.Websocket.GetWebsocketURL(), + ProxyURL: p.Websocket.GetProxyAddress(), + Verbose: p.Verbose, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } } } diff --git a/exchanges/poloniex/poloniex_test.go b/exchanges/poloniex/poloniex_test.go index 2eca506b..1bdd345a 100644 --- a/exchanges/poloniex/poloniex_test.go +++ b/exchanges/poloniex/poloniex_test.go @@ -11,6 +11,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" ) var p Poloniex @@ -443,22 +444,26 @@ func TestWsHandleAccountData(t *testing.T) { func TestWsAuth(t *testing.T) { TestSetup(t) if !p.Websocket.IsEnabled() && !p.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() { - t.Skip(exchange.WebsocketNotEnabled) + t.Skip(wshandler.WebsocketNotEnabled) + } + p.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: p.Name, + URL: p.Websocket.GetWebsocketURL(), + Verbose: p.Verbose, + ResponseMaxLimit: exchange.DefaultWebsocketResponseMaxLimit, + ResponseCheckTimeout: exchange.DefaultWebsocketResponseCheckTimeout, } - var err error var dialer websocket.Dialer - p.WebsocketConn, _, err = dialer.Dial(p.Websocket.GetWebsocketURL(), - http.Header{}) + err := p.WebsocketConn.Dial(&dialer, http.Header{}) if err != nil { t.Fatal(err) } p.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride() p.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride() go p.WsHandleData() - defer p.WebsocketConn.Close() err = p.wsSendAuthorisedCommand("subscribe") if err != nil { - t.Error(err) + t.Fatal(err) } timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout) select { diff --git a/exchanges/poloniex/poloniex_websocket.go b/exchanges/poloniex/poloniex_websocket.go index 7d840723..bba56693 100644 --- a/exchanges/poloniex/poloniex_websocket.go +++ b/exchanges/poloniex/poloniex_websocket.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "net/http" - "net/url" "strconv" "strings" "time" @@ -14,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" ) @@ -33,22 +33,10 @@ var ( // WsConnect initiates a websocket connection func (p *Poloniex) WsConnect() error { if !p.Websocket.IsEnabled() || !p.IsEnabled() { - return errors.New(exchange.WebsocketNotEnabled) + return errors.New(wshandler.WebsocketNotEnabled) } - var dialer websocket.Dialer - if p.Websocket.GetProxyAddress() != "" { - proxy, err := url.Parse(p.Websocket.GetProxyAddress()) - if err != nil { - return err - } - - dialer.Proxy = http.ProxyURL(proxy) - } - - var err error - p.WebsocketConn, _, err = dialer.Dial(p.Websocket.GetWebsocketURL(), - http.Header{}) + err := p.WebsocketConn.Dial(&dialer, http.Header{}) if err != nil { return err } @@ -71,17 +59,6 @@ func (p *Poloniex) WsConnect() error { return nil } -// WsReadData reads data from the websocket connection -func (p *Poloniex) WsReadData() (exchange.WebsocketResponse, error) { - _, resp, err := p.WebsocketConn.ReadMessage() - if err != nil { - return exchange.WebsocketResponse{}, err - } - - p.Websocket.TrafficAlert <- struct{}{} - return exchange.WebsocketResponse{Raw: resp}, nil -} - func getWSDataType(data interface{}) string { subData := data.([]interface{}) dataType := subData[0].(string) @@ -106,12 +83,12 @@ func (p *Poloniex) WsHandleData() { return default: - resp, err := p.WsReadData() + resp, err := p.WebsocketConn.ReadMessage() if err != nil { p.Websocket.DataHandler <- err return } - + p.Websocket.TrafficAlert <- struct{}{} var result interface{} err = common.JSONDecode(resp.Raw, &result) if err != nil { @@ -171,7 +148,7 @@ func (p *Poloniex) WsHandleData() { continue } - p.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + p.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Exchange: p.GetName(), Asset: "SPOT", Pair: currency.NewPairFromString(currencyPair), @@ -184,7 +161,7 @@ func (p *Poloniex) WsHandleData() { continue } - p.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + p.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Exchange: p.GetName(), Asset: "SPOT", Pair: currency.NewPairFromString(currencyPair), @@ -204,7 +181,7 @@ func (p *Poloniex) WsHandleData() { trade.Price, _ = strconv.ParseFloat(dataL3[4].(string), 64) trade.Timestamp = int64(dataL3[5].(float64)) - p.Websocket.DataHandler <- exchange.TradeData{ + p.Websocket.DataHandler <- wshandler.TradeData{ Timestamp: time.Unix(trade.Timestamp, 0), CurrencyPair: currency.NewPairFromString(currencyPair), Side: trade.Side, @@ -237,7 +214,7 @@ func (p *Poloniex) wsHandleTickerData(data []interface{}) { t.HighestTradeIn24H, _ = strconv.ParseFloat(tickerData[8].(string), 64) t.LowestTradePrice24H, _ = strconv.ParseFloat(tickerData[9].(string), 64) - p.Websocket.DataHandler <- exchange.TickerData{ + p.Websocket.DataHandler <- wshandler.TickerData{ Timestamp: time.Now(), Exchange: p.GetName(), AssetType: "SPOT", @@ -494,14 +471,14 @@ var CurrencyPairID = map[int]string{ // GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() func (p *Poloniex) GenerateDefaultSubscriptions() { - var subscriptions []exchange.WebsocketChannelSubscription + var subscriptions []wshandler.WebsocketChannelSubscription // Tickerdata is its own channel - subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ Channel: fmt.Sprintf("%v", wsTickerDataID), }) if p.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { - subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ Channel: fmt.Sprintf("%v", wsAccountNotificationID), }) } @@ -509,7 +486,7 @@ func (p *Poloniex) GenerateDefaultSubscriptions() { enabledCurrencies := p.GetEnabledCurrencies() for j := range enabledCurrencies { enabledCurrencies[j].Delimiter = "_" - subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ Channel: "orderbook", Currency: enabledCurrencies[j], }) @@ -518,7 +495,7 @@ func (p *Poloniex) GenerateDefaultSubscriptions() { } // Subscribe sends a websocket message to receive data from the channel -func (p *Poloniex) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { +func (p *Poloniex) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { subscriptionRequest := WsCommand{ Command: "subscribe", } @@ -530,11 +507,11 @@ func (p *Poloniex) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscri default: subscriptionRequest.Channel = channelToSubscribe.Currency.String() } - return p.wsSend(subscriptionRequest) + return p.WebsocketConn.SendMessage(subscriptionRequest) } // Unsubscribe sends a websocket message to stop receiving data from the channel -func (p *Poloniex) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { +func (p *Poloniex) Unsubscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { unsubscriptionRequest := WsCommand{ Command: "unsubscribe", } @@ -546,21 +523,7 @@ func (p *Poloniex) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubsc default: unsubscriptionRequest.Channel = channelToSubscribe.Currency.String() } - return p.wsSend(unsubscriptionRequest) -} - -// WsSend sends data to the websocket server -func (p *Poloniex) wsSend(data interface{}) error { - p.wsRequestMtx.Lock() - defer p.wsRequestMtx.Unlock() - json, err := common.JSONEncode(data) - if err != nil { - return err - } - if p.Verbose { - log.Debugf("%v sending message to websocket %v", p.Name, data) - } - return p.WebsocketConn.WriteMessage(websocket.TextMessage, json) + return p.WebsocketConn.SendMessage(unsubscriptionRequest) } func (p *Poloniex) wsSendAuthorisedCommand(command string) error { @@ -573,5 +536,5 @@ func (p *Poloniex) wsSendAuthorisedCommand(command string) error { Key: p.APIKey, Payload: nonce, } - return p.wsSend(request) + return p.WebsocketConn.SendMessage(request) } diff --git a/exchanges/poloniex/poloniex_wrapper.go b/exchanges/poloniex/poloniex_wrapper.go index 6864a5fe..91cd6c14 100644 --- a/exchanges/poloniex/poloniex_wrapper.go +++ b/exchanges/poloniex/poloniex_wrapper.go @@ -12,6 +12,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" ) @@ -301,7 +302,7 @@ func (p *Poloniex) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchang } // GetWebsocket returns a pointer to the exchange websocket -func (p *Poloniex) GetWebsocket() (*exchange.Websocket, error) { +func (p *Poloniex) GetWebsocket() (*wshandler.Websocket, error) { return p.Websocket, nil } @@ -396,20 +397,20 @@ func (p *Poloniex) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) // SubscribeToWebsocketChannels appends to ChannelsToSubscribe // which lets websocket.manageSubscriptions handle subscribing -func (p *Poloniex) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (p *Poloniex) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { p.Websocket.SubscribeToChannels(channels) return nil } // UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe // which lets websocket.manageSubscriptions handle unsubscribing -func (p *Poloniex) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { - p.Websocket.UnsubscribeToChannels(channels) +func (p *Poloniex) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { + p.Websocket.RemoveSubscribedChannels(channels) return nil } // GetSubscriptions returns a copied list of subscriptions -func (p *Poloniex) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) { +func (p *Poloniex) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { return p.Websocket.GetSubscriptions(), nil } diff --git a/exchanges/sharedtestvalues/sharedtestvalues.go b/exchanges/sharedtestvalues/sharedtestvalues.go index e8e1fef4..a268282e 100644 --- a/exchanges/sharedtestvalues/sharedtestvalues.go +++ b/exchanges/sharedtestvalues/sharedtestvalues.go @@ -12,7 +12,7 @@ const ( WebsocketResponseExtendedTimeout = (15 * time.Second) // WebsocketChannelOverrideCapacity used in websocket testing // Defines channel capacity as defaults size can block tests - WebsocketChannelOverrideCapacity = 5 + WebsocketChannelOverrideCapacity = 10 ) // GetWebsocketInterfaceChannelOverride returns a new interface based channel diff --git a/exchanges/exchange_websocket.go b/exchanges/wshandler/websocket.go similarity index 94% rename from exchanges/exchange_websocket.go rename to exchanges/wshandler/websocket.go index 1edb9874..fd616d31 100644 --- a/exchanges/exchange_websocket.go +++ b/exchanges/wshandler/websocket.go @@ -1,4 +1,4 @@ -package exchange +package wshandler import ( "errors" @@ -13,9 +13,9 @@ import ( log "github.com/thrasher-/gocryptotrader/logger" ) -// WebsocketInit initialises the websocket struct -func (e *Base) WebsocketInit() { - e.Websocket = &Websocket{ +// New initialises the websocket struct +func New() *Websocket { + return &Websocket{ defaultURL: "", enabled: false, proxyAddr: "", @@ -24,37 +24,38 @@ func (e *Base) WebsocketInit() { } } -// WebsocketSetup sets main variables for websocket connection -func (e *Base) WebsocketSetup(connector func() error, +// Setup sets main variables for websocket connection +func (w *Websocket) Setup(connector func() error, subscriber func(channelToSubscribe WebsocketChannelSubscription) error, unsubscriber func(channelToUnsubscribe WebsocketChannelSubscription) error, exchangeName string, wsEnabled, verbose bool, defaultURL, - runningURL string) error { + runningURL string, + authenticatedWebsocketAPISupport bool) error { - e.Websocket.DataHandler = make(chan interface{}, 1) - e.Websocket.Connected = make(chan struct{}, 1) - e.Websocket.Disconnected = make(chan struct{}, 1) - e.Websocket.TrafficAlert = make(chan struct{}, 1) - e.Websocket.verbose = verbose + w.DataHandler = make(chan interface{}, 1) + w.Connected = make(chan struct{}, 1) + w.Disconnected = make(chan struct{}, 1) + w.TrafficAlert = make(chan struct{}, 1) + w.verbose = verbose - e.Websocket.SetChannelSubscriber(subscriber) - e.Websocket.SetChannelUnsubscriber(unsubscriber) - err := e.Websocket.SetWsStatusAndConnection(wsEnabled) + w.SetChannelSubscriber(subscriber) + w.SetChannelUnsubscriber(unsubscriber) + err := w.SetWsStatusAndConnection(wsEnabled) if err != nil { return err } - e.Websocket.SetDefaultURL(defaultURL) - e.Websocket.SetConnector(connector) - e.Websocket.SetWebsocketURL(runningURL) - e.Websocket.SetExchangeName(exchangeName) - e.Websocket.SetCanUseAuthenticatedEndpoints(e.AuthenticatedWebsocketAPISupport) + w.SetDefaultURL(defaultURL) + w.SetConnector(connector) + w.SetWebsocketURL(runningURL) + w.SetExchangeName(exchangeName) + w.SetCanUseAuthenticatedEndpoints(authenticatedWebsocketAPISupport) - e.Websocket.init = false - e.Websocket.noConnectionCheckLimit = 5 - e.Websocket.reconnectionLimit = 10 + w.init = false + w.noConnectionCheckLimit = 5 + w.reconnectionLimit = 10 return nil } @@ -269,9 +270,6 @@ func (w *Websocket) trafficMonitor(wg *sync.WaitGroup) { w.connected = true } w.m.Unlock() - if w.verbose { - log.Debugf("%v received a traffic alert", w.exchangeName) - } trafficTimer.Reset(WebsocketTrafficLimitTime) case <-trafficTimer.C: // Falls through when timer runs out newtimer := time.NewTimer(10 * time.Second) // New secondary timer set @@ -689,6 +687,15 @@ func (w *Websocket) FormatFunctionality() string { case WebsocketWithdrawSupported: functionality = append(functionality, WebsocketWithdrawSupportedText) + case WebsocketMessageCorrelationSupported: + functionality = append(functionality, WebsocketMessageCorrelationSupportedText) + + case WebsocketSequenceNumberSupported: + functionality = append(functionality, WebsocketSequenceNumberSupportedText) + + case WebsocketDeadMansSwitchSupported: + functionality = append(functionality, WebsocketDeadMansSwitchSupportedText) + default: functionality = append(functionality, fmt.Sprintf("%s[1<<%v]", UnknownWebsocketFunctionality, i)) @@ -808,6 +815,13 @@ func (w *Websocket) unsubscribeToChannels() error { return nil } +// RemoveSubscribedChannels removes supplied channels from channelsToSubscribe +func (w *Websocket) RemoveSubscribedChannels(channels []WebsocketChannelSubscription) { + for i := range channels { + w.removeChannelToSubscribe(channels[i]) + } +} + // removeChannelToSubscribe removes an entry from w.channelsToSubscribe // so an unsubscribe event can be triggered func (w *Websocket) removeChannelToSubscribe(subscribedChannel WebsocketChannelSubscription) { @@ -867,13 +881,6 @@ func (w *Websocket) SubscribeToChannels(channels []WebsocketChannelSubscription) w.noConnectionChecks = 0 } -// UnsubscribeToChannels removes supplied channels from channelsToSubscribe -func (w *Websocket) UnsubscribeToChannels(channels []WebsocketChannelSubscription) { - for i := range channels { - w.removeChannelToSubscribe(channels[i]) - } -} - // Equal two WebsocketChannelSubscription to determine equality func (w *WebsocketChannelSubscription) Equal(subscribedChannel *WebsocketChannelSubscription) bool { return strings.EqualFold(w.Channel, subscribedChannel.Channel) && diff --git a/exchanges/wshandler/websocket_connection.go b/exchanges/wshandler/websocket_connection.go new file mode 100644 index 00000000..d4d7643e --- /dev/null +++ b/exchanges/wshandler/websocket_connection.go @@ -0,0 +1,177 @@ +package wshandler + +import ( + "bytes" + "compress/flate" + "compress/gzip" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "sync" + "time" + + "github.com/gorilla/websocket" + "github.com/thrasher-/gocryptotrader/common" + log "github.com/thrasher-/gocryptotrader/logger" +) + +// AddResponseWithID adds data to IDResponses with locks and a nil check +func (w *WebsocketConnection) AddResponseWithID(id int64, data []byte) { + w.Lock() + defer w.Unlock() + if w.IDResponses == nil { + w.IDResponses = make(map[int64][]byte) + } + w.IDResponses[id] = data +} + +// Dial sets proxy urls and then connects to the websocket +func (w *WebsocketConnection) Dial(dialer *websocket.Dialer, headers http.Header) error { + if w.ProxyURL != "" { + proxy, err := url.Parse(w.ProxyURL) + if err != nil { + return err + } + dialer.Proxy = http.ProxyURL(proxy) + } + + var err error + var conStatus *http.Response + w.Connection, conStatus, err = dialer.Dial(w.URL, headers) + if err != nil { + if conStatus != nil { + return fmt.Errorf("%v %v %v Error: %v", w.URL, conStatus, conStatus.StatusCode, err) + } + return fmt.Errorf("%v Error: %v", w.URL, err) + } + return nil +} + +// SendMessage the one true message request. Sends message to WS +func (w *WebsocketConnection) SendMessage(data interface{}) error { + w.Lock() + defer w.Unlock() + json, err := common.JSONEncode(data) + if err != nil { + return err + } + if w.Verbose { + log.Debugf("%v sending message to websocket %v", w.ExchangeName, string(json)) + } + if w.RateLimit > 0 { + time.Sleep(time.Duration(w.RateLimit) * time.Millisecond) + } + return w.Connection.WriteMessage(websocket.TextMessage, json) +} + +// SendMessageReturnResponse will send a WS message to the connection +// It will then run a goroutine to await a JSON response +// If there is no response it will return an error +func (w *WebsocketConnection) SendMessageReturnResponse(id int64, request interface{}) ([]byte, error) { + err := w.SendMessage(request) + if err != nil { + return nil, err + } + var wg sync.WaitGroup + wg.Add(1) + go w.WaitForResult(id, &wg) + defer func() { + delete(w.IDResponses, id) + }() + wg.Wait() + if _, ok := w.IDResponses[id]; !ok { + return nil, fmt.Errorf("timeout waiting for response with ID %v", id) + } + + return w.IDResponses[id], nil +} + +// WaitForResult will keep checking w.IDResponses for a response ID +// If the timer expires, it will return without +func (w *WebsocketConnection) WaitForResult(id int64, wg *sync.WaitGroup) { + defer wg.Done() + timer := time.NewTimer(w.ResponseMaxLimit) + for { + select { + case <-timer.C: + return + default: + w.Lock() + for k := range w.IDResponses { + if k == id { + w.Unlock() + return + } + } + w.Unlock() + time.Sleep(w.ResponseCheckTimeout) + } + } +} + +// ReadMessage reads messages, can handle text, gzip and binary +func (w *WebsocketConnection) ReadMessage() (WebsocketResponse, error) { + mType, resp, err := w.Connection.ReadMessage() + if err != nil { + return WebsocketResponse{}, err + } + var standardMessage []byte + switch mType { + case websocket.TextMessage: + standardMessage = resp + case websocket.BinaryMessage: + standardMessage, err = w.parseBinaryResponse(resp) + if err != nil { + return WebsocketResponse{}, err + } + } + if w.Verbose { + log.Debugf("%v Websocket message received: %v", + w.ExchangeName, + string(standardMessage)) + } + return WebsocketResponse{Raw: standardMessage, Type: mType}, nil +} + +// parseBinaryResponse parses a websocket binaray response into a usable byte array +func (w *WebsocketConnection) parseBinaryResponse(resp []byte) ([]byte, error) { + var standardMessage []byte + var err error + // Detect GZIP + if resp[0] == 31 && resp[1] == 139 { + b := bytes.NewReader(resp) + var gReader *gzip.Reader + gReader, err = gzip.NewReader(b) + if err != nil { + return standardMessage, err + } + standardMessage, err = ioutil.ReadAll(gReader) + if err != nil { + return standardMessage, err + } + err = gReader.Close() + if err != nil { + return standardMessage, err + } + } else { + reader := flate.NewReader(bytes.NewReader(resp)) + standardMessage, err = ioutil.ReadAll(reader) + if err != nil { + return standardMessage, err + } + err = reader.Close() + if err != nil { + return standardMessage, err + } + } + return standardMessage, nil +} + +// GenerateMessageID Creates a messageID to checkout +func (w *WebsocketConnection) GenerateMessageID(useNano bool) int64 { + if useNano { + return time.Now().UnixNano() + } + return time.Now().Unix() +} diff --git a/exchanges/wshandler/websocket_connection_test.go b/exchanges/wshandler/websocket_connection_test.go new file mode 100644 index 00000000..13c61bf2 --- /dev/null +++ b/exchanges/wshandler/websocket_connection_test.go @@ -0,0 +1,203 @@ +package wshandler + +import ( + "bytes" + "compress/flate" + "compress/gzip" + "errors" + "net/http" + "os" + "strings" + "testing" + "time" + + "github.com/gorilla/websocket" + "github.com/thrasher-/gocryptotrader/common" + "github.com/thrasher-/gocryptotrader/currency" +) + +const ( + websocketTestURL = "wss://www.bitmex.com/realtime" + returnResponseURL = "wss://ws.kraken.com" + useProxyTests = false // Disabled by default. Freely available proxy servers that work all the time are difficult to find + proxyURL = "http://212.186.171.4:80" // Replace with a usable proxy server +) + +var wc *WebsocketConnection +var dialer websocket.Dialer + +type testStruct struct { + Error error + WC WebsocketConnection +} + +type testRequest struct { + Event string `json:"event"` + RequestID int64 `json:"reqid,omitempty"` + Pairs []string `json:"pair"` + Subscription testRequestData `json:"subscription,omitempty"` +} + +// testRequestData contains details on WS channel +type testRequestData struct { + Name string `json:"name,omitempty"` + Interval int64 `json:"interval,omitempty"` + Depth int64 `json:"depth,omitempty"` +} + +type testResponse struct { + RequestID int64 `json:"reqid,omitempty"` +} + +// TestMain setup test +func TestMain(m *testing.M) { + wc = &WebsocketConnection{ + ExchangeName: "test", + Verbose: true, + URL: returnResponseURL, + ResponseMaxLimit: 7000000000, + ResponseCheckTimeout: 30000000, + } + os.Exit(m.Run()) +} + +// TestDial logic test +func TestDial(t *testing.T) { + var testCases = []testStruct{ + {Error: nil, WC: WebsocketConnection{ExchangeName: "test1", Verbose: true, URL: websocketTestURL, RateLimit: 10, ResponseCheckTimeout: 30000000, ResponseMaxLimit: 7000000000}}, + {Error: errors.New(" Error: malformed ws or wss URL"), WC: WebsocketConnection{ExchangeName: "test2", Verbose: true, URL: "", ResponseCheckTimeout: 30000000, ResponseMaxLimit: 7000000000}}, + {Error: nil, WC: WebsocketConnection{ExchangeName: "test3", Verbose: true, URL: websocketTestURL, ProxyURL: proxyURL, ResponseCheckTimeout: 30000000, ResponseMaxLimit: 7000000000}}, + } + for i := 0; i < len(testCases); i++ { + testData := &testCases[i] + t.Run(testData.WC.ExchangeName, func(t *testing.T) { + if testData.WC.ProxyURL != "" && !useProxyTests { + t.Skip("Proxy testing not enabled, skipping") + } + err := testData.WC.Dial(&dialer, http.Header{}) + if err != nil { + if testData.Error != nil && err.Error() == testData.Error.Error() { + return + } + t.Fatal(err) + } + }) + } +} + +// TestSendMessage logic test +func TestSendMessage(t *testing.T) { + var testCases = []testStruct{ + {Error: nil, WC: WebsocketConnection{ExchangeName: "test1", Verbose: true, URL: websocketTestURL, RateLimit: 10, ResponseCheckTimeout: 30000000, ResponseMaxLimit: 7000000000}}, + {Error: errors.New(" Error: malformed ws or wss URL"), WC: WebsocketConnection{ExchangeName: "test2", Verbose: true, URL: "", ResponseCheckTimeout: 30000000, ResponseMaxLimit: 7000000000}}, + {Error: nil, WC: WebsocketConnection{ExchangeName: "test3", Verbose: true, URL: websocketTestURL, ProxyURL: proxyURL, ResponseCheckTimeout: 30000000, ResponseMaxLimit: 7000000000}}, + } + for i := 0; i < len(testCases); i++ { + testData := &testCases[i] + t.Run(testData.WC.ExchangeName, func(t *testing.T) { + if testData.WC.ProxyURL != "" && !useProxyTests { + t.Skip("Proxy testing not enabled, skipping") + } + err := testData.WC.Dial(&dialer, http.Header{}) + if err != nil { + if testData.Error != nil && err.Error() == testData.Error.Error() { + return + } + t.Fatal(err) + } + err = testData.WC.SendMessage("ping") + if err != nil { + t.Error(err) + } + }) + } +} + +// TestSendMessageWithResponse logic test +func TestSendMessageWithResponse(t *testing.T) { + if wc.ProxyURL != "" && !useProxyTests { + t.Skip("Proxy testing not enabled, skipping") + } + err := wc.Dial(&dialer, http.Header{}) + if err != nil { + t.Fatal(err) + } + go readMesages(wc, t) + + request := testRequest{ + Event: "subscribe", + Pairs: []string{currency.NewPairWithDelimiter("XBT", "USD", "/").String()}, + Subscription: testRequestData{ + Name: "ticker", + }, + RequestID: wc.GenerateMessageID(true), + } + _, err = wc.SendMessageReturnResponse(request.RequestID, request) + if err != nil { + t.Error(err) + } +} + +// TestParseBinaryResponse logic test +func TestParseBinaryResponse(t *testing.T) { + var b bytes.Buffer + w := gzip.NewWriter(&b) + w.Write([]byte("hello")) + w.Close() + resp, err := wc.parseBinaryResponse(b.Bytes()) + if err != nil { + t.Error(err) + } + if !strings.EqualFold(string(resp), "hello") { + t.Errorf("GZip conversion failed. Received: '%v', Expected: 'hello'", string(resp)) + } + + var b2 bytes.Buffer + w2, err2 := flate.NewWriter(&b2, 1) + if err2 != nil { + t.Error(err2) + } + w2.Write([]byte("hello")) + w2.Close() + resp2, err3 := wc.parseBinaryResponse(b2.Bytes()) + if err3 != nil { + t.Error(err3) + } + if !strings.EqualFold(string(resp2), "hello") { + t.Errorf("GZip conversion failed. Received: '%v', Expected: 'hello'", string(resp2)) + } +} + +// TestAddResponseWithID logic test +func TestAddResponseWithID(t *testing.T) { + wc.IDResponses = nil + wc.AddResponseWithID(0, []byte("hi")) + wc.AddResponseWithID(1, []byte("hi")) +} + +// readMesages helper func +func readMesages(wc *WebsocketConnection, t *testing.T) { + timer := time.NewTimer(20 * time.Second) + for { + select { + case <-timer.C: + return + default: + resp, err := wc.ReadMessage() + if err != nil { + t.Error(err) + return + } + var incoming testResponse + err = common.JSONDecode(resp.Raw, &incoming) + if err != nil { + t.Error(err) + return + } + if incoming.RequestID > 0 { + wc.AddResponseWithID(incoming.RequestID, resp.Raw) + return + } + } + } +} diff --git a/exchanges/wshandler/websocket_connection_types.go b/exchanges/wshandler/websocket_connection_types.go new file mode 100644 index 00000000..214986a6 --- /dev/null +++ b/exchanges/wshandler/websocket_connection_types.go @@ -0,0 +1,25 @@ +package wshandler + +import ( + "sync" + "time" + + "github.com/gorilla/websocket" +) + +// WebsocketConnection contains all the data needed to send a message to a WS +type WebsocketConnection struct { + sync.Mutex + Verbose bool + RateLimit float64 + ExchangeName string + URL string + ProxyURL string + Wg sync.WaitGroup + Connection *websocket.Conn + Shutdown chan struct{} + // These are the request IDs and the corresponding response JSON + IDResponses map[int64][]byte + ResponseCheckTimeout time.Duration + ResponseMaxLimit time.Duration +} diff --git a/exchanges/exchange_websocket_test.go b/exchanges/wshandler/websocket_test.go similarity index 90% rename from exchanges/exchange_websocket_test.go rename to exchanges/wshandler/websocket_test.go index e61e28e7..1fdf9a8c 100644 --- a/exchanges/exchange_websocket_test.go +++ b/exchanges/wshandler/websocket_test.go @@ -1,4 +1,4 @@ -package exchange +package wshandler import ( "fmt" @@ -10,52 +10,48 @@ import ( "github.com/thrasher-/gocryptotrader/exchanges/orderbook" ) -var wsTest Base +var ws *Websocket func TestWebsocketInit(t *testing.T) { - if wsTest.Websocket != nil { - t.Error("test failed - WebsocketInit() error") - } - - wsTest.WebsocketInit() - - if wsTest.Websocket == nil { - t.Error("test failed - WebsocketInit() error") + ws = New() + if ws == nil { + t.Error("test failed - Websocket New() error") } } func TestWebsocket(t *testing.T) { - if err := wsTest.Websocket.SetProxyAddress("testProxy"); err != nil { + if err := ws.SetProxyAddress("testProxy"); err != nil { t.Error("test failed - SetProxyAddress", err) } - wsTest.WebsocketSetup(func() error { return nil }, + ws.Setup(func() error { return nil }, func(test WebsocketChannelSubscription) error { return nil }, func(test WebsocketChannelSubscription) error { return nil }, "testName", true, false, "testDefaultURL", - "testRunningURL") + "testRunningURL", + false) // Test variable setting and retreival - if wsTest.Websocket.GetName() != "testName" { + if ws.GetName() != "testName" { t.Error("test failed - WebsocketSetup") } - if !wsTest.Websocket.IsEnabled() { + if !ws.IsEnabled() { t.Error("test failed - WebsocketSetup") } - if wsTest.Websocket.GetProxyAddress() != "testProxy" { + if ws.GetProxyAddress() != "testProxy" { t.Error("test failed - WebsocketSetup") } - if wsTest.Websocket.GetDefaultURL() != "testDefaultURL" { + if ws.GetDefaultURL() != "testDefaultURL" { t.Error("test failed - WebsocketSetup") } - if wsTest.Websocket.GetWebsocketURL() != "testRunningURL" { + if ws.GetWebsocketURL() != "testRunningURL" { t.Error("test failed - WebsocketSetup") } @@ -69,53 +65,53 @@ func TestWebsocket(t *testing.T) { return } select { - case <-wsTest.Websocket.Connected: + case <-ws.Connected: count++ - case <-wsTest.Websocket.Disconnected: + case <-ws.Disconnected: count++ } } }() // -- Not connected shutdown - err := wsTest.Websocket.Shutdown() + err := ws.Shutdown() if err == nil { t.Fatal("test failed - should not be connected to able to shut down") } - wsTest.Websocket.Wg.Wait() + ws.Wg.Wait() // -- Normal connect - err = wsTest.Websocket.Connect() + err = ws.Connect() if err != nil { t.Fatal("test failed - WebsocketSetup", err) } // -- Already connected connect - err = wsTest.Websocket.Connect() + err = ws.Connect() if err == nil { t.Fatal("test failed - should not connect, already connected") } - wsTest.Websocket.SetWebsocketURL("") + ws.SetWebsocketURL("") // -- Set true when already true - err = wsTest.Websocket.SetWsStatusAndConnection(true) + err = ws.SetWsStatusAndConnection(true) if err == nil { t.Fatal("test failed - setting enabled should not work") } // -- Set false normal - err = wsTest.Websocket.SetWsStatusAndConnection(false) + err = ws.SetWsStatusAndConnection(false) if err != nil { t.Fatal("test failed - setting enabled should not work") } // -- Set true normal - err = wsTest.Websocket.SetWsStatusAndConnection(true) + err = ws.SetWsStatusAndConnection(true) if err != nil { t.Fatal("test failed - setting enabled should not work") } // -- Normal shutdown - err = wsTest.Websocket.Shutdown() + err = ws.Shutdown() if err != nil { t.Fatal("test failed - WebsocketSetup", err) } @@ -163,7 +159,7 @@ func TestInsertingSnapShots(t *testing.T) { snapShot1.AssetType = "SPOT" snapShot1.Pair = currency.NewPairFromString("BTCUSD") - wsTest.Websocket.Orderbook.LoadSnapshot(&snapShot1, "ExchangeTest", false) + ws.Orderbook.LoadSnapshot(&snapShot1, "ExchangeTest", false) var snapShot2 orderbook.Base asks = []orderbook.Item{ @@ -199,7 +195,7 @@ func TestInsertingSnapShots(t *testing.T) { snapShot2.AssetType = "SPOT" snapShot2.Pair = currency.NewPairFromString("LTCUSD") - wsTest.Websocket.Orderbook.LoadSnapshot(&snapShot2, "ExchangeTest", false) + ws.Orderbook.LoadSnapshot(&snapShot2, "ExchangeTest", false) var snapShot3 orderbook.Base asks = []orderbook.Item{ @@ -235,9 +231,9 @@ func TestInsertingSnapShots(t *testing.T) { snapShot3.AssetType = "FUTURES" snapShot3.Pair = currency.NewPairFromString("LTCUSD") - wsTest.Websocket.Orderbook.LoadSnapshot(&snapShot3, "ExchangeTest", false) + ws.Orderbook.LoadSnapshot(&snapShot3, "ExchangeTest", false) - if len(wsTest.Websocket.Orderbook.ob) != 3 { + if len(ws.Orderbook.ob) != 3 { t.Error("test failed - inserting orderbook data") } } @@ -259,7 +255,7 @@ func TestUpdate(t *testing.T) { {Price: 1337, Amount: 100}, // Append {Price: 1336, Amount: 0}, // Ghost delete } - err := wsTest.Websocket.Orderbook.Update(bidTargets, + err := ws.Orderbook.Update(bidTargets, askTargets, LTCUSDPAIR, time.Now(), @@ -270,7 +266,7 @@ func TestUpdate(t *testing.T) { t.Error("test failed - OrderbookUpdate error", err) } - err = wsTest.Websocket.Orderbook.Update(bidTargets, + err = ws.Orderbook.Update(bidTargets, askTargets, LTCUSDPAIR, time.Now(), @@ -295,7 +291,7 @@ func TestUpdate(t *testing.T) { {Price: 1336, Amount: 0}, // Ghost delete } - err = wsTest.Websocket.Orderbook.Update(bidTargets, + err = ws.Orderbook.Update(bidTargets, askTargets, BTCUSDPAIR, time.Now(), diff --git a/exchanges/exchange_websocket_types.go b/exchanges/wshandler/websocket_types.go similarity index 89% rename from exchanges/exchange_websocket_types.go rename to exchanges/wshandler/websocket_types.go index c35265fe..5348a630 100644 --- a/exchanges/exchange_websocket_types.go +++ b/exchanges/wshandler/websocket_types.go @@ -1,4 +1,4 @@ -package exchange +package wshandler import ( "sync" @@ -24,6 +24,9 @@ const ( WebsocketSubmitOrderSupported WebsocketCancelOrderSupported WebsocketWithdrawSupported + WebsocketMessageCorrelationSupported + WebsocketSequenceNumberSupported + WebsocketDeadMansSwitchSupported WebsocketTickerSupportedText = "TICKER STREAMING SUPPORTED" WebsocketOrderbookSupportedText = "ORDERBOOK STREAMING SUPPORTED" @@ -40,6 +43,9 @@ const ( WebsocketSubmitOrderSupportedText = "WEBSOCKET SUBMIT ORDER SUPPORTED" WebsocketCancelOrderSupportedText = "WEBSOCKET CANCEL ORDER SUPPORTED" WebsocketWithdrawSupportedText = "WEBSOCKET WITHDRAW SUPPORTED" + WebsocketMessageCorrelationSupportedText = "WEBSOCKET MESSAGE CORRELATION SUPPORTED" + WebsocketSequenceNumberSupportedText = "WEBSOCKET SEQUENCE NUMBER SUPPORTED" + WebsocketDeadMansSwitchSupportedText = "WEBSOCKET DEAD MANS SWITCH SUPPORTED" // WebsocketNotEnabled alerts of a disabled websocket WebsocketNotEnabled = "exchange_websocket_not_enabled" @@ -75,11 +81,10 @@ type Websocket struct { noConnectionChecks int reconnectionChecks int noConnectionCheckLimit int - // Subscriptions stuff - subscribedChannels []WebsocketChannelSubscription - channelsToSubscribe []WebsocketChannelSubscription - channelSubscriber func(channelToSubscribe WebsocketChannelSubscription) error - channelUnsubscriber func(channelToUnsubscribe WebsocketChannelSubscription) error + subscribedChannels []WebsocketChannelSubscription + channelsToSubscribe []WebsocketChannelSubscription + channelSubscriber func(channelToSubscribe WebsocketChannelSubscription) error + channelUnsubscriber func(channelToUnsubscribe WebsocketChannelSubscription) error // Connected denotes a channel switch for diversion of request flow Connected chan struct{} // Disconnected denotes a channel switch for diversion of request flow @@ -87,11 +92,9 @@ type Websocket struct { // DataHandler pipes websocket data to an exchange websocket data handler DataHandler chan interface{} // ShutdownC is the main shutdown channel which controls all websocket go funcs - ShutdownC chan struct{} - ShutdownConnectionMonitor chan struct{} + ShutdownC chan struct{} // Orderbook is a local cache of orderbooks Orderbook WebsocketOrderbookLocal - // Wg defines a wait group for websocket routines for cleanly shutting down // routines Wg sync.WaitGroup diff --git a/exchanges/yobit/yobit.go b/exchanges/yobit/yobit.go index c25a739b..04497f0e 100644 --- a/exchanges/yobit/yobit.go +++ b/exchanges/yobit/yobit.go @@ -15,6 +15,7 @@ import ( 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" ) @@ -74,7 +75,7 @@ func (y *Yobit) SetDefaults() { y.APIUrl = y.APIUrlDefault y.APIUrlSecondaryDefault = apiPrivateURL y.APIUrlSecondary = y.APIUrlSecondaryDefault - y.WebsocketInit() + y.Websocket = wshandler.New() } // Setup sets exchange configuration parameters for Yobit diff --git a/exchanges/yobit/yobit_wrapper.go b/exchanges/yobit/yobit_wrapper.go index 88cd1d86..f1663a6a 100644 --- a/exchanges/yobit/yobit_wrapper.go +++ b/exchanges/yobit/yobit_wrapper.go @@ -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" ) @@ -275,7 +276,7 @@ func (y *Yobit) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.W } // GetWebsocket returns a pointer to the exchange websocket -func (y *Yobit) GetWebsocket() (*exchange.Websocket, error) { +func (y *Yobit) GetWebsocket() (*wshandler.Websocket, error) { return nil, common.ErrFunctionNotSupported } @@ -367,18 +368,18 @@ func (y *Yobit) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([] // SubscribeToWebsocketChannels appends to ChannelsToSubscribe // which lets websocket.manageSubscriptions handle subscribing -func (y *Yobit) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (y *Yobit) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { return common.ErrFunctionNotSupported } // UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe // which lets websocket.manageSubscriptions handle unsubscribing -func (y *Yobit) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (y *Yobit) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { return common.ErrFunctionNotSupported } // GetSubscriptions returns a copied list of subscriptions -func (y *Yobit) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) { +func (y *Yobit) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { return nil, common.ErrFunctionNotSupported } diff --git a/exchanges/zb/zb.go b/exchanges/zb/zb.go index aab72d2c..faacc33f 100644 --- a/exchanges/zb/zb.go +++ b/exchanges/zb/zb.go @@ -8,17 +8,15 @@ import ( "net/url" "strconv" "strings" - "sync" "time" - "github.com/thrasher-/gocryptotrader/currency" - - "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" ) @@ -48,9 +46,8 @@ const ( // 47.91.169.147 api.zb.com // 47.52.55.212 trade.zb.com type ZB struct { - WebsocketConn *websocket.Conn + WebsocketConn *wshandler.WebsocketConnection exchange.Base - wsRequestMtx sync.Mutex } // SetDefaults sets default values for the exchange @@ -77,15 +74,18 @@ func (z *ZB) SetDefaults() { z.APIUrl = z.APIUrlDefault z.APIUrlSecondaryDefault = zbMarketURL z.APIUrlSecondary = z.APIUrlSecondaryDefault - z.WebsocketInit() - z.Websocket.Functionality = exchange.WebsocketTickerSupported | - exchange.WebsocketOrderbookSupported | - exchange.WebsocketTradeDataSupported | - exchange.WebsocketSubscribeSupported | - exchange.WebsocketAuthenticatedEndpointsSupported | - exchange.WebsocketAccountDataSupported | - exchange.WebsocketCancelOrderSupported | - exchange.WebsocketSubmitOrderSupported + z.Websocket = wshandler.New() + z.Websocket.Functionality = wshandler.WebsocketTickerSupported | + wshandler.WebsocketOrderbookSupported | + wshandler.WebsocketTradeDataSupported | + wshandler.WebsocketSubscribeSupported | + wshandler.WebsocketAuthenticatedEndpointsSupported | + wshandler.WebsocketAccountDataSupported | + wshandler.WebsocketCancelOrderSupported | + wshandler.WebsocketSubmitOrderSupported | + wshandler.WebsocketMessageCorrelationSupported + z.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + z.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout } // Setup sets user configuration @@ -127,17 +127,27 @@ func (z *ZB) Setup(exch *config.ExchangeConfig) { if err != nil { log.Fatal(err) } - err = z.WebsocketSetup(z.WsConnect, + err = z.Websocket.Setup(z.WsConnect, z.Subscribe, nil, exch.Name, exch.Websocket, exch.Verbose, zbWebsocketAPI, - exch.WebsocketURL) + exch.WebsocketURL, + exch.AuthenticatedWebsocketAPISupport) if err != nil { log.Fatal(err) } + z.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: z.Name, + URL: z.Websocket.GetWebsocketURL(), + ProxyURL: z.Websocket.GetProxyAddress(), + Verbose: z.Verbose, + RateLimit: zbWebsocketRateLimit, + ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + } } } diff --git a/exchanges/zb/zb_test.go b/exchanges/zb/zb_test.go index 6bbf5124..ee1c1781 100644 --- a/exchanges/zb/zb_test.go +++ b/exchanges/zb/zb_test.go @@ -4,14 +4,13 @@ import ( "fmt" "net/http" "testing" - "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/sharedtestvalues" + "github.com/thrasher-/gocryptotrader/exchanges/wshandler" ) // Please supply you own test keys here for due diligence testing. @@ -50,12 +49,17 @@ func setupWsAuth(t *testing.T) { z.SetDefaults() TestSetup(t) if !z.Websocket.IsEnabled() && !z.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() || !canManipulateRealOrders { - t.Skip(exchange.WebsocketNotEnabled) + t.Skip(wshandler.WebsocketNotEnabled) + } + z.WebsocketConn = &wshandler.WebsocketConnection{ + ExchangeName: z.Name, + URL: zbWebsocketAPI, + Verbose: z.Verbose, + ResponseMaxLimit: exchange.DefaultWebsocketResponseMaxLimit, + ResponseCheckTimeout: exchange.DefaultWebsocketResponseCheckTimeout, } - var err error var dialer websocket.Dialer - z.WebsocketConn, _, err = dialer.Dial(z.Websocket.GetWebsocketURL(), - http.Header{}) + err := z.WebsocketConn.Dial(&dialer, http.Header{}) if err != nil { t.Fatal(err) } @@ -503,224 +507,116 @@ func TestZBInvalidJSON(t *testing.T) { var response WsGetSubUserListResponse err := common.JSONDecode(fixedJSON, &response) if err != nil { - t.Log(err) + t.Fatal(err) } if response.Message[0].UserID != 1337 { - t.Error("Expected extracted JSON USERID to equal 1337") + t.Fatal("Expected extracted JSON USERID to equal 1337") } - json = `{"success":true,"code":1000,"channel":"createSubUserKey","message":"{"apiKey":"thisisnotareallykeyyousillybilly","apiSecret":"lol"}","no":"14728151154382111746154"}` + json = `{"success":true,"code":1000,"channel":"createSubUserKey","message":"{"apiKey":"thisisnotareallykeyyousillybilly","apiSecret":"lol"}","no":"123"}` fixedJSON = z.wsFixInvalidJSON([]byte(json)) var response2 WsRequestResponse err = common.JSONDecode(fixedJSON, &response2) if err != nil { - t.Log(err) + t.Error(err) } } // TestWsTransferFunds ws test func TestWsTransferFunds(t *testing.T) { setupWsAuth(t) - err := z.wsDoTransferFunds(currency.BTC, + _, err := z.wsDoTransferFunds(currency.BTC, 0.0001, "username1", "username2", ) if err != nil { - t.Error(err) + t.Fatal(err) } - timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout) - select { - case resp := <-z.Websocket.DataHandler: - if resp.(WsRequestResponse).Code == 1002 || resp.(WsRequestResponse).Code == 1003 { - t.Error("Hash not calculated correctly") - } - case <-timer.C: - t.Error("Have not received a response") - } - timer.Stop() } // TestWsCreateSuUserKey ws test func TestWsCreateSuUserKey(t *testing.T) { setupWsAuth(t) - z.wsGetSubUserList() - timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout) - var userID int64 - select { - case resp := <-z.Websocket.DataHandler: - if len(resp.(WsGetSubUserListResponse).Message) == 0 { - t.Fatal("Expected a userID. Ensure you have made a subuserID before running this test") - } - userID = resp.(WsGetSubUserListResponse).Message[0].UserID - case <-timer.C: - t.Fatal("Have not received a response") - } - timer.Stop() - err := z.wsCreateSubUserKey(true, true, true, true, "subu", fmt.Sprintf("%v", userID)) + subUsers, err := z.wsGetSubUserList() if err != nil { - t.Error(err) + t.Fatal(err) } - timer = time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout) - select { - case resp := <-z.Websocket.DataHandler: - if resp.(WsRequestResponse).Code == 1002 || resp.(WsRequestResponse).Code == 1003 { - t.Error("Hash not calculated correctly") - } - case <-timer.C: - t.Error("Have not received a response") + userID := subUsers.Message[0].UserID + _, err = z.wsCreateSubUserKey(true, true, true, true, "subu", fmt.Sprintf("%v", userID)) + if err != nil { + t.Fatal(err) } - timer.Stop() } // TestGetSubUserList ws test func TestGetSubUserList(t *testing.T) { setupWsAuth(t) - err := z.wsGetSubUserList() + _, err := z.wsGetSubUserList() if err != nil { t.Fatal(err) } - timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout) - select { - case resp := <-z.Websocket.DataHandler: - if resp.(WsGetSubUserListResponse).Code == 1002 || resp.(WsGetSubUserListResponse).Code == 1003 { - t.Error("Hash not calculated correctly") - } - case <-timer.C: - t.Error("Have not received a response") - } - timer.Stop() } // TestAddSubUser ws test func TestAddSubUser(t *testing.T) { setupWsAuth(t) - err := z.wsAddSubUser("abcde", "123456789101112aA!") + _, err := z.wsAddSubUser("1", "123456789101112aA!") if err != nil { t.Fatal(err) } - timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout) - select { - case resp := <-z.Websocket.DataHandler: - if resp.(WsRequestResponse).Code == 1002 || resp.(WsRequestResponse).Code == 1003 { - t.Error("Hash not calculated correctly") - } - case <-timer.C: - t.Error("Have not received a response") - } - timer.Stop() } // TestWsSubmitOrder ws test func TestWsSubmitOrder(t *testing.T) { setupWsAuth(t) - err := z.wsSubmitOrder(currency.NewPairWithDelimiter(currency.LTC.String(), currency.BTC.String(), "").Lower(), 1, 1, 1) + _, err := z.wsSubmitOrder(currency.NewPairWithDelimiter(currency.LTC.String(), currency.BTC.String(), "").Lower(), 1, 1, 1) if err != nil { t.Fatal(err) } - timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout) - select { - case resp := <-z.Websocket.DataHandler: - if resp.(WsSubmitOrderResponse).Code == 1002 || resp.(WsSubmitOrderResponse).Code == 1003 { - t.Error("Hash not calculated correctly") - } - case <-timer.C: - t.Error("Have not received a response") - } - timer.Stop() } // TestWsCancelOrder ws test func TestWsCancelOrder(t *testing.T) { setupWsAuth(t) - err := z.wsCancelOrder(currency.NewPairWithDelimiter(currency.LTC.String(), currency.BTC.String(), "").Lower(), 1234) + _, err := z.wsCancelOrder(currency.NewPairWithDelimiter(currency.LTC.String(), currency.BTC.String(), "").Lower(), 1234) if err != nil { t.Fatal(err) } - timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout) - select { - case resp := <-z.Websocket.DataHandler: - if resp.(WsCancelOrderResponse).Code == 1002 || resp.(WsCancelOrderResponse).Code == 1003 { - t.Error("Hash not calculated correctly") - } - case <-timer.C: - t.Error("Have not received a response") - } - timer.Stop() } // TestWsGetAccountInfo ws test func TestWsGetAccountInfo(t *testing.T) { setupWsAuth(t) - err := z.wsGetAccountInfoRequest() + _, err := z.wsGetAccountInfoRequest() if err != nil { t.Fatal(err) } - timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout) - select { - case resp := <-z.Websocket.DataHandler: - if resp.(WsGetAccountInfoResponse).Code == 1002 || resp.(WsGetAccountInfoResponse).Code == 1003 { - t.Error("Hash not calculated correctly") - } - case <-timer.C: - t.Error("Have not received a response") - } - timer.Stop() } // TestWsGetOrder ws test func TestWsGetOrder(t *testing.T) { setupWsAuth(t) - err := z.wsGetOrder(currency.NewPairWithDelimiter(currency.LTC.String(), currency.BTC.String(), "").Lower(), 1234) + _, err := z.wsGetOrder(currency.NewPairWithDelimiter(currency.LTC.String(), currency.BTC.String(), "").Lower(), 1234) if err != nil { t.Fatal(err) } - timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout) - select { - case resp := <-z.Websocket.DataHandler: - if resp.(WsGetOrderResponse).Code == 1002 || resp.(WsGetOrderResponse).Code == 1003 { - t.Error("Hash not calculated correctly") - } - case <-timer.C: - t.Error("Have not received a response") - } - timer.Stop() } // TestWsGetOrders ws test func TestWsGetOrders(t *testing.T) { setupWsAuth(t) - err := z.wsGetOrders(currency.NewPairWithDelimiter(currency.LTC.String(), currency.BTC.String(), "").Lower(), 1, 1) + _, err := z.wsGetOrders(currency.NewPairWithDelimiter(currency.LTC.String(), currency.BTC.String(), "").Lower(), 1, 1) if err != nil { t.Fatal(err) } - timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout) - select { - case resp := <-z.Websocket.DataHandler: - if resp.(WsGetOrdersResponse).Code == 1002 || resp.(WsGetOrdersResponse).Code == 1003 { - t.Error("Hash not calculated correctly") - } - case <-timer.C: - t.Error("Have not received a response") - } - timer.Stop() } // TestWsGetOrdersIgnoreTradeType ws test func TestWsGetOrdersIgnoreTradeType(t *testing.T) { setupWsAuth(t) - err := z.wsGetOrdersIgnoreTradeType(currency.NewPairWithDelimiter(currency.LTC.String(), currency.BTC.String(), "").Lower(), 1, 1) + _, err := z.wsGetOrdersIgnoreTradeType(currency.NewPairWithDelimiter(currency.LTC.String(), currency.BTC.String(), "").Lower(), 1, 1) if err != nil { t.Fatal(err) } - timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout) - select { - case resp := <-z.Websocket.DataHandler: - if resp.(WsGetOrdersResponse).Code == 1002 || resp.(WsGetOrdersResponse).Code == 1003 { - t.Error("Hash not calculated correctly") - } - case <-timer.C: - t.Error("Have not received a response") - } - timer.Stop() } diff --git a/exchanges/zb/zb_websocket.go b/exchanges/zb/zb_websocket.go index 1b420089..6e9d050b 100644 --- a/exchanges/zb/zb_websocket.go +++ b/exchanges/zb/zb_websocket.go @@ -4,9 +4,7 @@ import ( "errors" "fmt" "net/http" - "net/url" "regexp" - "strings" "time" "github.com/gorilla/websocket" @@ -14,33 +12,23 @@ 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" ) const ( zbWebsocketAPI = "wss://api.zb.cn:9999/websocket" zWebsocketAddChannel = "addChannel" + zbWebsocketRateLimit = 20 ) // WsConnect initiates a websocket connection func (z *ZB) WsConnect() error { if !z.Websocket.IsEnabled() || !z.IsEnabled() { - return errors.New(exchange.WebsocketNotEnabled) + return errors.New(wshandler.WebsocketNotEnabled) } - var dialer websocket.Dialer - if z.Websocket.GetProxyAddress() != "" { - proxy, err := url.Parse(z.Websocket.GetProxyAddress()) - if err != nil { - return err - } - - dialer.Proxy = http.ProxyURL(proxy) - } - - var err error - z.WebsocketConn, _, err = dialer.Dial(z.Websocket.GetWebsocketURL(), - http.Header{}) + err := z.WebsocketConn.Dial(&dialer, http.Header{}) if err != nil { return err } @@ -51,18 +39,6 @@ func (z *ZB) WsConnect() error { return nil } -// WsReadData reads from the websocket connection and returns the websocket -// response -func (z *ZB) WsReadData() (exchange.WebsocketResponse, error) { - _, resp, err := z.WebsocketConn.ReadMessage() - if err != nil { - return exchange.WebsocketResponse{}, err - } - - z.Websocket.TrafficAlert <- struct{}{} - return exchange.WebsocketResponse{Raw: resp}, nil -} - // WsHandleData handles all the websocket data coming from the websocket // connection func (z *ZB) WsHandleData() { @@ -77,12 +53,12 @@ func (z *ZB) WsHandleData() { case <-z.Websocket.ShutdownC: return default: - resp, err := z.WsReadData() + resp, err := z.WebsocketConn.ReadMessage() if err != nil { z.Websocket.DataHandler <- err - time.Sleep(time.Second) - continue + return } + z.Websocket.TrafficAlert <- struct{}{} fixedJSON := z.wsFixInvalidJSON(resp.Raw) var result Generic err = common.JSONDecode(fixedJSON, &result) @@ -90,13 +66,16 @@ func (z *ZB) WsHandleData() { z.Websocket.DataHandler <- err continue } + if result.No > 0 { + z.WebsocketConn.AddResponseWithID(result.No, fixedJSON) + continue + } + if result.Code > 0 && result.Code != 1000 { + z.Websocket.DataHandler <- fmt.Errorf("%v request failed, message: %v, error code: %v", z.Name, result.Message, wsErrCodes[result.Code]) + continue + } switch { case common.StringContains(result.Channel, "markets"): - if !result.Success { - z.Websocket.DataHandler <- fmt.Errorf("zb_websocket.go error - unsuccessful market response %s", wsErrCodes[result.Code]) - continue - } - var markets Markets err := common.JSONDecode(result.Data, &markets) if err != nil { @@ -106,16 +85,14 @@ func (z *ZB) WsHandleData() { case common.StringContains(result.Channel, "ticker"): cPair := common.SplitStrings(result.Channel, "_") - var ticker WsTicker - err := common.JSONDecode(fixedJSON, &ticker) if err != nil { z.Websocket.DataHandler <- err continue } - z.Websocket.DataHandler <- exchange.TickerData{ + z.Websocket.DataHandler <- wshandler.TickerData{ Timestamp: time.Unix(0, ticker.Date), Pair: currency.NewPairFromString(cPair[0]), AssetType: "SPOT", @@ -153,7 +130,6 @@ func (z *ZB) WsHandleData() { channelInfo := common.SplitStrings(result.Channel, "_") cPair := currency.NewPairFromString(channelInfo[0]) - var newOrderBook orderbook.Base newOrderBook.Asks = asks newOrderBook.Bids = bids @@ -168,7 +144,7 @@ func (z *ZB) WsHandleData() { continue } - z.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{ + z.Websocket.DataHandler <- wshandler.WebsocketOrderbookUpdate{ Pair: cPair, Asset: "SPOT", Exchange: z.GetName(), @@ -181,17 +157,14 @@ func (z *ZB) WsHandleData() { z.Websocket.DataHandler <- err continue } - // Most up to date trade if len(trades.Data) == 0 { continue } t := trades.Data[len(trades.Data)-1] - channelInfo := common.SplitStrings(result.Channel, "_") cPair := currency.NewPairFromString(channelInfo[0]) - - z.Websocket.DataHandler <- exchange.TradeData{ + z.Websocket.DataHandler <- wshandler.TradeData{ Timestamp: time.Unix(0, t.Date), CurrencyPair: cPair, AssetType: "SPOT", @@ -201,86 +174,6 @@ func (z *ZB) WsHandleData() { Amount: t.Amount, Side: t.TradeType, } - case strings.EqualFold(result.Channel, "addSubUser"): - var response WsRequestResponse - err := common.JSONDecode(fixedJSON, &response) - if err != nil { - z.Websocket.DataHandler <- err - continue - } - z.Websocket.DataHandler <- response - case strings.EqualFold(result.Channel, "getSubUserList"): - var response WsGetSubUserListResponse - err := common.JSONDecode(fixedJSON, &response) - if err != nil { - z.Websocket.DataHandler <- err - continue - } - z.Websocket.DataHandler <- response - case strings.EqualFold(result.Channel, "doTransferFunds"): - var response WsRequestResponse - err := common.JSONDecode(fixedJSON, &response) - if err != nil { - z.Websocket.DataHandler <- err - continue - } - z.Websocket.DataHandler <- response - case strings.EqualFold(result.Channel, "createSubUserKey"): - var response WsRequestResponse - err := common.JSONDecode(fixedJSON, &response) - if err != nil { - z.Websocket.DataHandler <- err - continue - } - z.Websocket.DataHandler <- response - case common.StringContains(result.Channel, "_order"): - var response WsSubmitOrderResponse - err := common.JSONDecode(fixedJSON, &response) - if err != nil { - z.Websocket.DataHandler <- err - continue - } - z.Websocket.DataHandler <- response - case common.StringContains(result.Channel, "_cancelorder"): - var response WsCancelOrderResponse - err := common.JSONDecode(fixedJSON, &response) - if err != nil { - z.Websocket.DataHandler <- err - continue - } - z.Websocket.DataHandler <- response - case common.StringContains(result.Channel, "_getorders"): - var response WsGetOrdersResponse - err := common.JSONDecode(fixedJSON, &response) - if err != nil { - z.Websocket.DataHandler <- err - continue - } - z.Websocket.DataHandler <- response - case common.StringContains(result.Channel, "_getorder"): - var response WsGetOrderResponse - err := common.JSONDecode(fixedJSON, &response) - if err != nil { - z.Websocket.DataHandler <- err - continue - } - z.Websocket.DataHandler <- response - case common.StringContains(result.Channel, "_getordersignoretradetype"): - var response WsGetOrdersIgnoreTradeTypeResponse - err := common.JSONDecode(fixedJSON, &response) - if err != nil { - z.Websocket.DataHandler <- err - continue - } - z.Websocket.DataHandler <- response - case strings.EqualFold(result.Channel, "getAccountInfo"): - var response WsGetAccountInfoResponse - err := common.JSONDecode(fixedJSON, &response) - if err != nil { - z.Websocket.DataHandler <- err - continue - } - z.Websocket.DataHandler <- response default: z.Websocket.DataHandler <- errors.New("zb_websocket.go error - unhandled websocket response") continue @@ -325,9 +218,9 @@ var wsErrCodes = map[int64]string{ // GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() func (z *ZB) GenerateDefaultSubscriptions() { - var subscriptions []exchange.WebsocketChannelSubscription + var subscriptions []wshandler.WebsocketChannelSubscription // Tickerdata is its own channel - subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ Channel: "markets", }) channels := []string{"%s_ticker", "%s_depth", "%s_trades"} @@ -335,7 +228,7 @@ func (z *ZB) GenerateDefaultSubscriptions() { for i := range channels { for j := range enabledCurrencies { enabledCurrencies[j].Delimiter = "" - subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{ + subscriptions = append(subscriptions, wshandler.WebsocketChannelSubscription{ Channel: fmt.Sprintf(channels[i], enabledCurrencies[j].Lower().String()), Currency: enabledCurrencies[j].Lower(), }) @@ -345,108 +238,24 @@ func (z *ZB) GenerateDefaultSubscriptions() { } // Subscribe sends a websocket message to receive data from the channel -func (z *ZB) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error { +func (z *ZB) Subscribe(channelToSubscribe wshandler.WebsocketChannelSubscription) error { subscriptionRequest := Subscription{ Event: zWebsocketAddChannel, Channel: channelToSubscribe.Channel, } - return z.wsSend(subscriptionRequest) -} - -// WsSend sends data to the websocket server -func (z *ZB) wsSend(data interface{}) error { - z.wsRequestMtx.Lock() - defer z.wsRequestMtx.Unlock() - json, err := common.JSONEncode(data) - if err != nil { - return err - } - if z.Verbose { - log.Debugf("%v sending message to websocket %v", z.Name, string(json)) - } - return z.WebsocketConn.WriteMessage(websocket.TextMessage, json) -} - -func (z *ZB) wsAddSubUser(username, password string) error { - if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { - return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name) - } - request := WsAddSubUserRequest{ - Memo: "Memo", - Password: password, - SubUserName: username, - } - request.Channel = "addSubUser" - request.Event = zWebsocketAddChannel - request.Accesskey = z.APIKey - request.Sign = z.wsGenerateSignature(request) - - return z.wsSend(request) -} - -func (z *ZB) wsGetSubUserList() error { - if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { - return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name) - } - request := WsAuthenticatedRequest{} - request.Channel = "getSubUserList" - request.Event = zWebsocketAddChannel - request.Accesskey = z.APIKey - request.Sign = z.wsGenerateSignature(request) - - return z.wsSend(request) -} - -func (z *ZB) wsDoTransferFunds(pair currency.Code, amount float64, fromUserName, toUserName string) error { - if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { - return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name) - } - request := WsDoTransferFundsRequest{ - Amount: amount, - Currency: pair, - FromUserName: fromUserName, - ToUserName: toUserName, - No: fmt.Sprintf("%v", time.Now().Unix()), - } - request.Channel = "doTransferFunds" - request.Event = zWebsocketAddChannel - request.Accesskey = z.APIKey - request.Sign = z.wsGenerateSignature(request) - - return z.wsSend(request) -} - -func (z *ZB) wsCreateSubUserKey(assetPerm, entrustPerm, leverPerm, moneyPerm bool, keyName, toUserID string) error { - if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { - return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name) - } - request := WsCreateSubUserKeyRequest{ - AssetPerm: assetPerm, - EntrustPerm: entrustPerm, - KeyName: keyName, - LeverPerm: leverPerm, - MoneyPerm: moneyPerm, - No: fmt.Sprintf("%v", time.Now().Unix()), - ToUserID: toUserID, - } - request.Channel = "createSubUserKey" - request.Event = zWebsocketAddChannel - request.Accesskey = z.APIKey - request.Sign = z.wsGenerateSignature(request) - - return z.wsSend(request) + return z.WebsocketConn.SendMessage(subscriptionRequest) } func (z *ZB) wsGenerateSignature(request interface{}) string { jsonResponse, err := common.JSONEncode(request) if err != nil { log.Error(err) + return "" } hmac := common.GetHMAC(common.HashMD5, jsonResponse, []byte(common.Sha1ToHex(z.APISecret))) return fmt.Sprintf("%x", hmac) - } func (z *ZB) wsFixInvalidJSON(json []byte) []byte { @@ -463,97 +272,296 @@ func (z *ZB) wsFixInvalidJSON(json []byte) []byte { return []byte(common.ReplaceString(string(json), string(matchingResults), fixedJSON, 1)) } -func (z *ZB) wsSubmitOrder(pair currency.Pair, amount, price float64, tradeType int64) error { +func (z *ZB) wsAddSubUser(username, password string) (*WsGetSubUserListResponse, error) { if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { - return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name) + return nil, fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name) + } + request := WsAddSubUserRequest{ + Memo: "memo", + Password: password, + SubUserName: username, + } + request.Channel = "addSubUser" + request.Event = zWebsocketAddChannel + request.Accesskey = z.APIKey + request.No = z.WebsocketConn.GenerateMessageID(true) + request.Sign = z.wsGenerateSignature(request) + resp, err := z.WebsocketConn.SendMessageReturnResponse(request.No, request) + if err != nil { + return nil, err + } + var genericResponse Generic + err = common.JSONDecode(resp, &genericResponse) + if err != nil { + return nil, err + } + if genericResponse.Code > 0 && genericResponse.Code != 1000 { + return nil, fmt.Errorf("%v request failed, message: %v, error code: %v", z.Name, genericResponse.Message, wsErrCodes[genericResponse.Code]) + } + var response WsGetSubUserListResponse + err = common.JSONDecode(resp, &response) + if err != nil { + return nil, err + } + return &response, nil +} + +func (z *ZB) wsGetSubUserList() (*WsGetSubUserListResponse, error) { + if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { + return nil, fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name) + } + request := WsAuthenticatedRequest{} + request.Channel = "getSubUserList" + request.Event = zWebsocketAddChannel + request.Accesskey = z.APIKey + request.No = z.WebsocketConn.GenerateMessageID(true) + request.Sign = z.wsGenerateSignature(request) + + resp, err := z.WebsocketConn.SendMessageReturnResponse(request.No, request) + if err != nil { + return nil, err + } + var response WsGetSubUserListResponse + err = common.JSONDecode(resp, &response) + if err != nil { + return nil, err + } + if response.Code > 0 && response.Code != 1000 { + return &response, fmt.Errorf("%v request failed, message: %v, error code: %v", z.Name, response.Message, wsErrCodes[response.Code]) + } + return &response, nil +} + +func (z *ZB) wsDoTransferFunds(pair currency.Code, amount float64, fromUserName, toUserName string) (*WsRequestResponse, error) { + if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { + return nil, fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name) + } + request := WsDoTransferFundsRequest{ + Amount: amount, + Currency: pair, + FromUserName: fromUserName, + ToUserName: toUserName, + No: z.WebsocketConn.GenerateMessageID(true), + } + request.Channel = "doTransferFunds" + request.Event = zWebsocketAddChannel + request.Accesskey = z.APIKey + request.Sign = z.wsGenerateSignature(request) + + resp, err := z.WebsocketConn.SendMessageReturnResponse(request.No, request) + if err != nil { + return nil, err + } + var response WsRequestResponse + err = common.JSONDecode(resp, &response) + if err != nil { + return nil, err + } + if response.Code > 0 && response.Code != 1000 { + return &response, fmt.Errorf("%v request failed, message: %v, error code: %v", z.Name, response.Message, wsErrCodes[response.Code]) + } + return &response, nil +} + +func (z *ZB) wsCreateSubUserKey(assetPerm, entrustPerm, leverPerm, moneyPerm bool, keyName, toUserID string) (*WsRequestResponse, error) { + if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { + return nil, fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name) + } + request := WsCreateSubUserKeyRequest{ + AssetPerm: assetPerm, + EntrustPerm: entrustPerm, + KeyName: keyName, + LeverPerm: leverPerm, + MoneyPerm: moneyPerm, + No: z.WebsocketConn.GenerateMessageID(true), + ToUserID: toUserID, + } + request.Channel = "createSubUserKey" + request.Event = zWebsocketAddChannel + request.Accesskey = z.APIKey + request.Sign = z.wsGenerateSignature(request) + + resp, err := z.WebsocketConn.SendMessageReturnResponse(request.No, request) + if err != nil { + return nil, err + } + var response WsRequestResponse + err = common.JSONDecode(resp, &response) + if err != nil { + return nil, err + } + if response.Code > 0 && response.Code != 1000 { + return &response, fmt.Errorf("%v request failed, message: %v, error code: %v", z.Name, response.Message, wsErrCodes[response.Code]) + } + return &response, nil +} + +func (z *ZB) wsSubmitOrder(pair currency.Pair, amount, price float64, tradeType int64) (*WsSubmitOrderResponse, error) { + if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { + return nil, fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name) } request := WsSubmitOrderRequest{ Amount: amount, Price: price, TradeType: tradeType, - No: fmt.Sprintf("%v", time.Now().Unix()), + No: z.WebsocketConn.GenerateMessageID(true), } request.Channel = fmt.Sprintf("%v_order", pair.String()) request.Event = zWebsocketAddChannel request.Accesskey = z.APIKey request.Sign = z.wsGenerateSignature(request) - return z.wsSend(request) + resp, err := z.WebsocketConn.SendMessageReturnResponse(request.No, request) + if err != nil { + return nil, err + } + var response WsSubmitOrderResponse + err = common.JSONDecode(resp, &response) + if err != nil { + return nil, err + } + if response.Code > 0 && response.Code != 1000 { + return &response, fmt.Errorf("%v request failed, message: %v, error code: %v", z.Name, response.Message, wsErrCodes[response.Code]) + } + return &response, nil } -func (z *ZB) wsCancelOrder(pair currency.Pair, orderID int64) error { +func (z *ZB) wsCancelOrder(pair currency.Pair, orderID int64) (*WsCancelOrderResponse, error) { if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { - return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name) + return nil, fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name) } request := WsCancelOrderRequest{ ID: orderID, + No: z.WebsocketConn.GenerateMessageID(true), } request.Channel = fmt.Sprintf("%v_cancelorder", pair.String()) request.Event = zWebsocketAddChannel request.Accesskey = z.APIKey request.Sign = z.wsGenerateSignature(request) - return z.wsSend(request) + resp, err := z.WebsocketConn.SendMessageReturnResponse(request.No, request) + if err != nil { + return nil, err + } + var response WsCancelOrderResponse + err = common.JSONDecode(resp, &response) + if err != nil { + return nil, err + } + if response.Code > 0 && response.Code != 1000 { + return &response, fmt.Errorf("%v request failed, message: %v, error code: %v", z.Name, response.Message, wsErrCodes[response.Code]) + } + return &response, nil } -func (z *ZB) wsGetOrder(pair currency.Pair, orderID int64) error { +func (z *ZB) wsGetOrder(pair currency.Pair, orderID int64) (*WsGetOrderResponse, error) { if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { - return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name) + return nil, fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name) } request := WsGetOrderRequest{ ID: orderID, + No: z.WebsocketConn.GenerateMessageID(true), } request.Channel = fmt.Sprintf("%v_getorder", pair.String()) request.Event = zWebsocketAddChannel request.Accesskey = z.APIKey request.Sign = z.wsGenerateSignature(request) - return z.wsSend(request) + resp, err := z.WebsocketConn.SendMessageReturnResponse(request.No, request) + if err != nil { + return nil, err + } + var response WsGetOrderResponse + err = common.JSONDecode(resp, &response) + if err != nil { + return nil, err + } + if response.Code > 0 && response.Code != 1000 { + return &response, fmt.Errorf("%v request failed, message: %v, error code: %v", z.Name, response.Message, wsErrCodes[response.Code]) + } + return &response, nil } -func (z *ZB) wsGetOrders(pair currency.Pair, pageIndex, tradeType int64) error { +func (z *ZB) wsGetOrders(pair currency.Pair, pageIndex, tradeType int64) (*WsGetOrdersResponse, error) { if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { - return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name) + return nil, fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name) } request := WsGetOrdersRequest{ PageIndex: pageIndex, TradeType: tradeType, + No: z.WebsocketConn.GenerateMessageID(true), } request.Channel = fmt.Sprintf("%v_getorders", pair.String()) request.Event = zWebsocketAddChannel request.Accesskey = z.APIKey request.Sign = z.wsGenerateSignature(request) - - return z.wsSend(request) + resp, err := z.WebsocketConn.SendMessageReturnResponse(request.No, request) + if err != nil { + return nil, err + } + var response WsGetOrdersResponse + err = common.JSONDecode(resp, &response) + if err != nil { + return nil, err + } + if response.Code > 0 && response.Code != 1000 { + return &response, fmt.Errorf("%v request failed, message: %v, error code: %v", z.Name, response.Message, wsErrCodes[response.Code]) + } + return &response, nil } -func (z *ZB) wsGetOrdersIgnoreTradeType(pair currency.Pair, pageIndex, pageSize int64) error { +func (z *ZB) wsGetOrdersIgnoreTradeType(pair currency.Pair, pageIndex, pageSize int64) (*WsGetOrdersIgnoreTradeTypeResponse, error) { if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { - return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name) + return nil, fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name) } request := WsGetOrdersIgnoreTradeTypeRequest{ PageIndex: pageIndex, PageSize: pageSize, + No: z.WebsocketConn.GenerateMessageID(true), } request.Channel = fmt.Sprintf("%v_getordersignoretradetype", pair.String()) request.Event = zWebsocketAddChannel request.Accesskey = z.APIKey request.Sign = z.wsGenerateSignature(request) - return z.wsSend(request) + resp, err := z.WebsocketConn.SendMessageReturnResponse(request.No, request) + if err != nil { + return nil, err + } + var response WsGetOrdersIgnoreTradeTypeResponse + err = common.JSONDecode(resp, &response) + if err != nil { + return nil, err + } + if response.Code > 0 && response.Code != 1000 { + return &response, fmt.Errorf("%v request failed, message: %v, error code: %v", z.Name, response.Message, wsErrCodes[response.Code]) + } + return &response, nil } -func (z *ZB) wsGetAccountInfoRequest() error { +func (z *ZB) wsGetAccountInfoRequest() (*WsGetAccountInfoResponse, error) { if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) { - return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name) + return nil, fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name) } request := WsAuthenticatedRequest{ Channel: "getaccountinfo", Event: zWebsocketAddChannel, Accesskey: z.APIKey, - No: fmt.Sprintf("%v", time.Now().Unix()), + No: z.WebsocketConn.GenerateMessageID(true), } request.Sign = z.wsGenerateSignature(request) - return z.wsSend(request) + resp, err := z.WebsocketConn.SendMessageReturnResponse(request.No, request) + if err != nil { + return nil, err + } + var response WsGetAccountInfoResponse + err = common.JSONDecode(resp, &response) + if err != nil { + return nil, err + } + if response.Code > 0 && response.Code != 1000 { + return &response, fmt.Errorf("%v request failed, message: %v, error code: %v", z.Name, response.Message, wsErrCodes[response.Code]) + } + return &response, nil } diff --git a/exchanges/zb/zb_websocket_types.go b/exchanges/zb/zb_websocket_types.go index 38fd95fa..dc4066e8 100644 --- a/exchanges/zb/zb_websocket_types.go +++ b/exchanges/zb/zb_websocket_types.go @@ -10,15 +10,15 @@ import ( type Subscription struct { Event string `json:"event"` Channel string `json:"channel"` + No int64 `json:"no,string,omitempty"` } // Generic defines a generic fields associated with many return types type Generic struct { Code int64 `json:"code"` - Success bool `json:"success"` Channel string `json:"channel"` Message interface{} `json:"message"` - No string `json:"no"` + No int64 `json:"no,string,omitempty"` Data json.RawMessage `json:"data"` } @@ -65,7 +65,7 @@ type WsAuthenticatedRequest struct { Accesskey string `json:"accesskey"` Channel string `json:"channel"` Event string `json:"event"` - No string `json:"no,omitempty"` + No int64 `json:"no,string,omitempty"` Sign string `json:"sign,omitempty"` } @@ -74,10 +74,11 @@ type WsAddSubUserRequest struct { Accesskey string `json:"accesskey"` Channel string `json:"channel"` Event string `json:"event"` - Sign string `json:"sign,omitempty"` Memo string `json:"memo"` Password string `json:"password"` SubUserName string `json:"subUserName"` + No int64 `json:"no,string,omtempty"` + Sign string `json:"sign,omitempty"` } // WsCreateSubUserKeyRequest data to add sub user keys @@ -90,7 +91,7 @@ type WsCreateSubUserKeyRequest struct { KeyName string `json:"keyName"` LeverPerm bool `json:"leverPerm,string"` MoneyPerm bool `json:"moneyPerm,string"` - No string `json:"no"` + No int64 `json:"no,string,omitempty"` Sign string `json:"sign,omitempty"` ToUserID string `json:"toUserId"` } @@ -103,7 +104,7 @@ type WsDoTransferFundsRequest struct { Currency currency.Code `json:"currency"` Event string `json:"event"` FromUserName string `json:"fromUserName"` - No string `json:"no"` + No int64 `json:"no,string"` Sign string `json:"sign,omitempty"` ToUserName string `json:"toUserName"` } @@ -114,7 +115,7 @@ type WsGetSubUserListResponse struct { Code int64 `json:"code"` Channel string `json:"channel"` Message []WsGetSubUserListResponseData `json:"message"` - No string `json:"no"` + No int64 `json:"no,string"` } // WsGetSubUserListResponseData user data @@ -132,7 +133,7 @@ type WsRequestResponse struct { Code int64 `json:"code"` Channel string `json:"channel"` Message interface{} `json:"message"` - No string `json:"no"` + No int64 `json:"no,string"` } // WsSubmitOrderRequest creates an order via ws @@ -141,7 +142,7 @@ type WsSubmitOrderRequest struct { Amount float64 `json:"amount,string"` Channel string `json:"channel"` Event string `json:"event"` - No string `json:"no,omitempty"` + No int64 `json:"no,string,omitempty"` Price float64 `json:"price,string"` Sign string `json:"sign,omitempty"` TradeType int64 `json:"tradeType,string"` @@ -150,7 +151,7 @@ type WsSubmitOrderRequest struct { // WsSubmitOrderResponse data about submitted order type WsSubmitOrderResponse struct { Message string `json:"message"` - No string `json:"no"` + No int64 `json:"no,string"` Data struct { EntrustID int64 `json:"intrustID"` } `json:"data"` @@ -166,12 +167,13 @@ type WsCancelOrderRequest struct { Event string `json:"event"` ID int64 `json:"id"` Sign string `json:"sign,omitempty"` + No int64 `json:"no,string"` } // WsCancelOrderResponse order cancel response type WsCancelOrderResponse struct { Message string `json:"message"` - No string `json:"no"` + No int64 `json:"no,string"` Code int64 `json:"code"` Channel string `json:"channel"` Success bool `json:"success"` @@ -184,12 +186,13 @@ type WsGetOrderRequest struct { Event string `json:"event"` ID int64 `json:"id"` Sign string `json:"sign,omitempty"` + No int64 `json:"no,string"` } // WsGetOrderResponse contains order data type WsGetOrderResponse struct { Message string `json:"message"` - No string `json:"no"` + No int64 `json:"no,string"` Code int64 `json:"code"` Channel string `json:"channel"` Success bool `json:"success"` @@ -209,6 +212,7 @@ type WsGetOrderResponseData struct { TradeDate int64 `json:"trade_date"` TradeMoney float64 `json:"trade_money"` Type int64 `json:"type"` + No int64 `json:"no,string"` } // WsGetOrdersRequest get more orders, with no orderID filtering @@ -216,6 +220,7 @@ type WsGetOrdersRequest struct { Accesskey string `json:"accesskey"` Channel string `json:"channel"` Event string `json:"event"` + No int64 `json:"no,string"` PageIndex int64 `json:"pageIndex"` TradeType int64 `json:"tradeType"` Sign string `json:"sign,omitempty"` @@ -224,7 +229,7 @@ type WsGetOrdersRequest struct { // WsGetOrdersResponse contains orders data type WsGetOrdersResponse struct { Message string `json:"message"` - No string `json:"no"` + No int64 `json:"no,string"` Code int64 `json:"code"` Channel string `json:"channel"` Success bool `json:"success"` @@ -236,7 +241,7 @@ type WsGetOrdersIgnoreTradeTypeRequest struct { Accesskey string `json:"accesskey"` Channel string `json:"channel"` Event string `json:"event"` - ID int64 `json:"id"` + No int64 `json:"no,string"` PageIndex int64 `json:"pageIndex"` PageSize int64 `json:"pageSize"` Sign string `json:"sign,omitempty"` @@ -245,7 +250,7 @@ type WsGetOrdersIgnoreTradeTypeRequest struct { // WsGetOrdersIgnoreTradeTypeResponse contains orders data type WsGetOrdersIgnoreTradeTypeResponse struct { Message string `json:"message"` - No string `json:"no"` + No int64 `json:"no,string"` Code int64 `json:"code"` Channel string `json:"channel"` Success bool `json:"success"` @@ -255,12 +260,12 @@ type WsGetOrdersIgnoreTradeTypeResponse struct { // WsGetAccountInfoResponse contains account data type WsGetAccountInfoResponse struct { Message string `json:"message"` - No string `json:"no"` + No int64 `json:"no,string"` Data struct { Coins []struct { Freez float64 `json:"freez,string"` EnName string `json:"enName"` - UnitDecimal int `json:"unitDecimal"` + UnitDecimal int64 `json:"unitDecimal"` CnName string `json:"cnName"` UnitTag string `json:"unitTag"` Available float64 `json:"available,string"` @@ -273,7 +278,7 @@ type WsGetAccountInfoResponse struct { AuthMobileEnabled bool `json:"auth_mobile_enabled"` } `json:"base"` } `json:"data"` - Code int `json:"code"` + Code int64 `json:"code"` Channel string `json:"channel"` Success bool `json:"success"` } diff --git a/exchanges/zb/zb_wrapper.go b/exchanges/zb/zb_wrapper.go index e977ee8d..fdf814b7 100644 --- a/exchanges/zb/zb_wrapper.go +++ b/exchanges/zb/zb_wrapper.go @@ -12,6 +12,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" ) @@ -299,7 +300,7 @@ func (z *ZB) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.With } // GetWebsocket returns a pointer to the exchange websocket -func (z *ZB) GetWebsocket() (*exchange.Websocket, error) { +func (z *ZB) GetWebsocket() (*wshandler.Websocket, error) { return z.Websocket, nil } @@ -414,19 +415,19 @@ func (z *ZB) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exc // SubscribeToWebsocketChannels appends to ChannelsToSubscribe // which lets websocket.manageSubscriptions handle subscribing -func (z *ZB) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (z *ZB) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { z.Websocket.SubscribeToChannels(channels) return nil } // UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe // which lets websocket.manageSubscriptions handle unsubscribing -func (z *ZB) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func (z *ZB) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { return common.ErrFunctionNotSupported } // GetSubscriptions returns a copied list of subscriptions -func (z *ZB) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) { +func (z *ZB) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { return z.Websocket.GetSubscriptions(), nil } diff --git a/routines.go b/routines.go index 99b5ce93..7cbfca77 100644 --- a/routines.go +++ b/routines.go @@ -12,6 +12,7 @@ import ( "github.com/thrasher-/gocryptotrader/exchanges/orderbook" "github.com/thrasher-/gocryptotrader/exchanges/stats" "github.com/thrasher-/gocryptotrader/exchanges/ticker" + "github.com/thrasher-/gocryptotrader/exchanges/wshandler" log "github.com/thrasher-/gocryptotrader/logger" ) @@ -311,7 +312,7 @@ func WebsocketRoutine(verbose bool) { err = ws.Connect() if err != nil { switch err.Error() { - case exchange.WebsocketNotEnabled: + case wshandler.WebsocketNotEnabled: log.Warnf("%s - websocket disabled", bot.exchanges[i].GetName()) default: log.Error(err) @@ -326,7 +327,7 @@ var wg sync.WaitGroup // Websocketshutdown shuts down the exchange routines and then shuts down // governing routines -func Websocketshutdown(ws *exchange.Websocket) error { +func Websocketshutdown(ws *wshandler.Websocket) error { err := ws.Shutdown() // shutdown routines on the exchange if err != nil { log.Errorf("routines.go error - failed to shutodwn %s", err) @@ -352,7 +353,7 @@ func Websocketshutdown(ws *exchange.Websocket) error { // streamDiversion is a diversion switch from websocket to REST or other // alternative feed -func streamDiversion(ws *exchange.Websocket, verbose bool) { +func streamDiversion(ws *wshandler.Websocket, verbose bool) { wg.Add(1) defer wg.Done() @@ -377,7 +378,7 @@ func streamDiversion(ws *exchange.Websocket, verbose bool) { // WebsocketDataHandler handles websocket data coming from a websocket feed // associated with an exchange -func WebsocketDataHandler(ws *exchange.Websocket, verbose bool) { +func WebsocketDataHandler(ws *wshandler.Websocket, verbose bool) { wg.Add(1) defer wg.Done() @@ -392,7 +393,7 @@ func WebsocketDataHandler(ws *exchange.Websocket, verbose bool) { switch d := data.(type) { case string: switch d { - case exchange.WebsocketNotEnabled: + case wshandler.WebsocketNotEnabled: if verbose { log.Warnf("routines.go warning - exchange %s weboscket not enabled", ws.GetName()) @@ -411,23 +412,23 @@ func WebsocketDataHandler(ws *exchange.Websocket, verbose bool) { log.Errorf("routines.go exchange %s websocket error - %s", ws.GetName(), data) } - case exchange.TradeData: + case wshandler.TradeData: // Trade Data if verbose { log.Infoln("Websocket trades Updated: ", d) } - case exchange.TickerData: + case wshandler.TickerData: // Ticker data if verbose { log.Infoln("Websocket Ticker Updated: ", d) } - case exchange.KlineData: + case wshandler.KlineData: // Kline data if verbose { log.Infoln("Websocket Kline Updated: ", d) } - case exchange.WebsocketOrderbookUpdate: + case wshandler.WebsocketOrderbookUpdate: // Orderbook data if verbose { log.Infoln("Websocket Orderbook Updated:", d) diff --git a/testdata/configtest.json b/testdata/configtest.json index b04f691d..282be76f 100644 --- a/testdata/configtest.json +++ b/testdata/configtest.json @@ -170,6 +170,8 @@ "useSandbox": false, "restPollingDelay": 10, "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpUserAgent": "", "httpDebugging": false, "authenticatedApiSupport": false, @@ -212,6 +214,8 @@ "useSandbox": false, "restPollingDelay": 10, "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpUserAgent": "", "httpDebugging": false, "authenticatedApiSupport": false, @@ -254,6 +258,8 @@ "useSandbox": false, "restPollingDelay": 10, "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpUserAgent": "", "httpDebugging": false, "authenticatedApiSupport": false, @@ -304,6 +310,8 @@ "useSandbox": false, "restPollingDelay": 10, "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpUserAgent": "", "httpDebugging": false, "authenticatedApiSupport": false, @@ -348,6 +356,8 @@ "useSandbox": false, "restPollingDelay": 10, "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpUserAgent": "", "httpDebugging": false, "authenticatedApiSupport": false, @@ -391,6 +401,8 @@ "useSandbox": false, "restPollingDelay": 10, "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpUserAgent": "", "httpDebugging": false, "authenticatedApiSupport": false, @@ -433,6 +445,8 @@ "useSandbox": false, "restPollingDelay": 10, "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpUserAgent": "", "httpDebugging": false, "authenticatedApiSupport": false, @@ -476,6 +490,8 @@ "useSandbox": false, "restPollingDelay": 10, "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpUserAgent": "", "httpDebugging": false, "authenticatedApiSupport": false, @@ -519,6 +535,8 @@ "useSandbox": false, "restPollingDelay": 10, "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpUserAgent": "", "httpDebugging": false, "authenticatedApiSupport": false, @@ -561,6 +579,8 @@ "useSandbox": false, "restPollingDelay": 10, "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpUserAgent": "", "httpDebugging": false, "authenticatedApiSupport": false, @@ -603,6 +623,8 @@ "useSandbox": false, "restPollingDelay": 10, "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpUserAgent": "", "httpDebugging": false, "authenticatedApiSupport": false, @@ -647,6 +669,8 @@ "useSandbox": false, "restPollingDelay": 10, "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpUserAgent": "", "httpDebugging": false, "authenticatedApiSupport": false, @@ -690,6 +714,8 @@ "useSandbox": false, "restPollingDelay": 10, "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpUserAgent": "", "httpDebugging": false, "authenticatedApiSupport": false, @@ -733,6 +759,8 @@ "useSandbox": false, "restPollingDelay": 10, "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpUserAgent": "", "httpDebugging": false, "authenticatedApiSupport": false, @@ -774,6 +802,8 @@ "useSandbox": false, "restPollingDelay": 10, "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpUserAgent": "", "httpDebugging": false, "authenticatedApiSupport": false, @@ -816,6 +846,8 @@ "useSandbox": false, "restPollingDelay": 10, "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpUserAgent": "", "httpDebugging": false, "authenticatedApiSupport": false, @@ -859,6 +891,8 @@ "useSandbox": false, "restPollingDelay": 10, "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpUserAgent": "", "httpDebugging": false, "authenticatedApiSupport": false, @@ -902,6 +936,8 @@ "useSandbox": false, "restPollingDelay": 10, "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpUserAgent": "", "httpDebugging": false, "authenticatedApiSupport": false, @@ -945,6 +981,8 @@ "useSandbox": false, "restPollingDelay": 10, "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpUserAgent": "", "httpDebugging": false, "authenticatedApiSupport": false, @@ -988,6 +1026,8 @@ "useSandbox": false, "restPollingDelay": 10, "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpUserAgent": "", "httpDebugging": false, "authenticatedApiSupport": false, @@ -1029,6 +1069,8 @@ "useSandbox": false, "restPollingDelay": 10, "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpUserAgent": "", "httpDebugging": false, "authenticatedApiSupport": false, @@ -1070,6 +1112,8 @@ "useSandbox": false, "restPollingDelay": 10, "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpUserAgent": "", "httpDebugging": false, "authenticatedApiSupport": false, @@ -1113,6 +1157,8 @@ "useSandbox": false, "restPollingDelay": 10, "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpUserAgent": "", "httpDebugging": false, "authenticatedApiSupport": false, @@ -1156,6 +1202,8 @@ "useSandbox": false, "restPollingDelay": 10, "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpUserAgent": "", "httpDebugging": false, "authenticatedApiSupport": false, @@ -1199,6 +1247,8 @@ "useSandbox": false, "restPollingDelay": 10, "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpUserAgent": "", "httpDebugging": false, "authenticatedApiSupport": false, @@ -1244,6 +1294,8 @@ "useSandbox": false, "restPollingDelay": 10, "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpUserAgent": "", "httpDebugging": false, "authenticatedApiSupport": false, @@ -1286,7 +1338,9 @@ "websocket": false, "useSandbox": false, "restPollingDelay": 10, - "httpTimeout": 10, + "httpTimeout": 15000000000, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, "httpUserAgent": "", "httpDebugging": false, "authenticatedApiSupport": false, @@ -1297,8 +1351,8 @@ "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", "proxyAddress": "", "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", - "availablePairs": "XRPM19,BCHM19,ADAM19,EOSM19,TRXM19,XBTUSD,XBT7D_U105,XBT7D_D95,XBTM19,XBTU19,ETHUSD,ETHM19,LTCM19", - "enabledPairs": "LTCM19", + "availablePairs": "XRPU19,BCHU19,ADAU19,ADAU19,TRXU19,XBTUSD,XBT7D_U105,XBT7D_D95,XBTU19,XBTZ19,ETHUSD,ETHU19,LTCU19", + "enabledPairs": "XBTUSD", "baseCurrencies": "USD", "assetTypes": "SPOT", "supportsAutoPairUpdates": true, diff --git a/tools/documentation/config_templates/config_readme.tmpl b/tools/documentation/config_templates/config_readme.tmpl index 46d37643..dfc3ab7a 100644 --- a/tools/documentation/config_templates/config_readme.tmpl +++ b/tools/documentation/config_templates/config_readme.tmpl @@ -64,8 +64,9 @@ have multiple deposit accounts for different FIAT deposit currencies. "Websocket": false, "UseSandbox": false, "RESTPollingDelay": 10, - "HTTPTimeout": 15000000000, - "AuthenticatedAPISupport": false, + "websocketResponseCheckTimeout": 30000000, + "websocketResponseMaxLimit": 7000000000, + "httpTimeout": 15000000000, "APIKey": "Key", "APISecret": "Secret", "AvailablePairs": "ATENC_GBP,ATENC_NZD,BTC_AUD,BTC_SGD,LTC_BTC,START_GBP,...", diff --git a/tools/exchange_template/main_file.tmpl b/tools/exchange_template/main_file.tmpl index 3458413a..69edd3b2 100644 --- a/tools/exchange_template/main_file.tmpl +++ b/tools/exchange_template/main_file.tmpl @@ -2,14 +2,15 @@ package {{.Name}} import ( - "log" "time" "github.com/thrasher-/gocryptotrader/common" "github.com/thrasher-/gocryptotrader/config" - "github.com/thrasher-/gocryptotrader/exchanges" + 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" ) // {{.CapitalName}} is the overarching type across this package @@ -46,7 +47,9 @@ func ({{.Variable}} *{{.CapitalName}}) SetDefaults() { common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) {{.Variable}}.APIUrlDefault = {{.Name}}APIURL {{.Variable}}.APIUrl = {{.Variable}}.APIUrlDefault - {{.Variable}}.WebsocketInit() + {{.Variable}}.Websocket = wshandler.New() + {{.Variable}}.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit + {{.Variable}}.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout } // Setup takes in the supplied exchange configuration details and sets params @@ -96,6 +99,14 @@ func ({{.Variable}} *{{.CapitalName}}) Setup(exch *config.ExchangeConfig) { // if err != nil { // log.Fatal(err) // } + // {{.Variable}}.WebsocketConn = &wshandler.WebsocketConnection{ + // ExchangeName: {{.Variable}}.Name, + // URL: {{.Variable}}.Websocket.GetWebsocketURL(), + // ProxyURL: {{.Variable}}.Websocket.GetProxyAddress(), + // Verbose: {{.Variable}}.Verbose, + // ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, + // ResponseMaxLimit: exch.WebsocketResponseMaxLimit, + // } } } {{end}} diff --git a/tools/exchange_template/wrapper_file.tmpl b/tools/exchange_template/wrapper_file.tmpl index 9fbcc894..82ace798 100644 --- a/tools/exchange_template/wrapper_file.tmpl +++ b/tools/exchange_template/wrapper_file.tmpl @@ -187,20 +187,20 @@ func ({{.Variable}} *{{.CapitalName}}) GetFeeByType(feeBuilder *exchange.FeeBuil // SubscribeToWebsocketChannels appends to ChannelsToSubscribe // which lets websocket.manageSubscriptions handle subscribing -func ({{.Variable}} *{{.CapitalName}}) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func ({{.Variable}} *{{.CapitalName}}) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { {{.Variable}}.Websocket.SubscribeToChannels(channels) return nil } // UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe // which lets websocket.manageSubscriptions handle unsubscribing -func ({{.Variable}} *{{.CapitalName}}) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error { +func ({{.Variable}} *{{.CapitalName}}) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { {{.Variable}}.Websocket.UnubscribeToChannels(channels) return nil } // GetSubscriptions returns a copied list of subscriptions -func ({{.Variable}} *{{.CapitalName}}) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) { +func ({{.Variable}} *{{.CapitalName}}) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { return nil, common.ErrNotYetImplemented } diff --git a/tools/websocket_client/main.go b/tools/websocket_client/main.go index 70c980c3..e0438b4a 100644 --- a/tools/websocket_client/main.go +++ b/tools/websocket_client/main.go @@ -84,8 +84,8 @@ func main() { common.ExtractPort(listenAddr)) log.Printf("Connecting to websocket host: %s", wsHost) - var Dialer websocket.Dialer - WSConn, _, err = Dialer.Dial(wsHost, http.Header{}) + var dialer websocket.Dialer + WSConn, _, err = dialer.Dial(wsHost, http.Header{}) if err != nil { log.Println("Unable to connect to websocket server") return