Websocket request-response correlation (#328)

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

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

* Successfully moved exchange_websocket into its own wshandler namespace. oof

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

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

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

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

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

* Adds GateIO id support

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

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

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

* Starts testing. Renames files

* Adds tests for websocket connection

* Reverts request.go change

* Linting everything

* Fixes rebase issue

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

* Final final commit, fixing ZB issues.

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

* Fixed string conversion

* Fixes issue with ZB not sending success codes

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

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

* Removes unused interface

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

* Updates template file

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

* Fixes two inconsistent websocket delay times

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

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

View File

@@ -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,
}
}
}

View File

@@ -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)
}
}

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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
}