Merge branch 'master' into engine

This commit is contained in:
Adrian Gallagher
2019-06-21 18:10:55 +10:00
87 changed files with 5669 additions and 992 deletions

View File

@@ -55,6 +55,7 @@ func ({{.Variable}} *{{.CapitalName}}) Setup(exch *config.ExchangeConfig) error
} else {
{{.Variable}}.Enabled = true
{{.Variable}}.API.AuthenticatedSupport = exch.API.AuthenticatedSupport
{{.Variable}}.API.AuthenticatedWebsocketSupport = exch.API.AuthenticatedWebsocketSupport
{{.Variable}}.SetAPIKeys(exch.API.Credentials.Key, exch.API.Credentials.Secret, "", false)
{{.Variable}}.SetHTTPClientTimeout(exch.HTTPTimeout)
{{.Variable}}.SetHTTPClientUserAgent(exch.HTTPUserAgent)

View File

@@ -199,4 +199,14 @@ func ({{.Variable}} *{{.CapitalName}}) UnsubscribeToWebsocketChannels(channels [
return nil
}
// GetSubscriptions returns a copied list of subscriptions
func ({{.Variable}} *{{.CapitalName}}) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return nil, common.ErrNotYetImplemented
}
// AuthenticateWebsocket sends an authentication message to the websocket
func ({{.Variable}} *{{.CapitalName}}) AuthenticateWebsocket() error {
return common.ErrNotYetImplemented
}
{{end}}

View File

@@ -215,10 +215,11 @@ func (c *Config) PurgeExchangeAPICredentials() {
m.Lock()
defer m.Unlock()
for x := range c.Exchanges {
if !c.Exchanges[x].API.AuthenticatedSupport {
if !c.Exchanges[x].API.AuthenticatedSupport && !c.Exchanges[x].API.AuthenticatedWebsocketSupport {
continue
}
c.Exchanges[x].API.AuthenticatedSupport = false
c.Exchanges[x].API.AuthenticatedWebsocketSupport = false
if c.Exchanges[x].API.CredentialsValidator.RequiresKey {
c.Exchanges[x].API.Credentials.Key = DefaultAPIKey
@@ -838,6 +839,9 @@ func (c *Config) CheckExchangeConfigValues() error {
if c.Exchanges[i].APIKey != nil {
// It is, migrate settings to new format
c.Exchanges[i].API.AuthenticatedSupport = *c.Exchanges[i].AuthenticatedAPISupport
if c.Exchanges[i].AuthenticatedWebsocketAPISupport != nil {
c.Exchanges[i].API.AuthenticatedWebsocketSupport = *c.Exchanges[i].AuthenticatedWebsocketAPISupport
}
c.Exchanges[i].API.Credentials.Key = *c.Exchanges[i].APIKey
c.Exchanges[i].API.Credentials.Secret = *c.Exchanges[i].APISecret
@@ -862,6 +866,7 @@ func (c *Config) CheckExchangeConfigValues() error {
// Flush settings
c.Exchanges[i].AuthenticatedAPISupport = nil
c.Exchanges[i].AuthenticatedWebsocketAPISupport = nil
c.Exchanges[i].APIKey = nil
c.Exchanges[i].APIAuthPEMKey = nil
c.Exchanges[i].APISecret = nil
@@ -941,20 +946,23 @@ func (c *Config) CheckExchangeConfigValues() error {
c.Exchanges[i].Enabled = false
continue
}
if c.Exchanges[i].API.AuthenticatedSupport && c.Exchanges[i].API.CredentialsValidator != nil {
if (c.Exchanges[i].API.AuthenticatedSupport || c.Exchanges[i].API.AuthenticatedWebsocketSupport) && c.Exchanges[i].API.CredentialsValidator != nil {
var failed bool
if c.Exchanges[i].API.CredentialsValidator.RequiresKey && (c.Exchanges[i].API.Credentials.Key == "" || c.Exchanges[i].API.Credentials.Key == DefaultAPIKey) {
c.Exchanges[i].API.AuthenticatedSupport = false
failed = true
}
if c.Exchanges[i].API.CredentialsValidator.RequiresSecret && (c.Exchanges[i].API.Credentials.Secret == "" || c.Exchanges[i].API.Credentials.Secret == DefaultAPISecret) {
c.Exchanges[i].API.AuthenticatedSupport = false
failed = true
}
if c.Exchanges[i].API.CredentialsValidator.RequiresClientID && (c.Exchanges[i].API.Credentials.ClientID == DefaultAPIClientID || c.Exchanges[i].API.Credentials.ClientID == "") {
c.Exchanges[i].API.AuthenticatedSupport = false
failed = true
}
if !c.Exchanges[i].API.AuthenticatedSupport {
if failed {
c.Exchanges[i].API.AuthenticatedSupport = false
c.Exchanges[i].API.AuthenticatedWebsocketSupport = false
log.Warnf(WarningExchangeAuthAPIDefaultOrEmptyValues, c.Exchanges[i].Name)
}
}

View File

@@ -628,6 +628,7 @@ func TestUpdateExchangeConfig(t *testing.T) {
}
}
// TestCheckExchangeConfigValues logic test
func TestCheckExchangeConfigValues(t *testing.T) {
checkExchangeConfigValues := Config{}
@@ -651,25 +652,43 @@ func TestCheckExchangeConfigValues(t *testing.T) {
t.Fatalf("Test failed. Expected exchange %s to have updated HTTPTimeout value", checkExchangeConfigValues.Exchanges[0].Name)
}
v := &APICredentialsValidatorConfig{
RequiresKey: true,
RequiresSecret: true,
}
checkExchangeConfigValues.Exchanges[0].API.CredentialsValidator = v
checkExchangeConfigValues.Exchanges[0].API.Credentials.Key = "Key"
checkExchangeConfigValues.Exchanges[0].API.Credentials.Secret = "Secret"
checkExchangeConfigValues.Exchanges[0].API.AuthenticatedSupport = true
err = checkExchangeConfigValues.CheckExchangeConfigValues()
if err != nil {
t.Errorf(
"Test failed. checkExchangeConfigValues.CheckExchangeConfigValues Error",
)
checkExchangeConfigValues.Exchanges[0].API.AuthenticatedWebsocketSupport = true
checkExchangeConfigValues.CheckExchangeConfigValues()
if checkExchangeConfigValues.Exchanges[0].API.AuthenticatedSupport ||
checkExchangeConfigValues.Exchanges[0].API.AuthenticatedWebsocketSupport {
t.Error("Expected authenticated endpoints to be false from invalid API keys")
}
v.RequiresKey = false
v.RequiresClientID = true
checkExchangeConfigValues.Exchanges[0].API.AuthenticatedSupport = true
checkExchangeConfigValues.Exchanges[0].API.Credentials.Key = "TESTYTEST"
checkExchangeConfigValues.Exchanges[0].API.AuthenticatedWebsocketSupport = true
checkExchangeConfigValues.Exchanges[0].API.Credentials.ClientID = DefaultAPIClientID
checkExchangeConfigValues.Exchanges[0].API.Credentials.Secret = "TESTYTEST"
checkExchangeConfigValues.Exchanges[0].Name = "ITBIT"
err = checkExchangeConfigValues.CheckExchangeConfigValues()
if err != nil {
t.Errorf(
"Test failed. checkExchangeConfigValues.CheckExchangeConfigValues Error",
)
checkExchangeConfigValues.CheckExchangeConfigValues()
if checkExchangeConfigValues.Exchanges[0].API.AuthenticatedSupport ||
checkExchangeConfigValues.Exchanges[0].API.AuthenticatedWebsocketSupport {
t.Error("Expected AuthenticatedAPISupport to be false from invalid API keys")
}
v.RequiresKey = true
checkExchangeConfigValues.Exchanges[0].API.AuthenticatedSupport = true
checkExchangeConfigValues.Exchanges[0].API.AuthenticatedWebsocketSupport = true
checkExchangeConfigValues.Exchanges[0].API.Credentials.Key = "meow"
checkExchangeConfigValues.Exchanges[0].API.Credentials.Secret = "test123"
checkExchangeConfigValues.Exchanges[0].API.Credentials.ClientID = "clientIDerino"
checkExchangeConfigValues.CheckExchangeConfigValues()
if !checkExchangeConfigValues.Exchanges[0].API.AuthenticatedSupport ||
!checkExchangeConfigValues.Exchanges[0].API.AuthenticatedWebsocketSupport {
t.Error("Expected AuthenticatedAPISupport and AuthenticatedWebsocketAPISupport to be false from invalid API keys")
}
checkExchangeConfigValues.Exchanges[0].Enabled = true

View File

@@ -61,23 +61,24 @@ type ExchangeConfig struct {
BankAccounts []BankAccount `json:"bankAccounts,omitempty"`
// Deprecated settings which will be removed in a future update
AvailablePairs *currency.Pairs `json:"availablePairs,omitempty"`
EnabledPairs *currency.Pairs `json:"enabledPairs,omitempty"`
AssetTypes *string `json:"assetTypes,omitempty"`
PairsLastUpdated *int64 `json:"pairsLastUpdated,omitempty"`
ConfigCurrencyPairFormat *currency.PairFormat `json:"configCurrencyPairFormat,omitempty"`
RequestCurrencyPairFormat *currency.PairFormat `json:"requestCurrencyPairFormat,omitempty"`
AuthenticatedAPISupport *bool `json:"authenticatedApiSupport,omitempty"`
APIKey *string `json:"apiKey,omitempty"`
APISecret *string `json:"apiSecret,omitempty"`
APIAuthPEMKeySupport *bool `json:"apiAuthPemKeySupport,omitempty"`
APIAuthPEMKey *string `json:"apiAuthPemKey,omitempty"`
APIURL *string `json:"apiUrl,omitempty"`
APIURLSecondary *string `json:"apiUrlSecondary,omitempty"`
ClientID *string `json:"clientId,omitempty"`
SupportsAutoPairUpdates *bool `json:"supportsAutoPairUpdates,omitempty"`
Websocket *bool `json:"websocket,omitempty"`
WebsocketURL *string `json:"websocketUrl,omitempty"`
AvailablePairs *currency.Pairs `json:"availablePairs,omitempty"`
EnabledPairs *currency.Pairs `json:"enabledPairs,omitempty"`
AssetTypes *string `json:"assetTypes,omitempty"`
PairsLastUpdated *int64 `json:"pairsLastUpdated,omitempty"`
ConfigCurrencyPairFormat *currency.PairFormat `json:"configCurrencyPairFormat,omitempty"`
RequestCurrencyPairFormat *currency.PairFormat `json:"requestCurrencyPairFormat,omitempty"`
AuthenticatedAPISupport *bool `json:"authenticatedApiSupport,omitempty"`
AuthenticatedWebsocketAPISupport *bool `json:"authenticatedWebsocketApiSupport,omitempty"`
APIKey *string `json:"apiKey,omitempty"`
APISecret *string `json:"apiSecret,omitempty"`
APIAuthPEMKeySupport *bool `json:"apiAuthPemKeySupport,omitempty"`
APIAuthPEMKey *string `json:"apiAuthPemKey,omitempty"`
APIURL *string `json:"apiUrl,omitempty"`
APIURLSecondary *string `json:"apiUrlSecondary,omitempty"`
ClientID *string `json:"clientId,omitempty"`
SupportsAutoPairUpdates *bool `json:"supportsAutoPairUpdates,omitempty"`
Websocket *bool `json:"websocket,omitempty"`
WebsocketURL *string `json:"websocketUrl,omitempty"`
}
// ProfilerConfig defines the profiler configuration to enable pprof
@@ -339,8 +340,9 @@ type APICredentialsValidatorConfig struct {
// APIConfig stores the exchange API config
type APIConfig struct {
AuthenticatedSupport bool `json:"authenticatedSupport"`
PEMKeySupport bool `json:"pemKeySupport,omitempty"`
AuthenticatedSupport bool `json:"authenticatedSupport"`
AuthenticatedWebsocketSupport bool `json:"authenticatedWebsocketApiSupport"`
PEMKeySupport bool `json:"pemKeySupport,omitempty"`
Endpoints APIEndpointsConfig `json:"endpoints"`
Credentials APICredentialsConfig `json:"credentials"`

View File

@@ -254,6 +254,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -379,6 +380,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -669,6 +671,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -711,6 +714,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -753,6 +757,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -793,6 +798,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -834,6 +840,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiAuthPemKey": "-----BEGIN EC PRIVATE KEY-----\nJUSTADUMMY\n-----END EC PRIVATE KEY-----\n",
@@ -876,6 +883,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiAuthPemKey": "-----BEGIN EC PRIVATE KEY-----\nJUSTADUMMY\n-----END EC PRIVATE KEY-----\n",
@@ -1082,6 +1090,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -1124,6 +1133,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -1166,6 +1176,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",

View File

@@ -153,7 +153,8 @@ func GetExchangeoOTPByName(exchName string) (string, error) {
func GetAuthAPISupportedExchanges() []string {
var exchanges []string
for x := range Bot.Exchanges {
if !Bot.Exchanges[x].GetAuthenticatedAPISupport() {
if !Bot.Exchanges[x].GetAuthenticatedAPISupport(exchange.RestAuthentication) &&
!Bot.Exchanges[x].GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
continue
}
exchanges = append(exchanges, Bot.Exchanges[x].GetName())
@@ -649,7 +650,7 @@ func GetExchangeCryptocurrencyDepositAddresses() map[string]map[string]string {
}
exchName := Bot.Exchanges[x].GetName()
if !Bot.Exchanges[x].GetAuthenticatedAPISupport() {
if !Bot.Exchanges[x].GetAuthenticatedAPISupport(exchange.RestAuthentication) {
if Bot.Settings.Verbose {
log.Debugf("GetExchangeCryptocurrencyDepositAddresses: Skippping %s due to disabled authenticated API support.", exchName)
}
@@ -771,7 +772,7 @@ func GetAllEnabledExchangeAccountInfo() AllEnabledExchangeAccounts {
var response AllEnabledExchangeAccounts
for _, individualBot := range Bot.Exchanges {
if individualBot != nil && individualBot.IsEnabled() {
if !individualBot.GetAuthenticatedAPISupport() {
if !individualBot.GetAuthenticatedAPISupport(exchange.RestAuthentication) {
if Bot.Settings.Verbose {
log.Debugf("GetAllEnabledExchangeAccountInfo: Skippping %s due to disabled authenticated API support.", individualBot.GetName())
}

View File

@@ -391,3 +391,13 @@ func (a *Alphapoint) SubscribeToWebsocketChannels(channels []exchange.WebsocketC
func (a *Alphapoint) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error {
return common.ErrFunctionNotSupported
}
// GetSubscriptions returns a copied list of subscriptions
func (a *Alphapoint) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return nil, common.ErrFunctionNotSupported
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (a *Alphapoint) AuthenticateWebsocket() error {
return common.ErrFunctionNotSupported
}

View File

@@ -551,3 +551,13 @@ func (a *ANX) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelS
func (a *ANX) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error {
return common.ErrFunctionNotSupported
}
// GetSubscriptions returns a copied list of subscriptions
func (a *ANX) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return nil, common.ErrFunctionNotSupported
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (a *ANX) AuthenticateWebsocket() error {
return common.ErrFunctionNotSupported
}

View File

@@ -541,3 +541,13 @@ func (b *Binance) SubscribeToWebsocketChannels(channels []exchange.WebsocketChan
func (b *Binance) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error {
return common.ErrFunctionNotSupported
}
// GetSubscriptions returns a copied list of subscriptions
func (b *Binance) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return b.Websocket.GetSubscriptions(), nil
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (b *Binance) AuthenticateWebsocket() error {
return common.ErrFunctionNotSupported
}

View File

@@ -1,15 +1,18 @@
package bitfinex
import (
"net/http"
"net/url"
"reflect"
"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"
)
// Please supply your own keys here to do better tests
@@ -38,6 +41,7 @@ func TestSetup(t *testing.T) {
}
b.API.AuthenticatedSupport = true
b.API.AuthenticatedWebsocketSupport = true
// custom rate limit for testing
b.Requester.SetRateLimit(true, time.Millisecond*300, 1)
b.Requester.SetRateLimit(false, time.Millisecond*300, 1)
@@ -962,3 +966,37 @@ func TestGetDepositAddress(t *testing.T) {
}
}
}
// TestWsAuth dials websocket, sends login request.
func TestWsAuth(t *testing.T) {
b.SetDefaults()
TestSetup(t)
if !b.Websocket.IsEnabled() && !b.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() {
t.Skip(exchange.WebsocketNotEnabled)
}
var err error
var dialer websocket.Dialer
b.WebsocketConn, _, err = dialer.Dial(b.Websocket.GetWebsocketURL(),
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)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case resp := <-b.Websocket.DataHandler:
if resp.(map[string]interface{})["event"] != "auth" && resp.(map[string]interface{})["status"] != "OK" {
t.Error("expected successful login")
}
case <-timer.C:
t.Error("Have not received a response")
}
timer.Stop()
}

View File

@@ -448,6 +448,18 @@ type WebsocketTradeExecuted struct {
PriceExecuted float64
}
// WebsocketTradeData holds executed trade data
type WebsocketTradeData struct {
TradeID int64
Pair string
Timestamp int64
OrderID int64
AmountExecuted float64
PriceExecuted float64
Fee float64
FeeCurrency string
}
// ErrorCapture is a simple type for returned errors from Bitfinex
type ErrorCapture struct {
Message string `json:"message"`

View File

@@ -20,28 +20,30 @@ import (
)
const (
bitfinexWebsocket = "wss://api.bitfinex.com/ws"
bitfinexWebsocketVersion = "1.1"
bitfinexWebsocketPositionSnapshot = "ps"
bitfinexWebsocketPositionNew = "pn"
bitfinexWebsocketPositionUpdate = "pu"
bitfinexWebsocketPositionClose = "pc"
bitfinexWebsocketWalletSnapshot = "ws"
bitfinexWebsocketWalletUpdate = "wu"
bitfinexWebsocketOrderSnapshot = "os"
bitfinexWebsocketOrderNew = "on"
bitfinexWebsocketOrderUpdate = "ou"
bitfinexWebsocketOrderCancel = "oc"
bitfinexWebsocketTradeExecuted = "te"
bitfinexWebsocketHeartbeat = "hb"
bitfinexWebsocketAlertRestarting = "20051"
bitfinexWebsocketAlertRefreshing = "20060"
bitfinexWebsocketAlertResume = "20061"
bitfinexWebsocketUnknownEvent = "10000"
bitfinexWebsocketUnknownPair = "10001"
bitfinexWebsocketSubscriptionFailed = "10300"
bitfinexWebsocketAlreadySubscribed = "10301"
bitfinexWebsocketUnknownChannel = "10302"
bitfinexWebsocket = "wss://api.bitfinex.com/ws"
bitfinexWebsocketVersion = "1.1"
bitfinexWebsocketPositionSnapshot = "ps"
bitfinexWebsocketPositionNew = "pn"
bitfinexWebsocketPositionUpdate = "pu"
bitfinexWebsocketPositionClose = "pc"
bitfinexWebsocketWalletSnapshot = "ws"
bitfinexWebsocketWalletUpdate = "wu"
bitfinexWebsocketOrderSnapshot = "os"
bitfinexWebsocketOrderNew = "on"
bitfinexWebsocketOrderUpdate = "ou"
bitfinexWebsocketOrderCancel = "oc"
bitfinexWebsocketTradeExecuted = "te"
bitfinexWebsocketTradeExecutionUpdate = "tu"
bitfinexWebsocketTradeSnapshots = "ts"
bitfinexWebsocketHeartbeat = "hb"
bitfinexWebsocketAlertRestarting = "20051"
bitfinexWebsocketAlertRefreshing = "20060"
bitfinexWebsocketAlertResume = "20061"
bitfinexWebsocketUnknownEvent = "10000"
bitfinexWebsocketUnknownPair = "10001"
bitfinexWebsocketSubscriptionFailed = "10300"
bitfinexWebsocketAlreadySubscribed = "10301"
bitfinexWebsocketUnknownChannel = "10302"
)
// WebsocketHandshake defines the communication between the websocket API for
@@ -78,6 +80,9 @@ func (b *Bitfinex) wsSend(data interface{}) error {
// WsSendAuth sends a autheticated event payload
func (b *Bitfinex) WsSendAuth() error {
if !b.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", b.Name)
}
req := make(map[string]interface{})
payload := "AUTH" + strconv.FormatInt(time.Now().UnixNano(), 10)[:13]
req["event"] = "auth"
@@ -91,7 +96,12 @@ func (b *Bitfinex) WsSendAuth() error {
req["authPayload"] = payload
return b.wsSend(req)
err := b.wsSend(req)
if err != nil {
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
return err
}
return nil
}
// WsSendUnauth sends an unauthenticated payload
@@ -151,6 +161,11 @@ func (b *Bitfinex) WsConnect() error {
return err
}
err = b.WsSendAuth()
if err != nil {
log.Errorf("%v - authentication failed: %v", b.Name, err)
}
b.GenerateDefaultSubscriptions()
if hs.Event == "info" {
if b.Verbose {
@@ -158,13 +173,6 @@ func (b *Bitfinex) WsConnect() error {
}
}
if b.AllowAuthenticatedRequest() {
err = b.WsSendAuth()
if err != nil {
return err
}
}
pongReceive = make(chan struct{}, 1)
go b.WsDataHandler()
@@ -226,15 +234,13 @@ func (b *Bitfinex) WsDataHandler() {
case "auth":
status := eventData["status"].(string)
if status == "OK" {
b.Websocket.DataHandler <- eventData
b.WsAddSubscriptionChannel(0, "account", "N/A")
} else if status == "fail" {
b.Websocket.DataHandler <- fmt.Errorf("bitfinex.go error - Websocket unable to AUTH. Error code: %s",
eventData["code"].(string))
b.API.AuthenticatedSupport = false
}
}
@@ -418,6 +424,19 @@ func (b *Bitfinex) WsDataHandler() {
AmountExecuted: data[4].(float64),
PriceExecuted: data[5].(float64)}
b.Websocket.DataHandler <- trade
case bitfinexWebsocketTradeSnapshots, bitfinexWebsocketTradeExecutionUpdate:
data := chanData[2].([]interface{})
trade := WebsocketTradeData{
TradeID: int64(data[0].(float64)),
Pair: data[1].(string),
Timestamp: int64(data[2].(float64)),
OrderID: int64(data[3].(float64)),
AmountExecuted: data[4].(float64),
PriceExecuted: data[5].(float64),
Fee: data[6].(float64),
FeeCurrency: data[7].(string)}
b.Websocket.DataHandler <- trade
}
@@ -603,7 +622,7 @@ func (b *Bitfinex) WsUpdateOrderbook(p currency.Pair, assetType asset.Item, book
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (b *Bitfinex) GenerateDefaultSubscriptions() {
var channels = []string{"book", "trades", "ticker"}
subscriptions := []exchange.WebsocketChannelSubscription{}
var subscriptions []exchange.WebsocketChannelSubscription
for i := range channels {
enabledPairs := b.GetEnabledPairs(asset.Spot)
for j := range enabledPairs {
@@ -626,7 +645,9 @@ func (b *Bitfinex) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscri
req := make(map[string]interface{})
req["event"] = "subscribe"
req["channel"] = channelToSubscribe.Channel
req["pair"] = channelToSubscribe.Currency.String()
if channelToSubscribe.Currency.String() != "" {
req["pair"] = channelToSubscribe.Currency.String()
}
if len(channelToSubscribe.Params) > 0 {
for k, v := range channelToSubscribe.Params {
req[k] = v

View File

@@ -579,3 +579,13 @@ func (b *Bitfinex) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketC
b.Websocket.UnsubscribeToChannels(channels)
return nil
}
// GetSubscriptions returns a copied list of subscriptions
func (b *Bitfinex) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return b.Websocket.GetSubscriptions(), nil
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (b *Bitfinex) AuthenticateWebsocket() error {
return b.WsSendAuth()
}

View File

@@ -364,3 +364,13 @@ func (b *Bitflyer) SubscribeToWebsocketChannels(channels []exchange.WebsocketCha
func (b *Bitflyer) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error {
return common.ErrFunctionNotSupported
}
// GetSubscriptions returns a copied list of subscriptions
func (b *Bitflyer) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return nil, common.ErrFunctionNotSupported
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (b *Bitflyer) AuthenticateWebsocket() error {
return common.ErrFunctionNotSupported
}

View File

@@ -520,3 +520,13 @@ func (b *Bithumb) SubscribeToWebsocketChannels(channels []exchange.WebsocketChan
func (b *Bithumb) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error {
return common.ErrFunctionNotSupported
}
// GetSubscriptions returns a copied list of subscriptions
func (b *Bithumb) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return nil, common.ErrFunctionNotSupported
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (b *Bithumb) AuthenticateWebsocket() error {
return common.ErrFunctionNotSupported
}

View File

@@ -1,14 +1,17 @@
package bitmex
import (
"net/http"
"sync"
"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"
)
// Please supply your own keys here for due diligence testing
@@ -33,6 +36,7 @@ func TestSetup(t *testing.T) {
}
bitmexConfig.API.AuthenticatedSupport = true
bitmexConfig.API.AuthenticatedWebsocketSupport = true
bitmexConfig.API.Credentials.Key = apiKey
bitmexConfig.API.Credentials.Secret = apiSecret
@@ -683,3 +687,37 @@ func TestGetDepositAddress(t *testing.T) {
}
}
}
// TestWsAuth dials websocket, sends login request.
func TestWsAuth(t *testing.T) {
b.SetDefaults()
TestSetup(t)
if !b.Websocket.IsEnabled() && !b.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() {
t.Skip(exchange.WebsocketNotEnabled)
}
var err error
var dialer websocket.Dialer
b.WebsocketConn, _, err = dialer.Dial(b.Websocket.GetWebsocketURL(),
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)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case resp := <-b.Websocket.DataHandler:
if !resp.(WebsocketSubscribeResp).Success {
t.Error("Expected successful subscription")
}
case <-timer.C:
t.Error("Have not received a response")
}
timer.Stop()
}

View File

@@ -110,12 +110,11 @@ func (b *Bitmex) WsConnector() error {
go b.wsHandleIncomingData()
b.GenerateDefaultSubscriptions()
if b.AllowAuthenticatedRequest() {
err := b.websocketSendAuth()
if err != nil {
return err
}
err = b.websocketSendAuth()
if err != nil {
log.Errorf("%v - authentication failed: %v", b.Name, err)
}
b.GenerateAuthenticatedSubscriptions()
return nil
}
@@ -193,11 +192,15 @@ func (b *Bitmex) wsHandleIncomingData() {
}
if decodedResp.Success {
if b.Verbose {
if len(quickCapture) == 3 {
b.Websocket.DataHandler <- decodedResp
if len(quickCapture) == 3 {
if b.Verbose {
log.Debugf("%s websocket: Successfully subscribed to %s",
b.Name, decodedResp.Subscribe)
} else {
}
} else {
b.Websocket.SetCanUseAuthenticatedEndpoints(true)
if b.Verbose {
log.Debugf("%s websocket: Successfully authenticated websocket connection",
b.Name)
}
@@ -267,7 +270,6 @@ func (b *Bitmex) wsHandleIncomingData() {
case bitmexWSAnnouncement:
var announcement AnnouncementData
err = common.JSONDecode(resp.Raw, &announcement)
if err != nil {
b.Websocket.DataHandler <- err
@@ -279,7 +281,70 @@ func (b *Bitmex) wsHandleIncomingData() {
}
b.Websocket.DataHandler <- announcement.Data
case bitmexWSAffiliate:
var response WsAffiliateResponse
err = common.JSONDecode(resp.Raw, &response)
if err != nil {
b.Websocket.DataHandler <- err
continue
}
b.Websocket.DataHandler <- response
case bitmexWSExecution:
var response WsExecutionResponse
err = common.JSONDecode(resp.Raw, &response)
if err != nil {
b.Websocket.DataHandler <- err
continue
}
b.Websocket.DataHandler <- response
case bitmexWSOrder:
var response WsOrderResponse
err = common.JSONDecode(resp.Raw, &response)
if err != nil {
b.Websocket.DataHandler <- err
continue
}
b.Websocket.DataHandler <- response
case bitmexWSMargin:
var response WsMarginResponse
err = common.JSONDecode(resp.Raw, &response)
if err != nil {
b.Websocket.DataHandler <- err
continue
}
b.Websocket.DataHandler <- response
case bitmexWSPosition:
var response WsPositionResponse
err = common.JSONDecode(resp.Raw, &response)
if err != nil {
b.Websocket.DataHandler <- err
continue
}
b.Websocket.DataHandler <- response
case bitmexWSPrivateNotifications:
var response WsPrivateNotificationsResponse
err = common.JSONDecode(resp.Raw, &response)
if err != nil {
b.Websocket.DataHandler <- err
continue
}
b.Websocket.DataHandler <- response
case bitmexWSTransact:
var response WsTransactResponse
err = common.JSONDecode(resp.Raw, &response)
if err != nil {
b.Websocket.DataHandler <- err
continue
}
b.Websocket.DataHandler <- response
case bitmexWSWallet:
var response WsWalletResponse
err = common.JSONDecode(resp.Raw, &response)
if err != nil {
b.Websocket.DataHandler <- err
continue
}
b.Websocket.DataHandler <- response
default:
b.Websocket.DataHandler <- fmt.Errorf("%s websocket error: Table unknown - %s",
b.Name, decodedResp.Table)
@@ -396,6 +461,47 @@ func (b *Bitmex) GenerateDefaultSubscriptions() {
Channel: bitmexWSAnnouncement,
},
}
for i := range channels {
for j := range contracts {
subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{
Channel: fmt.Sprintf("%v:%v", channels[i], contracts[j].String()),
Currency: contracts[j],
})
}
}
b.Websocket.SubscribeToChannels(subscriptions)
}
// GenerateAuthenticatedSubscriptions Adds authenticated subscriptions to websocket to be handled by ManageSubscriptions()
func (b *Bitmex) GenerateAuthenticatedSubscriptions() {
if !b.Websocket.CanUseAuthenticatedEndpoints() {
return
}
contracts := b.GetEnabledPairs(asset.PerpetualContract)
channels := []string{bitmexWSExecution,
bitmexWSPosition,
}
subscriptions := []exchange.WebsocketChannelSubscription{
{
Channel: bitmexWSAffiliate,
},
{
Channel: bitmexWSOrder,
},
{
Channel: bitmexWSMargin,
},
{
Channel: bitmexWSPrivateNotifications,
},
{
Channel: bitmexWSTransact,
},
{
Channel: bitmexWSWallet,
},
}
for i := range channels {
for j := range contracts {
subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{
@@ -427,19 +533,27 @@ func (b *Bitmex) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscri
// WebsocketSendAuth sends an authenticated subscription
func (b *Bitmex) websocketSendAuth() error {
if !b.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", b.Name)
}
b.Websocket.SetCanUseAuthenticatedEndpoints(true)
timestamp := time.Now().Add(time.Hour * 1).Unix()
newTimestamp := strconv.FormatInt(timestamp, 10)
hmac := crypto.GetHMAC(crypto.HashSHA256,
[]byte("GET/realtime"+newTimestamp),
[]byte(b.API.Credentials.Secret))
signature := crypto.HexEncodeToString(hmac)
var sendAuth WebsocketRequest
sendAuth.Command = "authKeyExpires"
sendAuth.Arguments = append(sendAuth.Arguments, b.API.Credentials.Key, timestamp,
signature)
return b.wsSend(sendAuth)
err := b.wsSend(sendAuth)
if err != nil {
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
return err
}
return nil
}
// WsSend sends data to the websocket server

View File

@@ -70,3 +70,260 @@ type AnnouncementData struct {
Data []Announcement `json:"data"`
Action string `json:"action"`
}
// WsAffiliateResponse private api response
type WsAffiliateResponse struct {
WsDataResponse
ForeignKeys interface{} `json:"foreignKeys"`
Attributes WsAffiliateResponseAttributes `json:"attributes"`
Filter WsAffiliateResponseFilter `json:"filter"`
Data []interface{} `json:"data"`
}
// WsAffiliateResponseAttributes private api data
type WsAffiliateResponseAttributes struct {
Account string `json:"account"`
Currency string `json:"currency"`
}
// WsAffiliateResponseFilter private api data
type WsAffiliateResponseFilter struct {
Account int64 `json:"account"`
}
// WsOrderResponse private api response
type WsOrderResponse struct {
WsDataResponse
ForeignKeys WsOrderResponseForeignKeys `json:"foreignKeys"`
Attributes WsOrderResponseAttributes `json:"attributes"`
Filter WsOrderResponseFilter `json:"filter"`
Data []interface{} `json:"data"`
}
// WsOrderResponseAttributes private api data
type WsOrderResponseAttributes struct {
OrderID string `json:"orderID"`
Account string `json:"account"`
OrdStatus string `json:"ordStatus"`
WorkingIndicator string `json:"workingIndicator"`
}
// WsOrderResponseFilter private api data
type WsOrderResponseFilter struct {
Account int64 `json:"account"`
}
// WsOrderResponseForeignKeys private api data
type WsOrderResponseForeignKeys struct {
Symbol string `json:"symbol"`
Side string `json:"side"`
OrdStatus string `json:"ordStatus"`
}
// WsTransactResponse private api response
type WsTransactResponse struct {
WsDataResponse
ForeignKeys interface{} `json:"foreignKeys"`
Attributes WsTransactResponseAttributes `json:"attributes"`
Filter WsTransactResponseFilter `json:"filter"`
Data []interface{} `json:"data"`
}
// WsTransactResponseAttributes private api data
type WsTransactResponseAttributes struct {
TransactID string `json:"transactID"`
TransactTime string `json:"transactTime"`
}
// WsTransactResponseFilter private api data
type WsTransactResponseFilter struct {
Account int64 `json:"account"`
}
// WsWalletResponse private api response
type WsWalletResponse struct {
WsDataResponse
ForeignKeys interface{} `json:"foreignKeys"`
Attributes WsWalletResponseAttributes `json:"attributes"`
Filter WsWalletResponseFilter `json:"filter"`
Data []WsWalletResponseData `json:"data"`
}
// WsWalletResponseAttributes private api data
type WsWalletResponseAttributes struct {
Account string `json:"account"`
Currency string `json:"currency"`
}
// WsWalletResponseData private api data
type WsWalletResponseData struct {
Account int64 `json:"account"`
Currency string `json:"currency"`
PrevDeposited float64 `json:"prevDeposited"`
PrevWithdrawn float64 `json:"prevWithdrawn"`
PrevTransferIn float64 `json:"prevTransferIn"`
PrevTransferOut float64 `json:"prevTransferOut"`
PrevAmount float64 `json:"prevAmount"`
PrevTimestamp string `json:"prevTimestamp"`
DeltaDeposited float64 `json:"deltaDeposited"`
DeltaWithdrawn float64 `json:"deltaWithdrawn"`
DeltaTransferIn float64 `json:"deltaTransferIn"`
DeltaTransferOut float64 `json:"deltaTransferOut"`
DeltaAmount float64 `json:"deltaAmount"`
Deposited float64 `json:"deposited"`
Withdrawn float64 `json:"withdrawn"`
TransferIn float64 `json:"transferIn"`
TransferOut float64 `json:"transferOut"`
Amount float64 `json:"amount"`
PendingCredit float64 `json:"pendingCredit"`
PendingDebit float64 `json:"pendingDebit"`
ConfirmedDebit int64 `json:"confirmedDebit"`
Timestamp string `json:"timestamp"`
Addr string `json:"addr"`
Script string `json:"script"`
WithdrawalLock []interface{} `json:"withdrawalLock"`
}
// WsWalletResponseFilter private api data
type WsWalletResponseFilter struct {
Account int64 `json:"account"`
}
// WsExecutionResponse private api response
type WsExecutionResponse struct {
WsDataResponse
ForeignKeys WsExecutionResponseForeignKeys `json:"foreignKeys"`
Attributes WsExecutionResponseAttributes `json:"attributes"`
Filter WsExecutionResponseFilter `json:"filter"`
Data []interface{} `json:"data"`
}
// WsExecutionResponseAttributes private api data
type WsExecutionResponseAttributes struct {
ExecID string `json:"execID"`
Account string `json:"account"`
ExecType string `json:"execType"`
TransactTime string `json:"transactTime"`
}
// WsExecutionResponseFilter private api data
type WsExecutionResponseFilter struct {
Account int64 `json:"account"`
Symbol string `json:"symbol"`
}
// WsExecutionResponseForeignKeys private api data
type WsExecutionResponseForeignKeys struct {
Symbol string `json:"symbol"`
Side string `json:"side"`
OrdStatus string `json:"ordStatus"`
}
// WsDataResponse contains common elements
type WsDataResponse struct {
Table string `json:"table"`
Action string `json:"action"`
Keys []string `json:"keys"`
Types map[string]string `json:"types"`
}
// WsMarginResponse private api response
type WsMarginResponse struct {
WsDataResponse
ForeignKeys interface{} `json:"foreignKeys"`
Attributes WsMarginResponseAttributes `json:"attributes"`
Filter WsMarginResponseFilter `json:"filter"`
Data []WsMarginResponseData `json:"data"`
}
// WsMarginResponseAttributes private api data
type WsMarginResponseAttributes struct {
Account string `json:"account"`
Currency string `json:"currency"`
}
// WsMarginResponseData private api data
type WsMarginResponseData struct {
Account int64 `json:"account"`
Currency string `json:"currency"`
RiskLimit float64 `json:"riskLimit"`
PrevState string `json:"prevState"`
State string `json:"state"`
Action string `json:"action"`
Amount float64 `json:"amount"`
PendingCredit float64 `json:"pendingCredit"`
PendingDebit float64 `json:"pendingDebit"`
ConfirmedDebit float64 `json:"confirmedDebit"`
PrevRealisedPnl float64 `json:"prevRealisedPnl"`
PrevUnrealisedPnl float64 `json:"prevUnrealisedPnl"`
GrossComm float64 `json:"grossComm"`
GrossOpenCost float64 `json:"grossOpenCost"`
GrossOpenPremium float64 `json:"grossOpenPremium"`
GrossExecCost float64 `json:"grossExecCost"`
GrossMarkValue float64 `json:"grossMarkValue"`
RiskValue float64 `json:"riskValue"`
TaxableMargin float64 `json:"taxableMargin"`
InitMargin float64 `json:"initMargin"`
MaintMargin float64 `json:"maintMargin"`
SessionMargin float64 `json:"sessionMargin"`
TargetExcessMargin float64 `json:"targetExcessMargin"`
VarMargin float64 `json:"varMargin"`
RealisedPnl float64 `json:"realisedPnl"`
UnrealisedPnl float64 `json:"unrealisedPnl"`
IndicativeTax float64 `json:"indicativeTax"`
UnrealisedProfit float64 `json:"unrealisedProfit"`
SyntheticMargin interface{} `json:"syntheticMargin"`
WalletBalance float64 `json:"walletBalance"`
MarginBalance float64 `json:"marginBalance"`
MarginBalancePcnt float64 `json:"marginBalancePcnt"`
MarginLeverage float64 `json:"marginLeverage"`
MarginUsedPcnt float64 `json:"marginUsedPcnt"`
ExcessMargin float64 `json:"excessMargin"`
ExcessMarginPcnt float64 `json:"excessMarginPcnt"`
AvailableMargin float64 `json:"availableMargin"`
WithdrawableMargin float64 `json:"withdrawableMargin"`
Timestamp string `json:"timestamp"`
GrossLastValue float64 `json:"grossLastValue"`
Commission interface{} `json:"commission"`
}
// WsMarginResponseFilter private api data
type WsMarginResponseFilter struct {
Account int64 `json:"account"`
}
// WsPositionResponse private api response
type WsPositionResponse struct {
WsDataResponse
ForeignKeys WsPositionResponseForeignKeys `json:"foreignKeys"`
Attributes WsPositionResponseAttributes `json:"attributes"`
Filter WsPositionResponseFilter `json:"filter"`
Data []interface{} `json:"data"`
}
// WsPositionResponseAttributes private api data
type WsPositionResponseAttributes struct {
Account string `json:"account"`
Symbol string `json:"symbol"`
Currency string `json:"currency"`
Underlying string `json:"underlying"`
QuoteCurrency string `json:"quoteCurrency"`
}
// WsPositionResponseFilter private api data
type WsPositionResponseFilter struct {
Account int64 `json:"account"`
Symbol string `json:"symbol"`
}
// WsPositionResponseForeignKeys private api data
type WsPositionResponseForeignKeys struct {
Symbol string `json:"symbol"`
}
// WsPrivateNotificationsResponse private api response
type WsPrivateNotificationsResponse struct {
Table string `json:"table"`
Action string `json:"action"`
Data []interface{} `json:"data"`
}

View File

@@ -587,3 +587,13 @@ func (b *Bitmex) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketCha
b.Websocket.UnsubscribeToChannels(channels)
return nil
}
// GetSubscriptions returns a copied list of subscriptions
func (b *Bitmex) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return b.Websocket.GetSubscriptions(), nil
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (b *Bitmex) AuthenticateWebsocket() error {
return b.websocketSendAuth()
}

View File

@@ -154,7 +154,7 @@ func (b *Bitstamp) WsHandleData() {
func (b *Bitstamp) generateDefaultSubscriptions() {
var channels = []string{"live_trades_", "diff_order_book_"}
enabledCurrencies := b.GetEnabledPairs(asset.Spot)
subscriptions := []exchange.WebsocketChannelSubscription{}
var subscriptions []exchange.WebsocketChannelSubscription
for i := range channels {
for j := range enabledCurrencies {
subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{

View File

@@ -535,3 +535,13 @@ func (b *Bitstamp) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketC
b.Websocket.UnsubscribeToChannels(channels)
return nil
}
// GetSubscriptions returns a copied list of subscriptions
func (b *Bitstamp) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return b.Websocket.GetSubscriptions(), nil
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (b *Bitstamp) AuthenticateWebsocket() error {
return common.ErrFunctionNotSupported
}

View File

@@ -507,3 +507,13 @@ func (b *Bittrex) SubscribeToWebsocketChannels(channels []exchange.WebsocketChan
func (b *Bittrex) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error {
return common.ErrFunctionNotSupported
}
// GetSubscriptions returns a copied list of subscriptions
func (b *Bittrex) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return nil, common.ErrFunctionNotSupported
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (b *Bittrex) AuthenticateWebsocket() error {
return common.ErrFunctionNotSupported
}

View File

@@ -577,3 +577,13 @@ func (b *BTCMarkets) SubscribeToWebsocketChannels(channels []exchange.WebsocketC
func (b *BTCMarkets) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error {
return common.ErrFunctionNotSupported
}
// GetSubscriptions returns a copied list of subscriptions
func (b *BTCMarkets) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return nil, common.ErrFunctionNotSupported
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (b *BTCMarkets) AuthenticateWebsocket() error {
return common.ErrFunctionNotSupported
}

View File

@@ -207,7 +207,7 @@ func (b *BTSE) wsProcessSnapshot(snapshot *websocketOrderbookSnapshot) error {
func (b *BTSE) GenerateDefaultSubscriptions() {
var channels = []string{"snapshot", "ticker"}
enabledCurrencies := b.GetEnabledPairs(asset.Spot)
subscriptions := []exchange.WebsocketChannelSubscription{}
var subscriptions []exchange.WebsocketChannelSubscription
for i := range channels {
for j := range enabledCurrencies {
subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{

View File

@@ -499,3 +499,13 @@ func (b *BTSE) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChann
b.Websocket.UnsubscribeToChannels(channels)
return nil
}
// GetSubscriptions returns a copied list of subscriptions
func (b *BTSE) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return b.Websocket.GetSubscriptions(), nil
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (b *BTSE) AuthenticateWebsocket() error {
return common.ErrFunctionNotSupported
}

View File

@@ -735,7 +735,7 @@ func (c *CoinbasePro) SendAuthenticatedHTTPRequest(method, path string, params m
}
}
n := c.Requester.GetNonce(true).String()
n := c.Requester.GetNonce(false).String()
message := n + method + "/" + path + string(payload)
hmac := crypto.GetHMAC(crypto.HashSHA256, []byte(message), []byte(c.API.Credentials.Secret))
headers := make(map[string]string)

View File

@@ -1,12 +1,15 @@
package coinbasepro
import (
"net/http"
"testing"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-/gocryptotrader/config"
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/sharedtestvalues"
)
var c CoinbasePro
@@ -33,7 +36,9 @@ func TestSetup(t *testing.T) {
}
gdxConfig.API.Credentials.Key = apiKey
gdxConfig.API.Credentials.Secret = apiSecret
gdxConfig.API.Credentials.ClientID = clientID
gdxConfig.API.AuthenticatedSupport = true
gdxConfig.API.AuthenticatedWebsocketSupport = true
c.Setup(gdxConfig)
}
@@ -87,139 +92,85 @@ func TestGetServerTime(t *testing.T) {
}
func TestAuthRequests(t *testing.T) {
if c.ValidateAPICredentials() {
_, err := c.GetAccounts()
if err == nil {
t.Error("Test failed - GetAccounts() error", err)
}
_, err = c.GetAccount("234cb213-ac6f-4ed8-b7b6-e62512930945")
if err == nil {
t.Error("Test failed - GetAccount() error", err)
}
_, err = c.GetAccountHistory("234cb213-ac6f-4ed8-b7b6-e62512930945")
if err == nil {
t.Error("Test failed - GetAccountHistory() error", err)
}
_, err = c.GetHolds("234cb213-ac6f-4ed8-b7b6-e62512930945")
if err == nil {
t.Error("Test failed - GetHolds() error", err)
}
_, err = c.PlaceLimitOrder("", 0, 0, exchange.BuyOrderSide.ToLower().ToString(),
"", "", "BTC-USD", "", false)
if err == nil {
t.Error("Test failed - PlaceLimitOrder() error", err)
}
_, err = c.PlaceMarketOrder("", 1, 0, exchange.BuyOrderSide.ToLower().ToString(),
"BTC-USD", "")
if err == nil {
t.Error("Test failed - PlaceMarketOrder() error", err)
}
err = c.CancelExistingOrder("1337")
if err == nil {
t.Error("Test failed - CancelExistingOrder() error", err)
}
_, err = c.CancelAllExistingOrders("BTC-USD")
if err == nil {
t.Error("Test failed - CancelAllExistingOrders() error", err)
}
_, err = c.GetOrders([]string{"open", "done"}, "BTC-USD")
if err == nil {
t.Error("Test failed - GetOrders() error", err)
}
_, err = c.GetOrder("1337")
if err == nil {
t.Error("Test failed - GetOrders() error", err)
}
_, err = c.GetFills("1337", "BTC-USD")
if err == nil {
t.Error("Test failed - GetFills() error", err)
}
_, err = c.GetFills("", "")
if err == nil {
t.Error("Test failed - GetFills() error", err)
}
_, err = c.GetFundingRecords("rejected")
if err == nil {
t.Error("Test failed - GetFundingRecords() error", err)
}
// _, err := c.RepayFunding("1", "BTC")
// if err != nil {
// t.Error("Test failed - RepayFunding() error", err)
// }
_, err = c.MarginTransfer(1, "withdraw", "45fa9e3b-00ba-4631-b907-8a98cbdf21be", "BTC")
if err == nil {
t.Error("Test failed - MarginTransfer() error", err)
}
_, err = c.GetPosition()
if err == nil {
t.Error("Test failed - GetPosition() error", err)
}
_, err = c.ClosePosition(false)
if err == nil {
t.Error("Test failed - ClosePosition() error", err)
}
_, err = c.GetPayMethods()
if err == nil {
t.Error("Test failed - GetPayMethods() error", err)
}
_, err = c.DepositViaPaymentMethod(1, "BTC", "1337")
if err == nil {
t.Error("Test failed - DepositViaPaymentMethod() error", err)
}
_, err = c.DepositViaCoinbase(1, "BTC", "1337")
if err == nil {
t.Error("Test failed - DepositViaCoinbase() error", err)
}
_, err = c.WithdrawViaPaymentMethod(1, "BTC", "1337")
if err == nil {
t.Error("Test failed - WithdrawViaPaymentMethod() error", err)
}
// _, err := c.WithdrawViaCoinbase(1, "BTC", "c13cd0fc-72ca-55e9-843b-b84ef628c198")
// if err != nil {
// t.Error("Test failed - WithdrawViaCoinbase() error", err)
// }
_, err = c.WithdrawCrypto(1, "BTC", "1337")
if err == nil {
t.Error("Test failed - WithdrawViaCoinbase() error", err)
}
_, err = c.GetCoinbaseAccounts()
if err == nil {
t.Error("Test failed - GetCoinbaseAccounts() error", err)
}
_, err = c.GetReportStatus("1337")
if err == nil {
t.Error("Test failed - GetReportStatus() error", err)
}
_, err = c.GetTrailingVolume()
if err == nil {
t.Error("Test failed - GetTrailingVolume() error", err)
}
if !areTestAPIKeysSet() {
t.Skip("API keys not set, skipping test")
}
_, err := c.GetAccounts()
if err != nil {
t.Error("Test failed - GetAccounts() error", err)
}
accountResponse, err := c.GetAccount("13371337-1337-1337-1337-133713371337")
if accountResponse.ID != "" {
t.Error("Expecting no data returned")
}
if err == nil {
t.Error("Expecting error")
}
accountHistoryResponse, err := c.GetAccountHistory("13371337-1337-1337-1337-133713371337")
if len(accountHistoryResponse) > 0 {
t.Error("Expecting no data returned")
}
if err == nil {
t.Error("Expecting error")
}
getHoldsResponse, err := c.GetHolds("13371337-1337-1337-1337-133713371337")
if len(getHoldsResponse) > 0 {
t.Error("Expecting no data returned")
}
if err == nil {
t.Error("Expecting error")
}
orderResponse, err := c.PlaceLimitOrder("", 0.001, 0.001, "buy", "", "", "BTC-USD", "", false)
if orderResponse != "" {
t.Error("Expecting no data returned")
}
if err == nil {
t.Error("Expecting error")
}
marketOrderResponse, err := c.PlaceMarketOrder("", 1, 0, "buy", "BTC-USD", "")
if marketOrderResponse != "" {
t.Error("Expecting no data returned")
}
if err == nil {
t.Error("Expecting error")
}
fillsResponse, err := c.GetFills("1337", "BTC-USD")
if len(fillsResponse) > 0 {
t.Error("Expecting no data returned")
}
if err == nil {
t.Error("Expecting error")
}
_, err = c.GetFills("", "")
if err == nil {
t.Error("Expecting error")
}
_, err = c.GetFundingRecords("rejected")
if err == nil {
t.Error("Expecting error")
}
marginTransferResponse, err := c.MarginTransfer(1, "withdraw", "13371337-1337-1337-1337-133713371337", "BTC")
if marginTransferResponse.ID != "" {
t.Error("Expecting no data returned")
}
if err == nil {
t.Error("Expecting error")
}
_, err = c.GetPosition()
if err == nil {
t.Error("Expecting error")
}
_, err = c.ClosePosition(false)
if err == nil {
t.Error("Expecting error")
}
_, err = c.GetPayMethods()
if err != nil {
t.Error("Test failed - GetPayMethods() error", err)
}
_, err = c.GetCoinbaseAccounts()
if err != nil {
t.Error("Test failed - GetCoinbaseAccounts() error", err)
}
}
@@ -637,3 +588,37 @@ func TestGetDepositAddress(t *testing.T) {
t.Error("Test Failed - GetDepositAddress() error", err)
}
}
// TestWsAuth dials websocket, sends login request.
func TestWsAuth(t *testing.T) {
c.SetDefaults()
TestSetup(t)
if !c.Websocket.IsEnabled() && !c.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() {
t.Skip(exchange.WebsocketNotEnabled)
}
var err error
var dialer websocket.Dialer
c.WebsocketConn, _, err = dialer.Dial(c.Websocket.GetWebsocketURL(),
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{
Channel: "user",
Currency: currency.NewPairFromString("BTC-USD"),
})
if err != nil {
t.Error(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case badResponse := <-c.Websocket.DataHandler:
t.Error(badResponse)
case <-timer.C:
}
timer.Stop()
}

View File

@@ -343,9 +343,13 @@ type FillResponse struct {
// WebsocketSubscribe takes in subscription information
type WebsocketSubscribe struct {
Type string `json:"type"`
ProductID string `json:"product_id,omitempty"`
Channels []WsChannels `json:"channels,omitempty"`
Type string `json:"type"`
ProductID string `json:"product_id,omitempty"`
Channels []WsChannels `json:"channels,omitempty"`
Signature string `json:"signature,omitempty"`
Key string `json:"key,omitempty"`
Passphrase string `json:"passphrase,omitempty"`
Timestamp string `json:"timestamp,omitempty"`
}
// WsChannels defines outgoing channels for subscription purposes
@@ -360,7 +364,8 @@ type WebsocketReceived struct {
OrderID string `json:"order_id"`
OrderType string `json:"order_type"`
Size float64 `json:"size,string"`
Price float64 `json:"price,string"`
Price float64 `json:"price,omitempty,string"`
Funds float64 `json:"funds,omitempty,string"`
Side string `json:"side"`
ClientOID string `json:"client_oid"`
ProductID string `json:"product_id"`
@@ -462,3 +467,20 @@ type WebsocketL2Update struct {
Time string `json:"time"`
Changes [][]interface{} `json:"changes"`
}
// WebsocketActivate an activate message is sent when a stop order is placed
type WebsocketActivate struct {
Type string `json:"type"`
ProductID string `json:"product_id"`
Timestamp string `json:"timestamp"`
UserID string `json:"user_id"`
ProfileID string `json:"profile_id"`
OrderID string `json:"order_id"`
StopType string `json:"stop_type"`
Side string `json:"side"`
StopPrice float64 `json:"stop_price,string"`
Size float64 `json:"size,string"`
Funds float64 `json:"funds,string"`
TakerFeeRate float64 `json:"taker_fee_rate,string"`
Private bool `json:"private"`
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/gorilla/websocket"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/common/crypto"
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/asset"
@@ -149,6 +150,51 @@ func (c *CoinbasePro) WsHandleData() {
c.Websocket.DataHandler <- err
continue
}
case "received":
// We currently use l2update to calculate orderbook changes
received := WebsocketReceived{}
err := common.JSONDecode(resp.Raw, &received)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
c.Websocket.DataHandler <- received
case "open":
// We currently use l2update to calculate orderbook changes
open := WebsocketOpen{}
err := common.JSONDecode(resp.Raw, &open)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
c.Websocket.DataHandler <- open
case "done":
// We currently use l2update to calculate orderbook changes
done := WebsocketDone{}
err := common.JSONDecode(resp.Raw, &done)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
c.Websocket.DataHandler <- done
case "change":
// We currently use l2update to calculate orderbook changes
change := WebsocketChange{}
err := common.JSONDecode(resp.Raw, &change)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
c.Websocket.DataHandler <- change
case "activate":
// We currently use l2update to calculate orderbook changes
activate := WebsocketActivate{}
err := common.JSONDecode(resp.Raw, &activate)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
c.Websocket.DataHandler <- activate
}
}
}
@@ -243,10 +289,13 @@ func (c *CoinbasePro) ProcessUpdate(update WebsocketL2Update) error {
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (c *CoinbasePro) GenerateDefaultSubscriptions() {
var channels = []string{"heartbeat", "level2", "ticker"}
var channels = []string{"heartbeat", "level2", "ticker", "user"}
enabledCurrencies := c.GetEnabledPairs(asset.Spot)
subscriptions := []exchange.WebsocketChannelSubscription{}
var subscriptions []exchange.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{
@@ -271,6 +320,16 @@ func (c *CoinbasePro) Subscribe(channelToSubscribe exchange.WebsocketChannelSubs
},
},
}
if channelToSubscribe.Channel == "user" || channelToSubscribe.Channel == "full" {
n := fmt.Sprintf("%v", time.Now().Unix())
message := n + "GET" + "/users/self/verify"
hmac := crypto.GetHMAC(crypto.HashSHA256, []byte(message),
[]byte(c.API.Credentials.Secret))
subscribe.Signature = crypto.Base64Encode(hmac)
subscribe.Key = c.API.Credentials.Key
subscribe.Passphrase = c.API.Credentials.ClientID
subscribe.Timestamp = n
}
return c.wsSend(subscribe)
}

View File

@@ -512,3 +512,13 @@ func (c *CoinbasePro) UnsubscribeToWebsocketChannels(channels []exchange.Websock
c.Websocket.UnsubscribeToChannels(channels)
return nil
}
// GetSubscriptions returns a copied list of subscriptions
func (c *CoinbasePro) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return c.Websocket.GetSubscriptions(), nil
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (c *CoinbasePro) AuthenticateWebsocket() error {
return common.ErrFunctionNotSupported
}

View File

@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"net/http"
"strings"
"sync"
"github.com/gorilla/websocket"
@@ -13,6 +14,7 @@ import (
"github.com/thrasher-/gocryptotrader/common/crypto"
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/asset"
log "github.com/thrasher-/gocryptotrader/logger"
)
@@ -53,7 +55,7 @@ type COINUT struct {
func (c *COINUT) GetInstruments() (Instruments, error) {
var result Instruments
params := make(map[string]interface{})
params["sec_type"] = "SPOT"
params["sec_type"] = strings.ToUpper(asset.Spot.String())
return result, c.SendHTTPRequest(coinutInstruments, params, false, &result)
}

View File

@@ -1,15 +1,20 @@
package coinut
import (
"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"
)
var c COINUT
var wsSetupRan bool
// Please supply your own keys here to do better tests
const (
@@ -30,6 +35,7 @@ func TestSetup(t *testing.T) {
t.Error("Test Failed - Coinut Setup() init error")
}
bConfig.API.AuthenticatedSupport = true
bConfig.API.AuthenticatedWebsocketSupport = true
bConfig.API.Credentials.Key = apiKey
bConfig.API.Credentials.ClientID = clientID
bConfig.Verbose = true
@@ -41,6 +47,46 @@ func TestSetup(t *testing.T) {
}
}
func setupWSTestAuth(t *testing.T) {
if wsSetupRan {
return
}
c.SetDefaults()
TestSetup(t)
if !c.Websocket.IsEnabled() && !c.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() {
t.Skip(exchange.WebsocketNotEnabled)
}
var err error
var dialer websocket.Dialer
c.WebsocketConn, _, err = dialer.Dial(c.Websocket.GetWebsocketURL(),
http.Header{})
if err != nil {
t.Fatal(err)
}
c.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
c.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride()
go c.WsHandleData()
err = c.wsAuthenticate()
if err != nil {
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
}
func TestGetInstruments(t *testing.T) {
_, err := c.GetInstruments()
if err != nil {
@@ -403,3 +449,101 @@ func TestGetDepositAddress(t *testing.T) {
t.Error("Test Failed - GetDepositAddress() function unsupported cannot be nil")
}
}
// TestWsAuthGetAccountBalance dials websocket, sends login request.
func TestWsAuthGetAccountBalance(t *testing.T) {
setupWSTestAuth(t)
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) {
setupWSTestAuth(t)
order := WsSubmitOrderParameters{
Amount: 1,
Currency: currency.NewPair(currency.LTC, currency.BTC),
OrderID: 1,
Price: 1,
Side: exchange.BuyOrderSide,
}
err := c.wsSubmitOrders([]WsSubmitOrderParameters{order, 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.
func TestWsAuthCancelOrders(t *testing.T) {
setupWSTestAuth(t)
order := WsCancelOrderParameters{
Currency: currency.NewPair(currency.LTC, currency.BTC),
OrderID: 1,
}
err := c.wsCancelOrders([]WsCancelOrderParameters{order, 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()
}
// TestWsAuthCancelOrder dials websocket, sends login request.
func TestWsAuthCancelOrder(t *testing.T) {
setupWSTestAuth(t)
order := WsCancelOrderParameters{
Currency: currency.NewPair(currency.LTC, currency.BTC),
OrderID: 1,
}
err := c.wsCancelOrder(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()
}
// TestWsAuthGetOpenOrders dials websocket, sends login request.
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()
}

View File

@@ -1,5 +1,10 @@
package coinut
import (
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
)
// GenericResponse is the generic response you will get from coinut
type GenericResponse struct {
Nonce int64 `json:"nonce"`
@@ -111,8 +116,8 @@ type OrderResponse struct {
// Commission holds trade commission structure
type Commission struct {
Currency string `json:"currency"`
Amount float64 `json:"amount,string"`
Currency currency.Pair `json:"currency"`
Amount float64 `json:"amount,string"`
}
// OrderFilledResponse contains order filled response
@@ -362,3 +367,248 @@ type WsSupportedCurrency struct {
DecimalPlaces int64 `json:"decimal_places"`
Quote string `json:"quote"`
}
// WsRequest base request
type WsRequest struct {
Request string `json:"request"`
Nonce int64 `json:"nonce"`
}
// WsTradeHistoryRequest ws request
type WsTradeHistoryRequest struct {
InstID int64 `json:"inst_id"`
Start int64 `json:"start,omitempty"`
Limit int64 `json:"limit,omitempty"`
WsRequest
}
// WsCancelOrdersRequest ws request
type WsCancelOrdersRequest struct {
Entries []WsCancelOrdersRequestEntry `json:"entries"`
WsRequest
}
// WsCancelOrdersRequestEntry ws request entry
type WsCancelOrdersRequestEntry struct {
InstID int64 `json:"inst_id"`
OrderID int64 `json:"order_id"`
}
// WsCancelOrderParameters ws request parameters
type WsCancelOrderParameters struct {
Currency currency.Pair
OrderID int64
}
// WsCancelOrderRequest ws request
type WsCancelOrderRequest struct {
InstID int64 `json:"inst_id"`
OrderID int64 `json:"order_id"`
WsRequest
}
// WsCancelOrderResponse ws response
type WsCancelOrderResponse struct {
Nonce int64 `json:"nonce"`
Reply string `json:"reply"`
OrderID int64 `json:"order_id"`
ClientOrdID int64 `json:"client_ord_id"`
Status []string `json:"status"`
}
// WsCancelOrdersResponse ws response
type WsCancelOrdersResponse struct {
WsRequest
Entries []WsCancelOrdersResponseEntry `json:"entries"`
}
// WsCancelOrdersResponseEntry ws response entry
type WsCancelOrdersResponseEntry struct {
InstID int64 `json:"inst_id"`
OrderID int64 `json:"order_id"`
}
// WsGetOpenOrdersRequest ws request
type WsGetOpenOrdersRequest struct {
InstID int64 `json:"inst_id"`
WsRequest
}
// WsSubmitOrdersRequest ws request
type WsSubmitOrdersRequest struct {
Orders []WsSubmitOrdersRequestData `json:"orders"`
WsRequest
}
// WsSubmitOrdersRequestData ws request data
type WsSubmitOrdersRequestData struct {
InstID int64 `json:"inst_id"`
Price float64 `json:"price,string"`
Qty float64 `json:"qty,string"`
ClientOrdID int `json:"client_ord_id"`
Side string `json:"side"`
}
// WsSubmitOrderRequest ws request
type WsSubmitOrderRequest struct {
InstID int64 `json:"inst_id"`
Price float64 `json:"price,string"`
Qty float64 `json:"qty,string"`
OrderID int64 `json:"client_ord_id"`
Side string `json:"side"`
WsRequest
}
// WsSubmitOrderParameters ws request parameters
type WsSubmitOrderParameters struct {
Currency currency.Pair
Side exchange.OrderSide
Amount, Price float64
OrderID int64
}
// WsUserBalanceResponse ws response
type WsUserBalanceResponse struct {
Nonce int64 `json:"nonce"`
Status []string `json:"status"`
Btc float64 `json:"BTC,string"`
Ltc float64 `json:"LTC,string"`
Etc float64 `json:"ETC,string"`
Eth float64 `json:"ETH,string"`
FloatingPl float64 `json:"floating_pl,string"`
InitialMargin float64 `json:"initial_margin,string"`
RealizedPl float64 `json:"realized_pl,string"`
MaintenanceMargin float64 `json:"maintenance_margin,string"`
Equity float64 `json:"equity,string"`
Reply string `json:"reply"`
TransID int64 `json:"trans_id"`
}
// WsOrderAcceptedResponse ws response
type WsOrderAcceptedResponse struct {
Nonce int64 `json:"nonce"`
Status []string `json:"status"`
OrderID int64 `json:"order_id"`
OpenQty float64 `json:"open_qty,string"`
InstID int64 `json:"inst_id"`
Qty float64 `json:"qty,string"`
ClientOrdID int64 `json:"client_ord_id"`
OrderPrice float64 `json:"order_price,string"`
Reply string `json:"reply"`
Side string `json:"side"`
TransID int64 `json:"trans_id"`
}
// WsOrderFilledResponse ws response
type WsOrderFilledResponse struct {
Commission WsOrderFilledCommissionData `json:"commission"`
FillPrice float64 `json:"fill_price,string"`
FillQty float64 `json:"fill_qty,string"`
Nonce int64 `json:"nonce"`
Order WsOrderData `json:"order"`
Reply string `json:"reply"`
Status []string `json:"status"`
Timestamp int64 `json:"timestamp"`
TransID int64 `json:"trans_id"`
}
// WsOrderData ws response data
type WsOrderData struct {
ClientOrdID int64 `json:"client_ord_id"`
InstID int64 `json:"inst_id"`
OpenQty float64 `json:"open_qty,string"`
OrderID int64 `json:"order_id"`
Price float64 `json:"price,string"`
Qty float64 `json:"qty,string"`
Side string `json:"side"`
Timestamp int64 `json:"timestamp"`
}
// WsOrderFilledCommissionData ws response data
type WsOrderFilledCommissionData struct {
Amount float64 `json:"amount,string"`
Currency currency.Pair `json:"currency"`
}
// WsOrderRejectedResponse ws response
type WsOrderRejectedResponse struct {
Nonce int64 `json:"nonce"`
Status []string `json:"status"`
OrderID int64 `json:"order_id"`
OpenQty float64 `json:"open_qty,string"`
Price float64 `json:"price,string"`
InstID int64 `json:"inst_id"`
Reasons []string `json:"reasons"`
ClientOrdID int64 `json:"client_ord_id"`
Timestamp int64 `json:"timestamp"`
Reply string `json:"reply"`
Qty float64 `json:"qty,string"`
Side string `json:"side"`
TransID int64 `json:"trans_id"`
}
// WsUserOpenOrdersResponse ws response
type WsUserOpenOrdersResponse struct {
Nonce int64 `json:"nonce"`
Reply string `json:"reply"`
Status []string `json:"status"`
Orders []WsOrderData `json:"orders"`
}
// WsTradeHistoryResponse ws response
type WsTradeHistoryResponse struct {
Nonce int64 `json:"nonce"`
Reply string `json:"reply"`
Status []string `json:"status"`
TotalNumber int64 `json:"total_number"`
Trades []WsOrderData `json:"trades"`
}
// WsTradeHistoryCommissionData ws response data
type WsTradeHistoryCommissionData struct {
Amount float64 `json:"amount,string"`
Currency currency.Pair `json:"currency"`
}
// WsTradeHistoryTradeData ws response data
type WsTradeHistoryTradeData struct {
Commission WsTradeHistoryCommissionData `json:"commission"`
Order WsOrderData `json:"order"`
FillPrice float64 `json:"fill_price,string"`
FillQty float64 `json:"fill_qty,string"`
Timestamp int64 `json:"timestamp"`
TransID int64 `json:"trans_id"`
}
// WsLoginResponse ws response data
type WsLoginResponse struct {
APIKey string `json:"api_key"`
Country string `json:"country"`
DepositEnabled bool `json:"deposit_enabled"`
Deposited bool `json:"deposited"`
Email string `json:"email"`
FailedTimes int64 `json:"failed_times"`
KycPassed bool `json:"kyc_passed"`
Lang string `json:"lang"`
Nonce int64 `json:"nonce"`
OtpEnabled bool `json:"otp_enabled"`
PhoneNumber string `json:"phone_number"`
ProductsEnabled []string `json:"products_enabled"`
Referred bool `json:"referred"`
Reply string `json:"reply"`
SessionID string `json:"session_id"`
Status []string `json:"status"`
Timezone string `json:"timezone"`
Traded bool `json:"traded"`
UnverifiedEmail string `json:"unverified_email"`
Username string `json:"username"`
WithdrawEnabled bool `json:"withdraw_enabled"`
}
// WsNewOrderResponse returns if new_order response failes
type WsNewOrderResponse struct {
Msg string `json:"msg"`
Nonce int64 `json:"nonce"`
Reply string `json:"reply"`
Status []string `json:"status"`
}

View File

@@ -2,12 +2,15 @@ package coinut
import (
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/common/crypto"
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/asset"
@@ -29,142 +32,6 @@ var populatedList bool
// wss://wsapi-na.coinut.com
// wss://wsapi-eu.coinut.com
// 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)
defer func() {
c.Websocket.Wg.Done()
}()
for {
select {
case <-c.Websocket.ShutdownC:
return
default:
resp, err := c.WsReadData()
if err != nil {
c.Websocket.DataHandler <- err
return
}
var incoming wsResponse
err = common.JSONDecode(resp.Raw, &incoming)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
switch incoming.Reply {
case "hb":
channels["hb"] <- resp.Raw
case "inst_tick":
var ticker WsTicker
err := common.JSONDecode(resp.Raw, &ticker)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
currencyPair := instrumentListByCode[ticker.InstID]
c.Websocket.DataHandler <- exchange.TickerData{
Timestamp: time.Unix(0, ticker.Timestamp),
Pair: currency.NewPairFromString(currencyPair),
Exchange: c.GetName(),
AssetType: asset.Spot,
HighPrice: ticker.HighestBuy,
LowPrice: ticker.LowestSell,
ClosePrice: ticker.Last,
Quantity: ticker.Volume,
}
case "inst_order_book":
var orderbooksnapshot WsOrderbookSnapshot
err := common.JSONDecode(resp.Raw, &orderbooksnapshot)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
err = c.WsProcessOrderbookSnapshot(&orderbooksnapshot)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
currencyPair := instrumentListByCode[orderbooksnapshot.InstID]
c.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
Exchange: c.GetName(),
Asset: asset.Spot,
Pair: currency.NewPairFromString(currencyPair),
}
case "inst_order_book_update":
var orderbookUpdate WsOrderbookUpdate
err := common.JSONDecode(resp.Raw, &orderbookUpdate)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
err = c.WsProcessOrderbookUpdate(&orderbookUpdate)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
currencyPair := instrumentListByCode[orderbookUpdate.InstID]
c.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
Exchange: c.GetName(),
Asset: asset.Spot,
Pair: currency.NewPairFromString(currencyPair),
}
case "inst_trade":
var tradeSnap WsTradeSnapshot
err := common.JSONDecode(resp.Raw, &tradeSnap)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
case "inst_trade_update":
var tradeUpdate WsTradeUpdate
err := common.JSONDecode(resp.Raw, &tradeUpdate)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
currencyPair := instrumentListByCode[tradeUpdate.InstID]
c.Websocket.DataHandler <- exchange.TradeData{
Timestamp: time.Unix(tradeUpdate.Timestamp, 0),
CurrencyPair: currency.NewPairFromString(currencyPair),
AssetType: asset.Spot,
Exchange: c.GetName(),
Price: tradeUpdate.Price,
Side: tradeUpdate.Side,
}
}
}
}
}
// WsConnect intiates a websocket connection
func (c *COINUT) WsConnect() error {
if !c.Websocket.IsEnabled() || !c.IsEnabled() {
@@ -200,7 +67,7 @@ func (c *COINUT) WsConnect() error {
}
populatedList = true
}
c.wsAuthenticate()
c.GenerateDefaultSubscriptions()
// define bi-directional communication
@@ -208,10 +75,244 @@ func (c *COINUT) WsConnect() error {
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)
defer func() {
c.Websocket.Wg.Done()
}()
for {
select {
case <-c.Websocket.ShutdownC:
return
default:
resp, err := c.WsReadData()
if err != nil {
c.Websocket.DataHandler <- err
return
}
if strings.HasPrefix(string(resp.Raw), "[") {
var incoming []wsResponse
err = common.JSONDecode(resp.Raw, &incoming)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
for i := range incoming {
var individualJSON []byte
individualJSON, err = common.JSONEncode(incoming[i])
if err != nil {
c.Websocket.DataHandler <- err
continue
}
c.wsProcessResponse(individualJSON)
}
} else {
var incoming wsResponse
err = common.JSONDecode(resp.Raw, &incoming)
if err != nil {
c.Websocket.DataHandler <- err
continue
}
c.wsProcessResponse(resp.Raw)
}
}
}
}
func (c *COINUT) wsProcessResponse(resp []byte) {
var incoming wsResponse
err := common.JSONDecode(resp, &incoming)
if err != nil {
c.Websocket.DataHandler <- err
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.API.Credentials.ClientID)
c.Websocket.DataHandler <- login
case "hb":
channels["hb"] <- resp
case "inst_tick":
var ticker WsTicker
err := common.JSONDecode(resp, &ticker)
if err != nil {
c.Websocket.DataHandler <- err
return
}
currencyPair := instrumentListByCode[ticker.InstID]
c.Websocket.DataHandler <- exchange.TickerData{
Timestamp: time.Unix(0, ticker.Timestamp),
Pair: currency.NewPairFromString(currencyPair),
Exchange: c.GetName(),
AssetType: asset.Spot,
HighPrice: ticker.HighestBuy,
LowPrice: ticker.LowestSell,
ClosePrice: ticker.Last,
Quantity: ticker.Volume,
}
case "inst_order_book":
var orderbooksnapshot WsOrderbookSnapshot
err := common.JSONDecode(resp, &orderbooksnapshot)
if err != nil {
c.Websocket.DataHandler <- err
return
}
err = c.WsProcessOrderbookSnapshot(&orderbooksnapshot)
if err != nil {
c.Websocket.DataHandler <- err
return
}
currencyPair := instrumentListByCode[orderbooksnapshot.InstID]
c.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
Exchange: c.GetName(),
Asset: asset.Spot,
Pair: currency.NewPairFromString(currencyPair),
}
case "inst_order_book_update":
var orderbookUpdate WsOrderbookUpdate
err := common.JSONDecode(resp, &orderbookUpdate)
if err != nil {
c.Websocket.DataHandler <- err
return
}
err = c.WsProcessOrderbookUpdate(&orderbookUpdate)
if err != nil {
c.Websocket.DataHandler <- err
return
}
currencyPair := instrumentListByCode[orderbookUpdate.InstID]
c.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{
Exchange: c.GetName(),
Asset: asset.Spot,
Pair: currency.NewPairFromString(currencyPair),
}
case "inst_trade":
var tradeSnap WsTradeSnapshot
err := common.JSONDecode(resp, &tradeSnap)
if err != nil {
c.Websocket.DataHandler <- err
return
}
case "inst_trade_update":
var tradeUpdate WsTradeUpdate
err := common.JSONDecode(resp, &tradeUpdate)
if err != nil {
c.Websocket.DataHandler <- err
return
}
currencyPair := instrumentListByCode[tradeUpdate.InstID]
c.Websocket.DataHandler <- exchange.TradeData{
Timestamp: time.Unix(tradeUpdate.Timestamp, 0),
CurrencyPair: currency.NewPairFromString(currencyPair),
AssetType: asset.Spot,
Exchange: c.GetName(),
Price: tradeUpdate.Price,
Side: tradeUpdate.Side,
}
case "user_balance":
var userBalance WsUserBalanceResponse
err := common.JSONDecode(resp, &userBalance)
if err != nil {
c.Websocket.DataHandler <- err
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
}
}
// GetNonce returns a nonce for a required request
func (c *COINUT) GetNonce() int64 {
if c.Nonce.Get() == 0 {
@@ -227,7 +328,7 @@ func (c *COINUT) GetNonce() int64 {
func (c *COINUT) WsSetInstrumentList() error {
err := c.wsSend(wsRequest{
Request: "inst_list",
SecType: "SPOT",
SecType: strings.ToUpper(asset.Spot.String()),
Nonce: c.GetNonce(),
})
if err != nil {
@@ -313,7 +414,7 @@ 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"}
subscriptions := []exchange.WebsocketChannelSubscription{}
var subscriptions []exchange.WebsocketChannelSubscription
enabledCurrencies := c.GetEnabledPairs(asset.Spot)
for i := range channels {
for j := range enabledCurrencies {
@@ -364,3 +465,146 @@ func (c *COINUT) wsSend(data interface{}) error {
time.Sleep(coinutWebsocketRateLimit)
return c.WebsocketConn.WriteMessage(websocket.TextMessage, json)
}
func (c *COINUT) wsAuthenticate() error {
if !c.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", c.Name)
}
timestamp := time.Now().Unix()
nonce := c.GetNonce()
payload := fmt.Sprintf("%v|%v|%v", c.API.Credentials.ClientID, timestamp, nonce)
hmac := crypto.GetHMAC(crypto.HashSHA256, []byte(payload), []byte(c.API.Credentials.Key))
loginRequest := struct {
Request string `json:"request"`
Username string `json:"username"`
Nonce int64 `json:"nonce"`
Hmac string `json:"hmac_sha256"`
Timestamp int64 `json:"timestamp"`
}{
Request: "login",
Username: c.API.Credentials.ClientID,
Nonce: nonce,
Hmac: crypto.HexEncodeToString(hmac),
Timestamp: timestamp,
}
err := c.wsSend(loginRequest)
if err != nil {
c.Websocket.SetCanUseAuthenticatedEndpoints(false)
return err
}
return nil
}
func (c *COINUT) wsGetAccountBalance() error {
if !c.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authorised to submit order", c.Name)
}
accBalance := wsRequest{
Request: "user_balance",
Nonce: c.GetNonce(),
}
return c.wsSend(accBalance)
}
func (c *COINUT) wsSubmitOrder(order *WsSubmitOrderParameters) error {
if !c.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authorised to submit order", c.Name)
}
currency := c.FormatExchangeCurrency(order.Currency, asset.Spot).String()
var orderSubmissionRequest WsSubmitOrderRequest
orderSubmissionRequest.Request = "new_order"
orderSubmissionRequest.Nonce = c.GetNonce()
orderSubmissionRequest.InstID = instrumentListByString[currency]
orderSubmissionRequest.Qty = order.Amount
orderSubmissionRequest.Price = order.Price
orderSubmissionRequest.Side = string(order.Side)
if order.OrderID > 0 {
orderSubmissionRequest.OrderID = order.OrderID
}
return c.wsSend(orderSubmissionRequest)
}
func (c *COINUT) wsSubmitOrders(orders []WsSubmitOrderParameters) error {
if !c.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authorised to submit orders", c.Name)
}
orderRequest := WsSubmitOrdersRequest{}
for i := range orders {
currency := c.FormatExchangeCurrency(orders[i].Currency, asset.Spot).String()
orderRequest.Orders = append(orderRequest.Orders,
WsSubmitOrdersRequestData{
Qty: orders[i].Amount,
Price: orders[i].Price,
Side: string(orders[i].Side),
InstID: instrumentListByString[currency],
ClientOrdID: i + 1,
})
}
orderRequest.Nonce = c.GetNonce()
orderRequest.Request = "new_orders"
return c.wsSend(orderRequest)
}
func (c *COINUT) wsGetOpenOrders(p currency.Pair) error {
if !c.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authorised to get open orders", c.Name)
}
currency := c.FormatExchangeCurrency(p, asset.Spot).String()
var openOrdersRequest WsGetOpenOrdersRequest
openOrdersRequest.Request = "user_open_orders"
openOrdersRequest.Nonce = c.GetNonce()
openOrdersRequest.InstID = instrumentListByString[currency]
return c.wsSend(openOrdersRequest)
}
func (c *COINUT) wsCancelOrder(cancellation WsCancelOrderParameters) error {
if !c.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authorised to cancel order", c.Name)
}
currency := c.FormatExchangeCurrency(cancellation.Currency, asset.Spot).String()
var cancellationRequest WsCancelOrderRequest
cancellationRequest.Request = "cancel_order"
cancellationRequest.InstID = instrumentListByString[currency]
cancellationRequest.OrderID = cancellation.OrderID
cancellationRequest.Nonce = c.GetNonce()
return c.wsSend(cancellationRequest)
}
func (c *COINUT) wsCancelOrders(cancellations []WsCancelOrderParameters) error {
if !c.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authorised to cancel orders", c.Name)
}
cancelOrderRequest := WsCancelOrdersRequest{}
for i := range cancellations {
currency := c.FormatExchangeCurrency(cancellations[i].Currency, asset.Spot).String()
cancelOrderRequest.Entries = append(cancelOrderRequest.Entries, WsCancelOrdersRequestEntry{
InstID: instrumentListByString[currency],
OrderID: cancellations[i].OrderID,
})
}
cancelOrderRequest.Request = "cancel_orders"
cancelOrderRequest.Nonce = c.GetNonce()
return c.wsSend(cancelOrderRequest)
}
func (c *COINUT) wsGetTradeHistory(p currency.Pair, start, limit int64) error {
if !c.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authorised to get trade history", c.Name)
}
currency := c.FormatExchangeCurrency(p, asset.Spot).String()
var request WsTradeHistoryRequest
request.Request = "trade_history"
request.InstID = instrumentListByString[currency]
request.Nonce = c.GetNonce()
request.Start = start
request.Limit = limit
return c.wsSend(request)
}

View File

@@ -631,3 +631,13 @@ func (c *COINUT) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketCha
c.Websocket.UnsubscribeToChannels(channels)
return nil
}
// GetSubscriptions returns a copied list of subscriptions
func (c *COINUT) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return c.Websocket.GetSubscriptions(), nil
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (c *COINUT) AuthenticateWebsocket() error {
return c.wsAuthenticate()
}

View File

@@ -282,8 +282,14 @@ func (e *Base) SetCurrencyPairFormat() {
// GetAuthenticatedAPISupport returns whether the exchange supports
// authenticated API requests
func (e *Base) GetAuthenticatedAPISupport() bool {
return e.API.AuthenticatedSupport
func (e *Base) GetAuthenticatedAPISupport(endpoint uint8) bool {
switch endpoint {
case RestAuthentication:
return e.API.AuthenticatedSupport
case WebsocketAuthentication:
return e.API.AuthenticatedWebsocketSupport
}
return false
}
// GetName is a method that returns the name of the exchange base
@@ -388,6 +394,7 @@ func (e *Base) SetAPIKeys(apiKey, apiSecret, clientID string) {
result, err := crypto.Base64Decode(apiSecret)
if err != nil {
e.API.AuthenticatedSupport = false
e.API.AuthenticatedWebsocketSupport = false
log.Warnf(warningBase64DecryptSecretKeyFailed, e.Name)
}
e.API.Credentials.Secret = string(result)
@@ -404,7 +411,8 @@ func (e *Base) SetupDefaults(exch *config.ExchangeConfig) error {
e.Verbose = exch.Verbose
e.API.AuthenticatedSupport = exch.API.AuthenticatedSupport
if e.API.AuthenticatedSupport {
e.API.AuthenticatedWebsocketSupport = exch.API.AuthenticatedWebsocketSupport
if e.API.AuthenticatedSupport || e.API.AuthenticatedWebsocketSupport {
e.SetAPIKeys(exch.API.Credentials.Key, exch.API.Credentials.Secret, exch.API.Credentials.ClientID)
}
@@ -456,13 +464,13 @@ func (e *Base) AllowAuthenticatedRequest() bool {
// Bot usage, AuthenticatedSupport can be disabled by user if desired, so don't
// allow authenticated requests.
if !e.API.AuthenticatedSupport && e.LoadedByConfig {
if (!e.API.AuthenticatedSupport && !e.API.AuthenticatedWebsocketSupport) && e.LoadedByConfig {
return false
}
// Check to see if the user has enabled AuthenticatedSupport, but has invalid
// API credentials set and loaded by config
if e.API.AuthenticatedSupport && e.LoadedByConfig && !e.ValidateAPICredentials() {
if (e.API.AuthenticatedSupport || e.API.AuthenticatedWebsocketSupport) && e.LoadedByConfig && !e.ValidateAPICredentials() {
return false
}

View File

@@ -263,10 +263,27 @@ func TestSetCurrencyPairFormat(t *testing.T) {
}
}
// TestGetAuthenticatedAPISupport logic test
func TestGetAuthenticatedAPISupport(t *testing.T) {
var base Base
if base.GetAuthenticatedAPISupport() {
t.Fatal("Test failed. TestGetAuthenticatedAPISupport returned true when it should of been false.")
base := Base{
API: API{
AuthenticatedSupport: true,
AuthenticatedWebsocketSupport: false,
},
}
if !base.GetAuthenticatedAPISupport(RestAuthentication) {
t.Fatal("Test failed. Expected RestAuthentication to return true")
}
if base.GetAuthenticatedAPISupport(WebsocketAuthentication) {
t.Fatal("Test failed. Expected WebsocketAuthentication to return false")
}
base.API.AuthenticatedWebsocketSupport = true
if !base.GetAuthenticatedAPISupport(WebsocketAuthentication) {
t.Fatal("Test failed. Expected WebsocketAuthentication to return true")
}
if base.GetAuthenticatedAPISupport(2) {
t.Fatal("Test failed. Expected default case of 'false' to be returned")
}
}
@@ -539,19 +556,21 @@ func TestIsEnabled(t *testing.T) {
}
}
// TestSetAPIKeys logic test
func TestSetAPIKeys(t *testing.T) {
SetAPIKeys := Base{
Name: "TESTNAME",
Enabled: false,
API: API{
AuthenticatedSupport: false,
AuthenticatedWebsocketSupport: false,
},
}
SetAPIKeys.SetAPIKeys("RocketMan", "Digereedoo", "007")
if SetAPIKeys.API.Credentials.Key != "RocketMan" && SetAPIKeys.API.Credentials.Secret != "Digereedoo" && SetAPIKeys.API.Credentials.ClientID != "007" {
t.Error("Test Failed - SetAPIKeys() unable to set API credentials")
}
SetAPIKeys.API.CredentialsValidator.RequiresBase64DecodeSecret = true
SetAPIKeys.SetAPIKeys("RocketMan", "Digereedoo", "007")
}
func TestSetPairs(t *testing.T) {

View File

@@ -8,6 +8,12 @@ import (
"github.com/thrasher-/gocryptotrader/exchanges/request"
)
// Endpoint authentication types
const (
RestAuthentication uint8 = 0
WebsocketAuthentication uint8 = 1
)
// FeeType custom type for calculating fees based on method
type FeeType uint8
@@ -261,8 +267,9 @@ type FeaturesSupported struct {
// API stores the exchange API settings
type API struct {
AuthenticatedSupport bool
PEMKeySupport bool
AuthenticatedSupport bool
AuthenticatedWebsocketSupport bool
PEMKeySupport bool
Endpoints struct {
URL string

View File

@@ -507,3 +507,13 @@ func (e *EXMO) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannel
func (e *EXMO) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error {
return common.ErrFunctionNotSupported
}
// GetSubscriptions returns a copied list of subscriptions
func (e *EXMO) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return nil, common.ErrFunctionNotSupported
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (e *EXMO) AuthenticateWebsocket() error {
return common.ErrFunctionNotSupported
}

View File

@@ -1,12 +1,17 @@
package gateio
import (
"net/http"
"strings"
"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"
)
// Please supply your own APIKEYS here for due diligence testing
@@ -31,6 +36,7 @@ func TestSetup(t *testing.T) {
t.Error("Test Failed - GateIO Setup() init error")
}
gateioConfig.API.AuthenticatedSupport = true
gateioConfig.API.AuthenticatedWebsocketSupport = true
gateioConfig.API.Credentials.Key = apiKey
gateioConfig.API.Credentials.Secret = apiSecret
@@ -493,3 +499,48 @@ func TestGetOrderInfo(t *testing.T) {
}
}
}
// TestWsAuth dials websocket, sends login request.
func TestWsAuth(t *testing.T) {
g.SetDefaults()
TestSetup(t)
if !g.Websocket.IsEnabled() && !g.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() {
t.Skip(exchange.WebsocketNotEnabled)
}
var err error
var dialer websocket.Dialer
g.WebsocketConn, _, err = dialer.Dial(g.Websocket.GetWebsocketURL(),
http.Header{})
if err != nil {
t.Fatal(err)
}
g.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
g.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride()
go g.WsHandleData()
defer g.WebsocketConn.Close()
err = g.wsServerSignIn()
if err != nil {
t.Error(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case resultString := <-g.Websocket.DataHandler:
if !strings.Contains(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()
}

View File

@@ -48,22 +48,21 @@ func (g *Gateio) WsConnect() error {
if err != nil {
return err
}
if g.API.AuthenticatedSupport {
err = g.wsServerSignIn()
if err != nil {
log.Errorf("%v - wsServerSignin() failed: %v", g.GetName(), err)
}
time.Sleep(time.Second * 2) // sleep to allow server to complete sign-on if further authenticated requests are sent piror to this they will fail
}
go g.WsHandleData()
g.GenerateDefaultSubscriptions()
err = g.wsServerSignIn()
if err != nil {
log.Errorf("%v - authentication failed: %v", g.Name, err)
}
g.GenerateAuthenticatedSubscriptions()
g.GenerateDefaultSubscriptions()
return nil
}
func (g *Gateio) wsServerSignIn() error {
if !g.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", g.Name)
}
nonce := int(time.Now().Unix() * 1000)
sigTemp := g.GenerateSignature(strconv.Itoa(nonce))
signature := crypto.Base64Encode(sigTemp)
@@ -72,7 +71,13 @@ func (g *Gateio) wsServerSignIn() error {
Method: "server.sign",
Params: []interface{}{g.API.Credentials.Key, signature, nonce},
}
return g.wsSend(signinWsRequest)
err := g.wsSend(signinWsRequest)
if err != nil {
g.Websocket.SetCanUseAuthenticatedEndpoints(false)
return 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
@@ -116,20 +121,22 @@ func (g *Gateio) WsHandleData() {
g.Websocket.DataHandler <- err
continue
}
if result.Error.Code != 0 {
if strings.Contains(result.Error.Message, "authentication") {
g.Websocket.DataHandler <- fmt.Errorf("%v - WebSocket authentication failed ",
g.GetName())
g.API.AuthenticatedSupport = false
g.Websocket.DataHandler <- fmt.Errorf("%v - authentication failed: %v", g.Name, err)
g.Websocket.SetCanUseAuthenticatedEndpoints(false)
continue
}
g.Websocket.DataHandler <- fmt.Errorf("gateio_websocket.go error %s",
result.Error.Message)
g.Websocket.DataHandler <- fmt.Errorf("%v error %s",
g.Name, result.Error.Message)
continue
}
switch result.ID {
case IDSignIn:
g.Websocket.SetCanUseAuthenticatedEndpoints(true)
g.Websocket.DataHandler <- string(result.Result)
case IDBalance:
var balance WebsocketBalance
var balanceInterface interface{}
@@ -342,14 +349,29 @@ func (g *Gateio) WsHandleData() {
}
}
// GenerateAuthenticatedSubscriptions Adds authenticated subscriptions to websocket to be handled by ManageSubscriptions()
func (g *Gateio) GenerateAuthenticatedSubscriptions() {
if !g.Websocket.CanUseAuthenticatedEndpoints() {
return
}
var channels = []string{"balance.subscribe", "order.subscribe"}
var subscriptions []exchange.WebsocketChannelSubscription
enabledCurrencies := g.GetEnabledPairs(asset.Spot)
for i := range channels {
for j := range enabledCurrencies {
subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{
Channel: channels[i],
Currency: enabledCurrencies[j],
})
}
}
g.Websocket.SubscribeToChannels(subscriptions)
}
// 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"}
if g.AllowAuthenticatedRequest() {
channels = append(channels, "balance.subscribe", "order.subscribe")
}
subscriptions := []exchange.WebsocketChannelSubscription{}
var subscriptions []exchange.WebsocketChannelSubscription
enabledCurrencies := g.GetEnabledPairs(asset.Spot)
for i := range channels {
for j := range enabledCurrencies {
@@ -402,6 +424,9 @@ func (g *Gateio) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscri
}
func (g *Gateio) wsGetBalance() error {
if !g.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authorised to get balance", g.Name)
}
balanceWsRequest := WebsocketRequest{
ID: IDBalance,
Method: "balance.query",
@@ -411,6 +436,9 @@ func (g *Gateio) wsGetBalance() error {
}
func (g *Gateio) wsGetOrderInfo(market string, offset, limit int) error {
if !g.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authorised to get order info", g.Name)
}
order := WebsocketRequest{
ID: IDOrderQuery,
Method: "order.query",

View File

@@ -575,3 +575,13 @@ func (g *Gateio) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketCha
g.Websocket.UnsubscribeToChannels(channels)
return nil
}
// GetSubscriptions returns a copied list of subscriptions
func (g *Gateio) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return g.Websocket.GetSubscriptions(), nil
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (g *Gateio) AuthenticateWebsocket() error {
return g.wsServerSignIn()
}

View File

@@ -3,11 +3,14 @@ package gemini
import (
"net/url"
"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"
)
// Please enter sandbox API keys & assigned roles for better testing procedures
@@ -62,6 +65,7 @@ func TestSetup(t *testing.T) {
}
geminiConfig.API.AuthenticatedSupport = true
geminiConfig.API.AuthenticatedWebsocketSupport = true
Session[1].Setup(geminiConfig)
Session[2].Setup(geminiConfig)
@@ -559,3 +563,32 @@ func TestGetDepositAddress(t *testing.T) {
t.Error("Test Failed - GetDepositAddress error cannot be nil")
}
}
// TestWsAuth dials websocket, sends login request.
func TestWsAuth(t *testing.T) {
TestAddSession(t)
TestSetDefaults(t)
TestSetup(t)
g := Session[1]
g.API.Endpoints.WebsocketURL = geminiWebsocketSandboxEndpoint
if !g.Websocket.IsEnabled() && !g.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() {
t.Skip(exchange.WebsocketNotEnabled)
}
var dialer websocket.Dialer
go g.WsHandleData()
err := g.WsSecureSubscribe(&dialer, geminiWsOrderEvents)
if err != nil {
t.Error(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case resp := <-g.Websocket.DataHandler:
if resp.(WsSubscriptionAcknowledgementResponse).Type != "subscription_ack" {
t.Error("Login failed")
}
case <-timer.C:
t.Error("Expected response")
}
timer.Stop()
}

View File

@@ -195,8 +195,13 @@ type ErrorCapture struct {
Message string `json:"message"`
}
// Response defines the main response type
type Response struct {
// WsResponse generic response
type WsResponse struct {
Type string `json:"type"`
}
// WsMarketUpdateResponse defines the main response type
type WsMarketUpdateResponse struct {
Type string `json:"type"`
EventID int64 `json:"eventId"`
Timestamp int64 `json:"timestamp"`
@@ -221,5 +226,192 @@ type Event struct {
type ReadData struct {
Raw []byte
Currency currency.Pair
FeedType string
}
// WsRequestPayload Request info to subscribe to a WS enpoint
type WsRequestPayload struct {
Request string `json:"request"`
Nonce int64 `json:"nonce"`
}
// WsSubscriptionAcknowledgementResponse The first message you receive acknowledges your subscription
type WsSubscriptionAcknowledgementResponse struct {
Type string `json:"type"`
AccountID int64 `json:"accountId"`
SubscriptionID string `json:"subscriptionId"`
SymbolFilter []string `json:"symbolFilter"`
APISessionFilter []string `json:"apiSessionFilter"`
EventTypeFilter []string `json:"eventTypeFilter"`
}
// WsHeartbeatResponse Gemini will send a heartbeat every five seconds so you'll know your WebSocket connection is active.
type WsHeartbeatResponse struct {
Type string `json:"type"`
Timestampms int64 `json:"timestampms"`
Sequence int64 `json:"sequence"`
TraceID string `json:"trace_id"`
SocketSequence int64 `json:"socket_sequence"`
}
// WsActiveOrdersResponse contains active orders
type WsActiveOrdersResponse struct {
Type string `json:"type"`
OrderID string `json:"order_id"`
APISession string `json:"api_session"`
Symbol currency.Pair `json:"symbol"`
Side string `json:"side"`
OrderType string `json:"order_type"`
Timestamp string `json:"timestamp"`
Timestampms int64 `json:"timestampms"`
IsLive bool `json:"is_live"`
IsCancelled bool `json:"is_cancelled"`
IsHidden bool `json:"is_hidden"`
AvgExecutionPrice float64 `json:"avg_execution_price,string"`
ExecutedAmount float64 `json:"executed_amount,string"`
RemainingAmount float64 `json:"remaining_amount,string"`
OriginalAmount float64 `json:"original_amount,string"`
Price float64 `json:"price,string"`
SocketSequence int64 `json:"socket_sequence"`
}
// WsOrderRejectedResponse ws response
type WsOrderRejectedResponse struct {
Type string `json:"type"`
OrderID string `json:"order_id"`
EventID string `json:"event_id"`
Reason string `json:"reason"`
APISession string `json:"api_session"`
Symbol currency.Pair `json:"symbol"`
Side string `json:"side"`
OrderType string `json:"order_type"`
Timestamp string `json:"timestamp"`
Timestampms int64 `json:"timestampms"`
IsLive bool `json:"is_live"`
OriginalAmount float64 `json:"original_amount,string"`
Price float64 `json:"price,string"`
SocketSequence int64 `json:"socket_sequence"`
}
// WsOrderBookedResponse ws response
type WsOrderBookedResponse struct {
Type string `json:"type"`
OrderID string `json:"order_id"`
EventID string `json:"event_id"`
APISession string `json:"api_session"`
Symbol currency.Pair `json:"symbol"`
Side string `json:"side"`
OrderType string `json:"order_type"`
Timestamp string `json:"timestamp"`
Timestampms int64 `json:"timestampms"`
IsLive bool `json:"is_live"`
IsCancelled bool `json:"is_cancelled"`
IsHidden bool `json:"is_hidden"`
AvgExecutionPrice float64 `json:"avg_execution_price,string"`
ExecutedAmount float64 `json:"executed_amount,string"`
RemainingAmount float64 `json:"remaining_amount,string"`
OriginalAmount float64 `json:"original_amount,string"`
Price float64 `json:"price,string"`
SocketSequence int64 `json:"socket_sequence"`
}
// WsOrderFilledResponse ws response
type WsOrderFilledResponse struct {
Type string `json:"type"`
OrderID string `json:"order_id"`
APISession string `json:"api_session"`
Symbol currency.Pair `json:"symbol"`
Side string `json:"side"`
OrderType string `json:"order_type"`
Timestamp string `json:"timestamp"`
Timestampms int64 `json:"timestampms"`
IsLive bool `json:"is_live"`
IsCancelled bool `json:"is_cancelled"`
IsHidden bool `json:"is_hidden"`
AvgExecutionPrice float64 `json:"avg_execution_price,string"`
ExecutedAmount float64 `json:"executed_amount,string"`
RemainingAmount float64 `json:"remaining_amount,string"`
OriginalAmount float64 `json:"original_amount,string"`
Price float64 `json:"price,string"`
Fill WsOrderFilledData `json:"fill"`
SocketSequence int64 `json:"socket_sequence"`
}
// WsOrderFilledData ws response data
type WsOrderFilledData struct {
TradeID string `json:"trade_id"`
Liquidity string `json:"liquidity"`
Price float64 `json:"price,string"`
Amount float64 `json:"amount,string"`
Fee float64 `json:"fee,string"`
FeeCurrency string `json:"fee_currency"`
}
// WsOrderCancelledResponse ws response
type WsOrderCancelledResponse struct {
Type string `json:"type"`
OrderID string `json:"order_id"`
EventID string `json:"event_id"`
CancelCommandID string `json:"cancel_command_id,omitempty"`
Reason string `json:"reason"`
APISession string `json:"api_session"`
Symbol currency.Pair `json:"symbol"`
Side string `json:"side"`
OrderType string `json:"order_type"`
Timestamp string `json:"timestamp"`
Timestampms int64 `json:"timestampms"`
IsLive bool `json:"is_live"`
IsCancelled bool `json:"is_cancelled"`
IsHidden bool `json:"is_hidden"`
AvgExecutionPrice float64 `json:"avg_execution_price,string"`
ExecutedAmount float64 `json:"executed_amount,string"`
RemainingAmount float64 `json:"remaining_amount,string"`
OriginalAmount float64 `json:"original_amount,string"`
Price float64 `json:"price,string"`
SocketSequence int64 `json:"socket_sequence"`
}
// WsOrderCancellationRejectedResponse ws response
type WsOrderCancellationRejectedResponse struct {
Type string `json:"type"`
OrderID string `json:"order_id"`
EventID string `json:"event_id"`
CancelCommandID string `json:"cancel_command_id"`
Reason string `json:"reason"`
APISession string `json:"api_session"`
Symbol currency.Pair `json:"symbol"`
Side string `json:"side"`
OrderType string `json:"order_type"`
Timestamp string `json:"timestamp"`
Timestampms int64 `json:"timestampms"`
IsLive bool `json:"is_live"`
IsCancelled bool `json:"is_cancelled"`
IsHidden bool `json:"is_hidden"`
AvgExecutionPrice float64 `json:"avg_execution_price,string"`
ExecutedAmount float64 `json:"executed_amount,string"`
RemainingAmount float64 `json:"remaining_amount,string"`
OriginalAmount float64 `json:"original_amount,string"`
Price float64 `json:"price,string"`
SocketSequence int64 `json:"socket_sequence"`
}
// WsOrderClosedResponse ws response
type WsOrderClosedResponse struct {
Type string `json:"type"`
OrderID string `json:"order_id"`
EventID string `json:"event_id"`
APISession string `json:"api_session"`
Symbol currency.Pair `json:"symbol"`
Side string `json:"side"`
OrderType string `json:"order_type"`
Timestamp string `json:"timestamp"`
Timestampms int64 `json:"timestampms"`
IsLive bool `json:"is_live"`
IsCancelled bool `json:"is_cancelled"`
IsHidden bool `json:"is_hidden"`
AvgExecutionPrice float64 `json:"avg_execution_price,string"`
ExecutedAmount float64 `json:"executed_amount,string"`
RemainingAmount float64 `json:"remaining_amount,string"`
OriginalAmount float64 `json:"original_amount,string"`
Price float64 `json:"price,string"`
SocketSequence int64 `json:"socket_sequence"`
}

View File

@@ -11,16 +11,20 @@ import (
"github.com/gorilla/websocket"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/common/crypto"
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/asset"
"github.com/thrasher-/gocryptotrader/exchanges/orderbook"
log "github.com/thrasher-/gocryptotrader/logger"
)
const (
geminiWebsocketEndpoint = "wss://api.gemini.com/v1/marketdata/%s?%s"
geminiWsEvent = "event"
geminiWsMarketData = "marketdata"
geminiWebsocketEndpoint = "wss://api.gemini.com/v1/"
geminiWebsocketSandboxEndpoint = "wss://api.sandbox.gemini.com/v1/"
geminiWsEvent = "event"
geminiWsMarketData = "marketdata"
geminiWsOrderEvents = "order/events"
)
// Instantiates a communications channel between websocket connections
@@ -38,12 +42,14 @@ func (g *Gemini) WsConnect() error {
if err != nil {
return err
}
dialer.Proxy = http.ProxyURL(proxy)
}
go g.WsHandleData()
err := g.WsSecureSubscribe(&dialer, geminiWsOrderEvents)
if err != nil {
log.Errorf("%v - authentication failed: %v", g.Name, err)
}
return g.WsSubscribe(&dialer)
}
@@ -53,59 +59,76 @@ func (g *Gemini) WsSubscribe(dialer *websocket.Dialer) error {
for i, c := range enabledCurrencies {
val := url.Values{}
val.Set("heartbeat", "true")
endpoint := fmt.Sprintf(g.Websocket.GetWebsocketURL(),
endpoint := fmt.Sprintf("%s%s/%s?%s",
g.API.Endpoints.WebsocketURL,
geminiWsMarketData,
c.String(),
val.Encode())
conn, _, err := dialer.Dial(endpoint, http.Header{})
conn, conStatus, err := dialer.Dial(endpoint, http.Header{})
if err != nil {
return err
return fmt.Errorf("%s websocket endpoint: %v Status: %v Error: %v", g.Name,
endpoint, conStatus, err)
}
go g.WsReadData(conn, c, geminiWsMarketData)
go g.WsReadData(conn, c)
if len(enabledCurrencies)-1 == i {
return nil
}
time.Sleep(5 * time.Second) // rate limiter, limit of 12 requests per
// minute
}
return nil
}
// WsSecureSubscribe will connect to Gemini's secure endpoint
func (g *Gemini) WsSecureSubscribe(dialer *websocket.Dialer, url string) error {
if !g.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", g.Name)
}
payload := WsRequestPayload{
Request: fmt.Sprintf("/v1/%v", url),
Nonce: time.Now().UnixNano(),
}
PayloadJSON, err := common.JSONEncode(payload)
if err != nil {
return fmt.Errorf("%v sendAuthenticatedHTTPRequest: Unable to JSON request", g.Name)
}
endpoint := fmt.Sprintf("%v%v", g.API.Endpoints.WebsocketURL, url)
PayloadBase64 := crypto.Base64Encode(PayloadJSON)
hmac := crypto.GetHMAC(crypto.HashSHA512_384, []byte(PayloadBase64), []byte(g.API.Credentials.Secret))
headers := http.Header{}
headers.Add("Content-Length", "0")
headers.Add("Content-Type", "text/plain")
headers.Add("X-GEMINI-PAYLOAD", PayloadBase64)
headers.Add("X-GEMINI-APIKEY", g.API.Credentials.Key)
headers.Add("X-GEMINI-SIGNATURE", crypto.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)
}
go g.WsReadData(conn, 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, feedType string) {
func (g *Gemini) WsReadData(ws *websocket.Conn, c currency.Pair) {
g.Websocket.Wg.Add(1)
defer func() {
err := ws.Close()
if err != nil {
g.Websocket.DataHandler <- fmt.Errorf("gemini_websocket.go - Unable to to close Websocket connection. Error: %s",
err)
}
g.Websocket.Wg.Done()
}()
defer g.Websocket.Wg.Done()
for {
select {
case <-g.Websocket.ShutdownC:
return
default:
_, resp, err := ws.ReadMessage()
if err != nil {
g.Websocket.DataHandler <- err
return
}
g.Websocket.TrafficAlert <- struct{}{}
comms <- ReadData{Raw: resp, Currency: c, FeedType: feedType}
comms <- ReadData{Raw: resp, Currency: c}
}
}
}
// WsHandleData handles all the websocket data coming from the websocket
@@ -113,120 +136,191 @@ func (g *Gemini) WsReadData(ws *websocket.Conn, c currency.Pair, feedType string
func (g *Gemini) WsHandleData() {
g.Websocket.Wg.Add(1)
defer g.Websocket.Wg.Done()
for {
select {
case <-g.Websocket.ShutdownC:
return
case resp := <-comms:
switch resp.FeedType {
case geminiWsEvent:
case geminiWsMarketData:
var result Response
// Gemini likes to send empty arrays
if string(resp.Raw) == "[]" {
continue
}
var result map[string]interface{}
err := common.JSONDecode(resp.Raw, &result)
if err != nil {
g.Websocket.DataHandler <- fmt.Errorf("%v Error: %v, Raw: %v", g.Name, err, string(resp.Raw))
continue
}
switch result["type"] {
case "subscription_ack":
var result WsSubscriptionAcknowledgementResponse
err := common.JSONDecode(resp.Raw, &result)
if err != nil {
g.Websocket.DataHandler <- err
continue
}
switch result.Type {
case "update":
if result.Timestamp == 0 && result.TimestampMS == 0 {
var bids, asks []orderbook.Item
for _, event := range result.Events {
if event.Reason != "initial" {
g.Websocket.DataHandler <- errors.New("gemini_websocket.go orderbook should be snapshot only")
continue
}
if event.Side == "ask" {
asks = append(asks, orderbook.Item{
Amount: event.Remaining,
Price: event.Price,
})
} else {
bids = append(bids, orderbook.Item{
Amount: event.Remaining,
Price: event.Price,
})
}
}
var newOrderBook orderbook.Base
newOrderBook.Asks = asks
newOrderBook.Bids = bids
newOrderBook.AssetType = asset.Spot
newOrderBook.LastUpdated = time.Now()
newOrderBook.Pair = resp.Currency
err := g.Websocket.Orderbook.LoadSnapshot(&newOrderBook,
g.GetName(),
false)
if err != nil {
g.Websocket.DataHandler <- err
break
}
g.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{Pair: resp.Currency,
Asset: asset.Spot,
Exchange: g.GetName()}
} else {
for _, event := range result.Events {
if event.Type == "trade" {
g.Websocket.DataHandler <- exchange.TradeData{
Timestamp: time.Now(),
CurrencyPair: resp.Currency,
AssetType: asset.Spot,
Exchange: g.GetName(),
EventTime: result.Timestamp,
Price: event.Price,
Amount: event.Amount,
Side: event.MakerSide,
}
} else {
var i orderbook.Item
i.Amount = event.Remaining
i.Price = event.Price
if event.Side == "ask" {
err := g.Websocket.Orderbook.Update(nil,
[]orderbook.Item{i},
resp.Currency,
time.Now(),
g.GetName(),
asset.Spot)
if err != nil {
g.Websocket.DataHandler <- err
}
} else {
err := g.Websocket.Orderbook.Update([]orderbook.Item{i},
nil,
resp.Currency,
time.Now(),
g.GetName(),
asset.Spot)
if err != nil {
g.Websocket.DataHandler <- err
}
}
}
}
g.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{Pair: resp.Currency,
Asset: asset.Spot,
Exchange: g.GetName()}
}
case "heartbeat":
default:
g.Websocket.DataHandler <- fmt.Errorf("gemini_websocket.go - unhandled data %s",
resp.Raw)
g.Websocket.DataHandler <- result
case "initial":
var result WsSubscriptionAcknowledgementResponse
err := common.JSONDecode(resp.Raw, &result)
if err != nil {
g.Websocket.DataHandler <- err
continue
}
g.Websocket.DataHandler <- result
case "accepted":
var result WsActiveOrdersResponse
err := common.JSONDecode(resp.Raw, &result)
if err != nil {
g.Websocket.DataHandler <- err
continue
}
g.Websocket.DataHandler <- result
case "booked":
var result WsOrderBookedResponse
err := common.JSONDecode(resp.Raw, &result)
if err != nil {
g.Websocket.DataHandler <- err
continue
}
g.Websocket.DataHandler <- result
case "fill":
var result WsOrderFilledResponse
err := common.JSONDecode(resp.Raw, &result)
if err != nil {
g.Websocket.DataHandler <- err
continue
}
g.Websocket.DataHandler <- result
case "cancelled":
var result WsOrderCancelledResponse
err := common.JSONDecode(resp.Raw, &result)
if err != nil {
g.Websocket.DataHandler <- err
continue
}
g.Websocket.DataHandler <- result
case "closed":
var result WsOrderClosedResponse
err := common.JSONDecode(resp.Raw, &result)
if err != nil {
g.Websocket.DataHandler <- err
continue
}
g.Websocket.DataHandler <- result
case "heartbeat":
var result WsHeartbeatResponse
err := common.JSONDecode(resp.Raw, &result)
if err != nil {
g.Websocket.DataHandler <- err
continue
}
g.Websocket.DataHandler <- result
case "update":
if resp.Currency.IsEmpty() {
g.Websocket.DataHandler <- fmt.Errorf("%v - unhandled data %s",
g.Name, resp.Raw)
continue
}
var marketUpdate WsMarketUpdateResponse
err := common.JSONDecode(resp.Raw, &marketUpdate)
if err != nil {
g.Websocket.DataHandler <- err
continue
}
g.wsProcessUpdate(marketUpdate, resp.Currency)
default:
g.Websocket.DataHandler <- fmt.Errorf("%v - unhandled data %s",
g.Name, resp.Raw)
}
}
}
}
// wsProcessUpdate handles order book data
func (g *Gemini) wsProcessUpdate(result WsMarketUpdateResponse, pair currency.Pair) {
if result.Timestamp == 0 && result.TimestampMS == 0 {
var bids, asks []orderbook.Item
for _, event := range result.Events {
if event.Reason != "initial" {
g.Websocket.DataHandler <- errors.New("gemini_websocket.go orderbook should be snapshot only")
continue
}
if event.Side == "ask" {
asks = append(asks, orderbook.Item{
Amount: event.Remaining,
Price: event.Price,
})
} else {
bids = append(bids, orderbook.Item{
Amount: event.Remaining,
Price: event.Price,
})
}
}
var newOrderBook orderbook.Base
newOrderBook.Asks = asks
newOrderBook.Bids = bids
newOrderBook.AssetType = asset.Spot
newOrderBook.Pair = pair
err := g.Websocket.Orderbook.LoadSnapshot(&newOrderBook,
g.GetName(),
false)
if err != nil {
g.Websocket.DataHandler <- err
return
}
g.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{Pair: pair,
Asset: asset.Spot,
Exchange: g.GetName()}
} else {
for _, event := range result.Events {
if event.Type == "trade" {
g.Websocket.DataHandler <- exchange.TradeData{
Timestamp: time.Now(),
CurrencyPair: pair,
AssetType: asset.Spot,
Exchange: g.Name,
EventTime: result.Timestamp,
Price: event.Price,
Amount: event.Amount,
Side: event.MakerSide,
}
} else {
var i orderbook.Item
i.Amount = event.Remaining
i.Price = event.Price
if event.Side == "ask" {
err := g.Websocket.Orderbook.Update(nil,
[]orderbook.Item{i},
pair,
time.Now(),
g.GetName(),
asset.Spot)
if err != nil {
g.Websocket.DataHandler <- err
}
} else {
err := g.Websocket.Orderbook.Update([]orderbook.Item{i},
nil,
pair,
time.Now(),
g.GetName(),
asset.Spot)
if err != nil {
g.Websocket.DataHandler <- err
}
}
}
}
g.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{Pair: pair,
Asset: asset.Spot,
Exchange: g.GetName()}
}
}

View File

@@ -480,3 +480,13 @@ func (g *Gemini) SubscribeToWebsocketChannels(channels []exchange.WebsocketChann
func (g *Gemini) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error {
return common.ErrFunctionNotSupported
}
// GetSubscriptions returns a copied list of subscriptions
func (g *Gemini) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return nil, common.ErrFunctionNotSupported
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (g *Gemini) AuthenticateWebsocket() error {
return common.ErrFunctionNotSupported
}

View File

@@ -1,12 +1,16 @@
package hitbtc
import (
"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"
)
var h HitBTC
@@ -30,6 +34,7 @@ func TestSetup(t *testing.T) {
t.Error("Test Failed - HitBTC Setup() init error")
}
hitbtcConfig.API.AuthenticatedSupport = true
hitbtcConfig.API.AuthenticatedWebsocketSupport = true
hitbtcConfig.API.Credentials.Key = apiKey
hitbtcConfig.API.Credentials.Secret = apiSecret
@@ -99,7 +104,7 @@ func TestGetFee(t *testing.T) {
// CryptocurrencyTradeFee Basic
if resp, err := h.GetFee(feeBuilder); resp != float64(0.001) || err != nil {
t.Error(err)
t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.001), resp)
t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.002), resp)
}
// CryptocurrencyTradeFee High quantity
@@ -107,7 +112,7 @@ func TestGetFee(t *testing.T) {
feeBuilder.Amount = 1000
feeBuilder.PurchasePrice = 1000
if resp, err := h.GetFee(feeBuilder); resp != float64(1000) || err != nil {
t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(1000), resp)
t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(2000), resp)
t.Error(err)
}
@@ -115,7 +120,7 @@ func TestGetFee(t *testing.T) {
feeBuilder = setFeeBuilder()
feeBuilder.IsMaker = true
if resp, err := h.GetFee(feeBuilder); resp != float64(-0.0001) || err != nil {
t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(-0.0001), resp)
t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.001), resp)
t.Error(err)
}
@@ -123,7 +128,7 @@ func TestGetFee(t *testing.T) {
feeBuilder = setFeeBuilder()
feeBuilder.PurchasePrice = -1000
if resp, err := h.GetFee(feeBuilder); resp != float64(-1) || err != nil {
t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(-1), resp)
t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0), resp)
t.Error(err)
}
@@ -131,7 +136,7 @@ func TestGetFee(t *testing.T) {
feeBuilder = setFeeBuilder()
feeBuilder.FeeType = exchange.CryptocurrencyWithdrawalFee
if resp, err := h.GetFee(feeBuilder); resp != float64(0.009580) || err != nil {
t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.009580), resp)
t.Errorf("Test Failed - GetFee() error. Expected: %f, Received: %f", float64(0.042800), resp)
t.Error(err)
}
@@ -383,3 +388,107 @@ func TestGetDepositAddress(t *testing.T) {
}
}
}
func setupWsAuth(t *testing.T) {
TestSetDefaults(t)
TestSetup(t)
if !h.Websocket.IsEnabled() && !h.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() {
t.Skip(exchange.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{})
if err != nil {
t.Fatal(err)
}
go h.WsHandleData()
h.wsLogin()
timer := time.NewTimer(time.Second)
select {
case loginError := <-h.Websocket.DataHandler:
t.Fatal(loginError)
case <-timer.C:
}
timer.Stop()
}
// TestWsCancelOrder dials websocket, sends cancel request.
func TestWsCancelOrder(t *testing.T) {
setupWsAuth(t)
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 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 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()
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()
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()
}

View File

@@ -1,6 +1,10 @@
package hitbtc
import "time"
import (
"time"
"github.com/thrasher-/gocryptotrader/currency"
)
// Ticker holds ticker information
type Ticker struct {
@@ -186,19 +190,19 @@ type AuthenticatedTradeHistoryResponse struct {
// OrderHistoryResponse used for GetOrderHistory
type OrderHistoryResponse struct {
ID string `json:"id"`
ClientOrderID string `json:"clientOrderId"`
Symbol string `json:"symbol"`
Side string `json:"side"`
Status string `json:"status"`
Type string `json:"type"`
TimeInForce string `json:"timeInForce"`
Price float64 `json:"price,string"`
Quantity float64 `json:"quantity,string"`
PostOnly bool `json:"postOnly"`
CumQuantity float64 `json:"cumQuantity,string"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
ID string `json:"id"`
ClientOrderID string `json:"clientOrderId"`
Symbol string `json:"symbol"`
Side string `json:"side"`
Status string `json:"status"`
Type string `json:"type"`
TimeInForce string `json:"timeInForce"`
Price float64 `json:"price,string"`
Quantity float64 `json:"quantity,string"`
PostOnly bool `json:"postOnly"`
CumQuantity float64 `json:"cumQuantity,string"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
// ResultingTrades holds resulting trade information
@@ -295,12 +299,13 @@ type LendingHistory struct {
}
type capture struct {
Method string `json:"method"`
Result bool `json:"result"`
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"`
}
// WsRequest defines a request obj for the JSON-RPC and gets a websocket
@@ -314,13 +319,13 @@ type WsRequest struct {
// WsNotification defines a notification obj for the JSON-RPC this does not get
// a websocket response
type WsNotification struct {
JSONRPCVersion string `json:"jsonrpc"`
JSONRPCVersion string `json:"jsonrpc,omitempty"`
Method string `json:"method"`
Params interface{} `json:"params"`
}
type params struct {
Symbol string `json:"symbol"`
Symbol string `json:"symbol,omitempty"`
Period string `json:"period,omitempty"`
Limit int64 `json:"limit,omitempty"`
}
@@ -370,3 +375,234 @@ type WsTrade struct {
Symbol string `json:"symbol"`
} `json:"params"`
}
// WsLoginRequest defines login requirements for ws
type WsLoginRequest struct {
Method string `json:"method"`
Params WsLoginData `json:"params"`
}
// WsLoginData sets credentials for WsLoginRequest
type WsLoginData struct {
Algo string `json:"algo"`
PKey string `json:"pKey"`
Nonce string `json:"nonce"`
Signature string `json:"signature"`
}
// WsActiveOrdersResponse Active order response for auth subscription to reports
type WsActiveOrdersResponse struct {
Params []WsActiveOrdersResponseData `json:"params"`
}
// WsActiveOrdersResponseData Active order data for WsActiveOrdersResponse
type WsActiveOrdersResponseData struct {
ID string `json:"id"`
ClientOrderID string `json:"clientOrderId"`
Symbol currency.Pair `json:"symbol"`
Side string `json:"side"`
Status string `json:"status"`
Type string `json:"type"`
TimeInForce string `json:"timeInForce"`
Quantity float64 `json:"quantity,string"`
Price float64 `json:"price,string"`
CumQuantity float64 `json:"cumQuantity,string"`
PostOnly bool `json:"postOnly"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ReportType string `json:"reportType"`
}
// WsReportResponse report response for auth subscription to reports
type WsReportResponse struct {
Params WsReportResponseData `json:"params"`
}
// WsReportResponseData Report data for WsReportResponse
type WsReportResponseData struct {
ID string `json:"id"`
ClientOrderID string `json:"clientOrderId"`
Symbol currency.Pair `json:"symbol"`
Side string `json:"side"`
Status string `json:"status"`
Type string `json:"type"`
TimeInForce string `json:"timeInForce"`
Quantity float64 `json:"quantity,string"`
Price float64 `json:"price,string"`
CumQuantity float64 `json:"cumQuantity,string"`
PostOnly bool `json:"postOnly"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ReportType string `json:"reportType"`
TradeQuantity float64 `json:"tradeQuantity,string"`
TradePrice float64 `json:"tradePrice,string"`
TradeID int64 `json:"tradeId"`
TradeFee float64 `json:"tradeFee,string"`
}
// WsSubmitOrderRequest WS request
type WsSubmitOrderRequest struct {
Method string `json:"method"`
Params WsSubmitOrderRequestData `json:"params"`
ID int64 `json:"id"`
}
// WsSubmitOrderRequestData WS request data
type WsSubmitOrderRequestData struct {
ClientOrderID string `json:"clientOrderId"`
Symbol currency.Pair `json:"symbol"`
Side string `json:"side"`
Price float64 `json:"price,string"`
Quantity float64 `json:"quantity,string"`
}
// WsSubmitOrderSuccessResponse WS response
type WsSubmitOrderSuccessResponse struct {
Result WsSubmitOrderSuccessResponseData `json:"result"`
ID int64 `json:"id"`
}
// WsSubmitOrderSuccessResponseData WS response data
type WsSubmitOrderSuccessResponseData struct {
ID string `json:"id"`
ClientOrderID string `json:"clientOrderId"`
Symbol string `json:"symbol"`
Side string `json:"side"`
Status string `json:"status"`
Type string `json:"type"`
TimeInForce string `json:"timeInForce"`
Quantity float64 `json:"quantity,string"`
Price float64 `json:"price,string"`
CumQuantity float64 `json:"cumQuantity,string"`
PostOnly bool `json:"postOnly"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ReportType string `json:"reportType"`
}
// WsSubmitOrderErrorResponse WS error response
type WsSubmitOrderErrorResponse struct {
Error WsSubmitOrderErrorResponseData `json:"error"`
ID int64 `json:"id"`
}
// WsSubmitOrderErrorResponseData WS error response data
type WsSubmitOrderErrorResponseData struct {
Code int64 `json:"code"`
Message string `json:"message"`
Description string `json:"description"`
}
// WsCancelOrderResponse WS response
type WsCancelOrderResponse struct {
Result WsCancelOrderResponseData `json:"result"`
ID int64 `json:"id"`
}
// WsCancelOrderResponseData WS response data
type WsCancelOrderResponseData struct {
ID string `json:"id"`
ClientOrderID string `json:"clientOrderId"`
Symbol currency.Pair `json:"symbol"`
Side string `json:"side"`
Status string `json:"status"`
Type string `json:"type"`
TimeInForce string `json:"timeInForce"`
Quantity float64 `json:"quantity,string"`
Price float64 `json:"price,string"`
CumQuantity float64 `json:"cumQuantity,string"`
PostOnly bool `json:"postOnly"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ReportType string `json:"reportType"`
}
// WsReplaceOrderResponse WS response
type WsReplaceOrderResponse struct {
Result WsReplaceOrderResponseData `json:"result"`
ID int64 `json:"id"`
}
// WsReplaceOrderResponseData WS response data
type WsReplaceOrderResponseData struct {
ID string `json:"id"`
ClientOrderID string `json:"clientOrderId"`
Symbol currency.Pair `json:"symbol"`
Side string `json:"side"`
Status string `json:"status"`
Type string `json:"type"`
TimeInForce string `json:"timeInForce"`
Quantity float64 `json:"quantity,string"`
Price float64 `json:"price,string"`
CumQuantity float64 `json:"cumQuantity,string"`
PostOnly bool `json:"postOnly"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ReportType string `json:"reportType"`
OriginalRequestClientOrderID string `json:"originalRequestClientOrderId"`
}
// WsGetActiveOrdersResponse WS response
type WsGetActiveOrdersResponse struct {
Result []WsGetActiveOrdersResponseData `json:"result"`
ID int64 `json:"id"`
}
// WsGetActiveOrdersResponseData WS response data
type WsGetActiveOrdersResponseData struct {
ID string `json:"id"`
ClientOrderID string `json:"clientOrderId"`
Symbol currency.Pair `json:"symbol"`
Side string `json:"side"`
Status string `json:"status"`
Type string `json:"type"`
TimeInForce string `json:"timeInForce"`
Quantity float64 `json:"quantity,string"`
Price float64 `json:"price,string"`
CumQuantity float64 `json:"cumQuantity,string"`
PostOnly bool `json:"postOnly"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
ReportType string `json:"reportType"`
OriginalRequestClientOrderID string `json:"originalRequestClientOrderId"`
}
// WsGetTradingBalanceResponse WS response
type WsGetTradingBalanceResponse struct {
Result []WsGetTradingBalanceResponseData `json:"result"`
ID int64 `json:"id"`
}
// WsGetTradingBalanceResponseData WS response data
type WsGetTradingBalanceResponseData struct {
Currency currency.Code `json:"currency"`
Available float64 `json:"available,string"`
Reserved float64 `json:"reserved,string"`
}
// WsCancelOrderRequest WS request
type WsCancelOrderRequest struct {
Method string `json:"method"`
Params WsCancelOrderRequestData `json:"params"`
ID int64 `json:"id"`
}
// WsCancelOrderRequestData WS request data
type WsCancelOrderRequestData struct {
ClientOrderID string `json:"clientOrderId"`
}
// WsReplaceOrderRequest WS request
type WsReplaceOrderRequest struct {
Method string `json:"method"`
Params WsReplaceOrderRequestData `json:"params"`
ID int64 `json:"id,omitempty"`
}
// WsReplaceOrderRequestData WS request data
type WsReplaceOrderRequestData struct {
ClientOrderID string `json:"clientOrderId,omitempty"`
RequestClientID string `json:"requestClientId,omitempty"`
Quantity float64 `json:"quantity,string,omitempty"`
Price float64 `json:"price,string,omitempty"`
}

View File

@@ -10,9 +10,11 @@ import (
"github.com/gorilla/websocket"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/common/crypto"
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/asset"
"github.com/thrasher-/gocryptotrader/exchanges/nonce"
"github.com/thrasher-/gocryptotrader/exchanges/orderbook"
log "github.com/thrasher-/gocryptotrader/logger"
)
@@ -22,6 +24,8 @@ const (
rpcVersion = "2.0"
)
var requestID nonce.Nonce
// WsConnect starts a new connection with the websocket API
func (h *HitBTC) WsConnect() error {
if !h.Websocket.IsEnabled() || !h.IsEnabled() {
@@ -46,6 +50,11 @@ func (h *HitBTC) WsConnect() error {
}
go h.WsHandleData()
err = h.wsLogin()
if err != nil {
log.Errorf("%v - authentication failed: %v", h.Name, err)
}
h.GenerateDefaultSubscriptions()
return nil
@@ -90,86 +99,146 @@ func (h *HitBTC) WsHandleData() {
}
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)
continue
}
if init.Result {
if _, ok := init.Result.(bool); ok {
continue
}
switch init.Method {
case "ticker":
var ticker WsTicker
err := common.JSONDecode(resp.Raw, &ticker)
if err != nil {
h.Websocket.DataHandler <- err
continue
}
ts, err := time.Parse(time.RFC3339, ticker.Params.Timestamp)
if err != nil {
h.Websocket.DataHandler <- err
continue
}
h.Websocket.DataHandler <- exchange.TickerData{
Exchange: h.GetName(),
AssetType: asset.Spot,
Pair: currency.NewPairFromString(ticker.Params.Symbol),
Quantity: ticker.Params.Volume,
Timestamp: ts,
OpenPrice: ticker.Params.Open,
HighPrice: ticker.Params.High,
LowPrice: ticker.Params.Low,
}
case "snapshotOrderbook":
var obSnapshot WsOrderbook
err := common.JSONDecode(resp.Raw, &obSnapshot)
if err != nil {
h.Websocket.DataHandler <- err
continue
}
err = h.WsProcessOrderbookSnapshot(obSnapshot)
if err != nil {
h.Websocket.DataHandler <- err
continue
}
case "updateOrderbook":
var obUpdate WsOrderbook
err := common.JSONDecode(resp.Raw, &obUpdate)
if err != nil {
h.Websocket.DataHandler <- err
continue
}
h.WsProcessOrderbookUpdate(obUpdate)
case "snapshotTrades":
var tradeSnapshot WsTrade
err := common.JSONDecode(resp.Raw, &tradeSnapshot)
if err != nil {
h.Websocket.DataHandler <- err
continue
}
case "updateTrades":
var tradeUpdates WsTrade
err := common.JSONDecode(resp.Raw, &tradeUpdates)
if err != nil {
h.Websocket.DataHandler <- err
continue
}
if init.Method != "" {
h.handleSubscriptionUpdates(resp, init)
} else {
h.handleCommandResponses(resp, init)
}
}
}
}
func (h *HitBTC) handleSubscriptionUpdates(resp exchange.WebsocketResponse, init capture) {
switch init.Method {
case "ticker":
var ticker WsTicker
err := common.JSONDecode(resp.Raw, &ticker)
if err != nil {
h.Websocket.DataHandler <- err
return
}
ts, err := time.Parse(time.RFC3339, ticker.Params.Timestamp)
if err != nil {
h.Websocket.DataHandler <- err
return
}
h.Websocket.DataHandler <- exchange.TickerData{
Exchange: h.GetName(),
AssetType: asset.Spot,
Pair: currency.NewPairFromString(ticker.Params.Symbol),
Quantity: ticker.Params.Volume,
Timestamp: ts,
OpenPrice: ticker.Params.Open,
HighPrice: ticker.Params.High,
LowPrice: ticker.Params.Low,
}
case "snapshotOrderbook":
var obSnapshot WsOrderbook
err := common.JSONDecode(resp.Raw, &obSnapshot)
if err != nil {
h.Websocket.DataHandler <- err
}
err = h.WsProcessOrderbookSnapshot(obSnapshot)
if err != nil {
h.Websocket.DataHandler <- err
}
case "updateOrderbook":
var obUpdate WsOrderbook
err := common.JSONDecode(resp.Raw, &obUpdate)
if err != nil {
h.Websocket.DataHandler <- err
}
h.WsProcessOrderbookUpdate(obUpdate)
case "snapshotTrades":
var tradeSnapshot WsTrade
err := common.JSONDecode(resp.Raw, &tradeSnapshot)
if err != nil {
h.Websocket.DataHandler <- err
}
case "updateTrades":
var tradeUpdates WsTrade
err := common.JSONDecode(resp.Raw, &tradeUpdates)
if err != nil {
h.Websocket.DataHandler <- err
}
case "activeOrders":
var activeOrders WsActiveOrdersResponse
err := common.JSONDecode(resp.Raw, &activeOrders)
if err != nil {
h.Websocket.DataHandler <- err
}
h.Websocket.DataHandler <- activeOrders
case "report":
var reportData WsReportResponse
err := common.JSONDecode(resp.Raw, &reportData)
if err != nil {
h.Websocket.DataHandler <- err
}
h.Websocket.DataHandler <- reportData
}
}
func (h *HitBTC) handleCommandResponses(resp exchange.WebsocketResponse, init capture) {
switch resultType := init.Result.(type) {
case map[string]interface{}:
switch resultType["reportType"].(string) {
case "new":
var response WsSubmitOrderSuccessResponse
err := common.JSONDecode(resp.Raw, &response)
if err != nil {
h.Websocket.DataHandler <- err
}
h.Websocket.DataHandler <- response
case "canceled":
var response WsCancelOrderResponse
err := common.JSONDecode(resp.Raw, &response)
if err != nil {
h.Websocket.DataHandler <- err
}
h.Websocket.DataHandler <- response
case "replaced":
var response WsReplaceOrderResponse
err := common.JSONDecode(resp.Raw, &response)
if err != nil {
h.Websocket.DataHandler <- err
}
h.Websocket.DataHandler <- response
}
case []interface{}:
if len(resultType) == 0 {
h.Websocket.DataHandler <- fmt.Sprintf("No data returned. ID: %v", init.ID)
return
}
data := resultType[0].(map[string]interface{})
if _, ok := data["clientOrderId"]; ok {
var response WsActiveOrdersResponse
err := common.JSONDecode(resp.Raw, &response)
if err != nil {
h.Websocket.DataHandler <- err
}
h.Websocket.DataHandler <- response
} else if _, ok := data["available"]; ok {
var response WsGetTradingBalanceResponse
err := common.JSONDecode(resp.Raw, &response)
if err != nil {
h.Websocket.DataHandler <- err
}
h.Websocket.DataHandler <- response
}
}
}
// WsProcessOrderbookSnapshot processes a full orderbook snapshot to a local cache
func (h *HitBTC) WsProcessOrderbookSnapshot(ob WsOrderbook) error {
if len(ob.Params.Bid) == 0 || len(ob.Params.Ask) == 0 {
@@ -242,7 +311,12 @@ 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"}
subscriptions := []exchange.WebsocketChannelSubscription{}
var subscriptions []exchange.WebsocketChannelSubscription
if h.Websocket.CanUseAuthenticatedEndpoints() {
subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{
Channel: "subscribeReports",
})
}
enabledCurrencies := h.GetEnabledPairs(asset.Spot)
for i := range channels {
for j := range enabledCurrencies {
@@ -259,11 +333,12 @@ func (h *HitBTC) GenerateDefaultSubscriptions() {
// Subscribe sends a websocket message to receive data from the channel
func (h *HitBTC) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error {
subscribe := WsNotification{
JSONRPCVersion: rpcVersion,
Method: channelToSubscribe.Channel,
Params: params{
Method: channelToSubscribe.Channel,
}
if channelToSubscribe.Currency.String() != "" {
subscribe.Params = params{
Symbol: channelToSubscribe.Currency.String(),
},
}
}
if strings.EqualFold(channelToSubscribe.Channel, "subscribeTrades") {
subscribe.Params = params{
@@ -316,7 +391,111 @@ func (h *HitBTC) wsSend(data interface{}) error {
return err
}
if h.Verbose {
log.Debugf("%v sending message to websocket %v", h.Name, data)
log.Debugf("%v sending message to websocket %v", h.Name, string(json))
}
return h.WebsocketConn.WriteMessage(websocket.TextMessage, json)
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (h *HitBTC) wsLogin() error {
if !h.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", h.Name)
}
h.Websocket.SetCanUseAuthenticatedEndpoints(true)
nonce := fmt.Sprintf("%v", time.Now().Unix())
hmac := crypto.GetHMAC(crypto.HashSHA256, []byte(nonce), []byte(h.API.Credentials.Secret))
request := WsLoginRequest{
Method: "login",
Params: WsLoginData{
Algo: "HS256",
PKey: h.API.Credentials.Key,
Nonce: nonce,
Signature: crypto.HexEncodeToString(hmac),
},
}
err := h.wsSend(request)
if err != nil {
h.Websocket.SetCanUseAuthenticatedEndpoints(false)
return err
}
return nil
}
// wsPlaceOrder sends a websocket message to submit an order
func (h *HitBTC) wsPlaceOrder(pair currency.Pair, side string, price, quantity float64) error {
if !h.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authenticated, cannot place order", h.Name)
}
request := WsSubmitOrderRequest{
Method: "newOrder",
Params: WsSubmitOrderRequestData{
ClientOrderID: fmt.Sprintf("%v", time.Now().Unix()),
Symbol: pair,
Side: strings.ToLower(side),
Price: price,
Quantity: quantity,
},
ID: int64(requestID.GetInc()),
}
return h.wsSend(request)
}
// wsCancelOrder sends a websocket message to cancel an order
func (h *HitBTC) wsCancelOrder(clientOrderID string) error {
if !h.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authenticated, cannot place order", h.Name)
}
request := WsCancelOrderRequest{
Method: "cancelOrder",
Params: WsCancelOrderRequestData{
ClientOrderID: clientOrderID,
},
ID: int64(requestID.GetInc()),
}
return h.wsSend(request)
}
// wsReplaceOrder sends a websocket message to replace an order
func (h *HitBTC) wsReplaceOrder(clientOrderID string, quantity, price float64) error {
if !h.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authenticated, cannot place order", h.Name)
}
request := WsReplaceOrderRequest{
Method: "cancelReplaceOrder",
Params: WsReplaceOrderRequestData{
ClientOrderID: clientOrderID,
RequestClientID: fmt.Sprintf("%v", time.Now().Unix()),
Quantity: quantity,
Price: price,
},
ID: int64(requestID.GetInc()),
}
return h.wsSend(request)
}
// wsGetActiveOrders sends a websocket message to get all active orders
func (h *HitBTC) wsGetActiveOrders() error {
if !h.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authenticated, cannot place order", h.Name)
}
request := WsReplaceOrderRequest{
Method: "getOrders",
Params: WsReplaceOrderRequestData{},
ID: int64(requestID.GetInc()),
}
return h.wsSend(request)
}
// wsGetTradingBalance sends a websocket message to get trading balance
func (h *HitBTC) wsGetTradingBalance() error {
if !h.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authenticated, cannot place order", h.Name)
}
request := WsReplaceOrderRequest{
Method: "getTradingBalance",
Params: WsReplaceOrderRequestData{},
ID: int64(requestID.GetInc()),
}
return h.wsSend(request)
}

View File

@@ -427,18 +427,12 @@ func (h *HitBTC) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([
symbol := currency.NewPairDelimiter(allOrders[i].Symbol,
h.CurrencyPairs.Get(asset.Spot).ConfigFormat.Delimiter)
side := exchange.OrderSide(strings.ToUpper(allOrders[i].Side))
orderDate, err := time.Parse(time.RFC3339, allOrders[i].CreatedAt)
if err != nil {
log.Warnf("Exchange %v Func %v Order %v Could not parse date to unix with value of %v",
h.Name, "GetActiveOrders", allOrders[i].ID, allOrders[i].CreatedAt)
}
orders = append(orders, exchange.OrderDetail{
ID: allOrders[i].ID,
Amount: allOrders[i].Quantity,
Exchange: h.Name,
Price: allOrders[i].Price,
OrderDate: orderDate,
OrderDate: allOrders[i].CreatedAt,
OrderSide: side,
CurrencyPair: symbol,
})
@@ -471,18 +465,12 @@ func (h *HitBTC) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([
symbol := currency.NewPairDelimiter(allOrders[i].Symbol,
h.CurrencyPairs.Get(asset.Spot).ConfigFormat.Delimiter)
side := exchange.OrderSide(strings.ToUpper(allOrders[i].Side))
orderDate, err := time.Parse(time.RFC3339, allOrders[i].CreatedAt)
if err != nil {
log.Warnf("Exchange %v Func %v Order %v Could not parse date to unix with value of %v",
h.Name, "GetOrderHistory", allOrders[i].ID, allOrders[i].CreatedAt)
}
orders = append(orders, exchange.OrderDetail{
ID: allOrders[i].ID,
Amount: allOrders[i].Quantity,
Exchange: h.Name,
Price: allOrders[i].Price,
OrderDate: orderDate,
OrderDate: allOrders[i].CreatedAt,
OrderSide: side,
CurrencyPair: symbol,
})
@@ -506,3 +494,13 @@ func (h *HitBTC) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketCha
h.Websocket.UnsubscribeToChannels(channels)
return nil
}
// GetSubscriptions returns a copied list of subscriptions
func (h *HitBTC) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return h.Websocket.GetSubscriptions(), nil
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (h *HitBTC) AuthenticateWebsocket() error {
return h.wsLogin()
}

View File

@@ -64,9 +64,10 @@ const (
// HUOBI is the overarching type across this package
type HUOBI struct {
exchange.Base
AccountID string
WebsocketConn *websocket.Conn
wsRequestMtx sync.Mutex
AccountID string
WebsocketConn *websocket.Conn
AuthenticatedWebsocketConn *websocket.Conn
wsRequestMtx sync.Mutex
}
// GetSpotKline returns kline data

View File

@@ -9,12 +9,15 @@ import (
"strconv"
"strings"
"testing"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/common/crypto"
"github.com/thrasher-/gocryptotrader/config"
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/sharedtestvalues"
)
// Please supply you own test keys here for due diligence testing.
@@ -26,6 +29,7 @@ const (
)
var h HUOBI
var wsSetupRan bool
func TestSetDefaults(t *testing.T) {
h.SetDefaults()
@@ -39,12 +43,54 @@ func TestSetup(t *testing.T) {
t.Error("Test Failed - Huobi Setup() init error")
}
hConfig.API.AuthenticatedSupport = true
hConfig.API.AuthenticatedWebsocketSupport = true
hConfig.API.Credentials.Key = apiKey
hConfig.API.Credentials.Secret = apiSecret
h.Setup(hConfig)
}
func setupWsTests(t *testing.T) {
if wsSetupRan {
return
}
TestSetDefaults(t)
TestSetup(t)
if !h.Websocket.IsEnabled() && !h.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() {
t.Skip(exchange.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)
if err != nil {
t.Error(err)
}
err = h.wsLogin()
if err != nil {
t.Error(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
}
func TestGetSpotKline(t *testing.T) {
t.Parallel()
_, err := h.GetSpotKline(KlinesRequestParams{
@@ -592,3 +638,50 @@ func TestGetDepositAddress(t *testing.T) {
t.Error("Test Failed - GetDepositAddress() error cannot be nil")
}
}
// 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")
}
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")
}
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")
}
timer.Stop()
}

View File

@@ -1,5 +1,7 @@
package huobi
import "github.com/thrasher-/gocryptotrader/currency"
// Response stores the Huobi response information
type Response struct {
Status string `json:"status"`
@@ -271,13 +273,13 @@ type WsRequest struct {
// WsResponse defines a response from the websocket connection when there
// is an error
type WsResponse struct {
TS int64 `json:"ts"`
Status string `json:"status"`
ErrorCode string `json:"err-code"`
ErrorMessage string `json:"err-msg"`
Ping int64 `json:"ping"`
Channel string `json:"ch"`
Subscribed string `json:"subbed"`
TS int64 `json:"ts"`
Status string `json:"status"`
ErrorCode interface{} `json:"err-code"`
ErrorMessage string `json:"err-msg"`
Ping int64 `json:"ping"`
Channel string `json:"ch"`
Subscribed string `json:"subbed"`
}
// WsHeartBeat defines a heartbeat request
@@ -323,9 +325,201 @@ type WsTrade struct {
Data []struct {
Amount float64 `json:"amount"`
Timestamp int64 `json:"ts"`
ID float64 `json:"id,string"`
ID float64 `json:"id"`
Price float64 `json:"price"`
Direction string `json:"direction"`
} `json:"data"`
}
}
// WsAuthenticationRequest data for login
type WsAuthenticationRequest struct {
Op string `json:"op"`
AccessKeyID string `json:"AccessKeyId"`
SignatureMethod string `json:"SignatureMethod"`
SignatureVersion string `json:"SignatureVersion"`
Timestamp string `json:"Timestamp"`
Signature string `json:"Signature"`
}
// WsMessage defines read data from the websocket connection
type WsMessage struct {
Raw []byte
URL string
}
// WsAuthenticatedSubscriptionRequest request for subscription on authenticated connection
type WsAuthenticatedSubscriptionRequest struct {
Op string `json:"op"`
AccessKeyID string `json:"AccessKeyId"`
SignatureMethod string `json:"SignatureMethod"`
SignatureVersion string `json:"SignatureVersion"`
Timestamp string `json:"Timestamp"`
Signature string `json:"Signature"`
Topic string `json:"topic"`
}
// WsAuthenticatedAccountsListRequest request for account list authenticated connection
type WsAuthenticatedAccountsListRequest struct {
Op string `json:"op"`
AccessKeyID string `json:"AccessKeyId"`
SignatureMethod string `json:"SignatureMethod"`
SignatureVersion string `json:"SignatureVersion"`
Timestamp string `json:"Timestamp"`
Signature string `json:"Signature"`
Topic string `json:"topic"`
Symbol currency.Pair `json:"symbol"`
}
// WsAuthenticatedOrderDetailsRequest request for order details authenticated connection
type WsAuthenticatedOrderDetailsRequest struct {
Op string `json:"op"`
AccessKeyID string `json:"AccessKeyId"`
SignatureMethod string `json:"SignatureMethod"`
SignatureVersion string `json:"SignatureVersion"`
Timestamp string `json:"Timestamp"`
Signature string `json:"Signature"`
Topic string `json:"topic"`
OrderID string `json:"order-id"`
}
// WsAuthenticatedOrdersListRequest request for orderslist authenticated connection
type WsAuthenticatedOrdersListRequest struct {
Op string `json:"op"`
AccessKeyID string `json:"AccessKeyId"`
SignatureMethod string `json:"SignatureMethod"`
SignatureVersion string `json:"SignatureVersion"`
Timestamp string `json:"Timestamp"`
Signature string `json:"Signature"`
Topic string `json:"topic"`
States string `json:"states"`
AccountID int64 `json:"account-id"`
Symbol currency.Pair `json:"symbol"`
}
// WsAuthenticatedDataResponse response from authenticated connection
type WsAuthenticatedDataResponse struct {
Op string `json:"op,omitempty"`
Ts int64 `json:"ts,omitempty"`
Topic string `json:"topic,omitempty"`
ErrorCode int64 `json:"err-code,omitempty"`
ErrorMessage string `json:"err-msg,omitempty"`
Ping int64 `json:"ping,omitempty"`
CID string `json:"cid,omitempty"`
}
// WsAuthenticatedAccountsResponse response from Accounts authenticated subscription
type WsAuthenticatedAccountsResponse struct {
WsAuthenticatedDataResponse
Data WsAuthenticatedAccountsResponseData `json:"data"`
}
// WsAuthenticatedAccountsResponseData account data
type WsAuthenticatedAccountsResponseData struct {
Event string `json:"event"`
List []WsAuthenticatedAccountsResponseDataList `json:"list"`
}
// WsAuthenticatedAccountsResponseDataList detailed account data
type WsAuthenticatedAccountsResponseDataList struct {
AccountID int64 `json:"account-id"`
Currency string `json:"currency"`
Type string `json:"type"`
Balance float64 `json:"balance,string"`
}
// WsAuthenticatedOrdersUpdateResponse response from OrdersUpdate authenticated subscription
type WsAuthenticatedOrdersUpdateResponse struct {
WsAuthenticatedDataResponse
Data WsAuthenticatedOrdersUpdateResponseData `json:"data"`
}
// WsAuthenticatedOrdersUpdateResponseData order updatedata
type WsAuthenticatedOrdersUpdateResponseData struct {
UnfilledAmount float64 `json:"unfilled-amount,string"`
FilledAmount float64 `json:"filled-amount,string"`
Price float64 `json:"price,string"`
OrderID int64 `json:"order-id"`
Symbol currency.Pair `json:"symbol"`
MatchID int64 `json:"match-id"`
FilledCashAmount float64 `json:"filled-cash-amount,string"`
Role string `json:"role"`
OrderState string `json:"order-state"`
}
// WsAuthenticatedOrdersResponse response from Orders authenticated subscription
type WsAuthenticatedOrdersResponse struct {
WsAuthenticatedDataResponse
Data []WsAuthenticatedOrdersResponseData `json:"data"`
}
// WsAuthenticatedOrdersResponseData order data
type WsAuthenticatedOrdersResponseData struct {
SeqID int64 `json:"seq-id"`
OrderID int64 `json:"order-id"`
Symbol currency.Pair `json:"symbol"`
AccountID int64 `json:"account-id"`
OrderAmount float64 `json:"order-amount,string"`
OrderPrice float64 `json:"order-price,string"`
CreatedAt int64 `json:"created-at"`
OrderType string `json:"order-type"`
OrderSource string `json:"order-source"`
OrderState string `json:"order-state"`
Role string `json:"role"`
Price float64 `json:"price,string"`
FilledAmount float64 `json:"filled-amount,string"`
UnfilledAmount float64 `json:"unfilled-amount,string"`
FilledCashAmount float64 `json:"filled-cash-amount,string"`
FilledFees float64 `json:"filled-fees,string"`
}
// WsAuthenticatedAccountsListResponse response from AccountsList authenticated endpoint
type WsAuthenticatedAccountsListResponse struct {
WsAuthenticatedDataResponse
Data []WsAuthenticatedAccountsListResponseData `json:"data"`
}
// WsAuthenticatedAccountsListResponseData account data
type WsAuthenticatedAccountsListResponseData struct {
ID int64 `json:"id"`
Type string `json:"type"`
State string `json:"state"`
List []WsAuthenticatedAccountsListResponseDataList `json:"list"`
}
// WsAuthenticatedAccountsListResponseDataList detailed account data
type WsAuthenticatedAccountsListResponseDataList struct {
Currency string `json:"currency"`
Type string `json:"type"`
Balance float64 `json:"balance,string"`
}
// WsAuthenticatedOrdersListResponse response from OrdersList authenticated endpoint
type WsAuthenticatedOrdersListResponse struct {
WsAuthenticatedDataResponse
Data []WsAuthenticatedOrdersListResponseData `json:"data"`
}
// WsAuthenticatedOrdersListResponseData contains order details
type WsAuthenticatedOrdersListResponseData struct {
ID int64 `json:"id"`
Symbol currency.Pair `json:"symbol"`
AccountID int64 `json:"account-id"`
Amount float64 `json:"amount,string"`
Price float64 `json:"price,string"`
CreatedAt int64 `json:"created-at"`
Type string `json:"type"`
FilledAmount float64 `json:"filled-amount,string"`
FilledCashAmount float64 `json:"filled-cash-amount,string"`
FilledFees float64 `json:"filled-fees,string"`
FinishedAt int64 `json:"finished-at"`
Source string `json:"source"`
State string `json:"state"`
CanceledAt int64 `json:"canceled-at"`
}
// WsAuthenticatedOrderDetailResponse response from OrderDetail authenticated endpoint
type WsAuthenticatedOrderDetailResponse struct {
WsAuthenticatedDataResponse
Data WsAuthenticatedOrdersListResponseData `json:"data"`
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/gorilla/websocket"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/common/crypto"
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/asset"
@@ -21,12 +22,33 @@ import (
)
const (
huobiSocketIOAddress = "wss://api.huobi.pro/hbus/ws"
wsMarketKline = "market.%s.kline.1min"
wsMarketDepth = "market.%s.depth.step0"
wsMarketTrade = "market.%s.trade.detail"
baseWSURL = "wss://api.huobi.pro"
wsMarketURL = baseWSURL + "/ws"
wsMarketKline = "market.%s.kline.1min"
wsMarketDepth = "market.%s.depth.step0"
wsMarketTrade = "market.%s.trade.detail"
wsAccountsOrdersEndPoint = "/ws/v1"
wsAccountsList = "accounts.list"
wsOrdersList = "orders.list"
wsOrdersDetail = "orders.detail"
wsAccountsOrdersURL = baseWSURL + wsAccountsOrdersEndPoint
wsAccountListEndpoint = wsAccountsOrdersEndPoint + "/" + wsAccountsList
wsOrdersListEndpoint = wsAccountsOrdersEndPoint + "/" + wsOrdersList
wsOrdersDetailEndpoint = wsAccountsOrdersEndPoint + "/" + wsOrdersDetail
wsDateTimeFormatting = "2006-01-02T15:04:05"
signatureMethod = "HmacSHA256"
signatureVersion = "2"
requestOp = "req"
authOp = "auth"
)
// Instantiates a communications channel between websocket connections
var comms = make(chan WsMessage, 1)
// WsConnect initiates a new websocket connection
func (h *HUOBI) WsConnect() error {
if !h.Websocket.IsEnabled() || !h.IsEnabled() {
@@ -44,140 +66,264 @@ func (h *HUOBI) WsConnect() error {
dialer.Proxy = http.ProxyURL(proxy)
}
var err error
h.WebsocketConn, _, err = dialer.Dial(h.Websocket.GetWebsocketURL(), http.Header{})
err := h.wsDial(&dialer)
if err != nil {
return err
}
err = h.wsAuthenticatedDial(&dialer)
if err != nil {
log.Errorf("%v - authenticated dial failed: %v", h.Name, err)
}
err = h.wsLogin()
if err != nil {
log.Errorf("%v - authentication failed: %v", h.Name, err)
}
go h.WsHandleData()
h.GenerateDefaultSubscriptions()
return nil
}
// WsReadData reads data from the websocket connection
func (h *HUOBI) WsReadData() (exchange.WebsocketResponse, error) {
_, resp, err := h.WebsocketConn.ReadMessage()
func (h *HUOBI) wsDial(dialer *websocket.Dialer) error {
var err error
var conStatus *http.Response
h.WebsocketConn, conStatus, err = dialer.Dial(wsMarketURL, http.Header{})
if err != nil {
return exchange.WebsocketResponse{}, err
return fmt.Errorf("%v %v %v Error: %v", wsMarketURL, conStatus, conStatus.StatusCode, err)
}
go h.wsMultiConnectionFunnel(h.WebsocketConn, wsMarketURL)
return nil
}
h.Websocket.TrafficAlert <- struct{}{}
b := bytes.NewReader(resp)
gReader, err := gzip.NewReader(b)
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{})
if err != nil {
return exchange.WebsocketResponse{}, err
return fmt.Errorf("%v %v %v Error: %v", wsAccountsOrdersURL, conStatus, conStatus.StatusCode, err)
}
go h.wsMultiConnectionFunnel(h.AuthenticatedWebsocketConn, wsAccountsOrdersURL)
return nil
}
unzipped, err := ioutil.ReadAll(gReader)
if err != nil {
return exchange.WebsocketResponse{}, err
// wsMultiConnectionFunnel manages data from multiple endpoints and passes it to a channel
func (h *HUOBI) wsMultiConnectionFunnel(ws *websocket.Conn, url string) {
h.Websocket.Wg.Add(1)
defer h.Websocket.Wg.Done()
for {
select {
case <-h.Websocket.ShutdownC:
return
default:
_, 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}
}
}
gReader.Close()
return exchange.WebsocketResponse{Raw: unzipped}, nil
}
// WsHandleData handles data read from the websocket connection
func (h *HUOBI) WsHandleData() {
h.Websocket.Wg.Add(1)
defer func() {
h.Websocket.Wg.Done()
}()
defer h.Websocket.Wg.Done()
for {
select {
case <-h.Websocket.ShutdownC:
return
default:
resp, err := h.WsReadData()
if err != nil {
h.Websocket.DataHandler <- err
return
case resp := <-comms:
if h.Verbose {
log.Debugf("%v: %v: %v", h.Name, resp.URL, string(resp.Raw))
}
var init WsResponse
err = common.JSONDecode(resp.Raw, &init)
if err != nil {
h.Websocket.DataHandler <- err
continue
switch resp.URL {
case wsMarketURL:
h.wsHandleMarketData(resp)
case wsAccountsOrdersURL:
h.wsHandleAuthenticatedData(resp)
}
}
}
}
if init.Status == "error" {
h.Websocket.DataHandler <- fmt.Errorf("huobi.go Websocker error %s %s",
init.ErrorCode,
init.ErrorMessage)
continue
}
func (h *HUOBI) wsHandleAuthenticatedData(resp WsMessage) {
var init WsAuthenticatedDataResponse
err := common.JSONDecode(resp.Raw, &init)
if err != nil {
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}`)
if err != nil {
log.Error(err)
}
return
}
if init.Subscribed != "" {
continue
}
if init.Op == "sub" {
if h.Verbose {
log.Debugf("%v: %v: Successfully subscribed to %v", h.Name, resp.URL, init.Topic)
}
return
}
if init.Ping != 0 {
err = h.WebsocketConn.WriteJSON(`{"pong":1337}`)
if err != nil {
log.Error(err)
}
continue
}
switch {
case strings.EqualFold(init.Op, authOp):
h.Websocket.SetCanUseAuthenticatedEndpoints(true)
var response WsAuthenticatedDataResponse
err := common.JSONDecode(resp.Raw, &response)
if err != nil {
h.Websocket.DataHandler <- err
}
h.Websocket.DataHandler <- response
case strings.EqualFold(init.Topic, "accounts"):
var response WsAuthenticatedAccountsResponse
err := common.JSONDecode(resp.Raw, &response)
if err != nil {
h.Websocket.DataHandler <- err
}
h.Websocket.DataHandler <- response
case strings.Contains(init.Topic, "orders") &&
strings.Contains(init.Topic, "update"):
var response WsAuthenticatedOrdersUpdateResponse
err := common.JSONDecode(resp.Raw, &response)
if err != nil {
h.Websocket.DataHandler <- err
}
h.Websocket.DataHandler <- response
case strings.Contains(init.Topic, "orders"):
var response WsAuthenticatedOrdersResponse
err := common.JSONDecode(resp.Raw, &response)
if err != nil {
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
}
}
switch {
case strings.Contains(init.Channel, "depth"):
var depth WsDepth
err := common.JSONDecode(resp.Raw, &depth)
if err != nil {
h.Websocket.DataHandler <- err
continue
}
func (h *HUOBI) wsHandleMarketData(resp WsMessage) {
var init WsResponse
err := common.JSONDecode(resp.Raw, &init)
if err != nil {
h.Websocket.DataHandler <- err
return
}
if init.Status == "error" {
h.Websocket.DataHandler <- fmt.Errorf("%v %v Websocket error %s %s",
h.Name,
resp.URL,
init.ErrorCode,
init.ErrorMessage)
return
}
if init.Subscribed != "" {
return
}
if init.Ping != 0 {
err = h.WebsocketConn.WriteJSON(`{"pong":1337}`)
if err != nil {
log.Error(err)
}
return
}
data := strings.Split(depth.Channel, ".")
h.WsProcessOrderbook(&depth, data[1])
case strings.Contains(init.Channel, "kline"):
var kline WsKline
err := common.JSONDecode(resp.Raw, &kline)
if err != nil {
h.Websocket.DataHandler <- err
continue
}
data := strings.Split(kline.Channel, ".")
h.Websocket.DataHandler <- exchange.KlineData{
Timestamp: time.Unix(0, kline.Timestamp),
Exchange: h.GetName(),
AssetType: asset.Spot,
Pair: currency.NewPairFromString(data[1]),
OpenPrice: kline.Tick.Open,
ClosePrice: kline.Tick.Close,
HighPrice: kline.Tick.High,
LowPrice: kline.Tick.Low,
Volume: kline.Tick.Volume,
}
case strings.Contains(init.Channel, "trade"):
var trade WsTrade
err := common.JSONDecode(resp.Raw, &trade)
if err != nil {
h.Websocket.DataHandler <- err
continue
}
data := strings.Split(trade.Channel, ".")
h.Websocket.DataHandler <- exchange.TradeData{
Exchange: h.GetName(),
AssetType: asset.Spot,
CurrencyPair: currency.NewPairFromString(data[1]),
Timestamp: time.Unix(0, trade.Tick.Timestamp),
}
}
switch {
case strings.Contains(init.Channel, "depth"):
var depth WsDepth
err := common.JSONDecode(resp.Raw, &depth)
if err != nil {
h.Websocket.DataHandler <- err
return
}
data := strings.Split(depth.Channel, ".")
h.WsProcessOrderbook(&depth, data[1])
case strings.Contains(init.Channel, "kline"):
var kline WsKline
err := common.JSONDecode(resp.Raw, &kline)
if err != nil {
h.Websocket.DataHandler <- err
return
}
data := strings.Split(kline.Channel, ".")
h.Websocket.DataHandler <- exchange.KlineData{
Timestamp: time.Unix(0, kline.Timestamp),
Exchange: h.GetName(),
AssetType: "SPOT",
Pair: currency.NewPairFromString(data[1]),
OpenPrice: kline.Tick.Open,
ClosePrice: kline.Tick.Close,
HighPrice: kline.Tick.High,
LowPrice: kline.Tick.Low,
Volume: kline.Tick.Volume,
}
case strings.Contains(init.Channel, "trade"):
var trade WsTrade
err := common.JSONDecode(resp.Raw, &trade)
if err != nil {
h.Websocket.DataHandler <- err
return
}
data := strings.Split(trade.Channel, ".")
h.Websocket.DataHandler <- exchange.TradeData{
Exchange: h.GetName(),
AssetType: "SPOT",
CurrencyPair: currency.NewPairFromString(data[1]),
Timestamp: time.Unix(0, trade.Tick.Timestamp),
}
}
}
@@ -222,8 +368,14 @@ 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
if h.Websocket.CanUseAuthenticatedEndpoints() {
channels = append(channels, "orders.%v", "orders.%v.update")
subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{
Channel: "accounts",
})
}
enabledCurrencies := h.GetEnabledPairs(asset.Spot)
subscriptions := []exchange.WebsocketChannelSubscription{}
for i := range channels {
for j := range enabledCurrencies {
enabledCurrencies[j].Delimiter = ""
@@ -239,11 +391,11 @@ func (h *HUOBI) GenerateDefaultSubscriptions() {
// Subscribe sends a websocket message to receive data from the channel
func (h *HUOBI) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error {
subscriptionRequest := WsRequest{Subscribe: channelToSubscribe.Channel}
if h.Verbose {
log.Debugf("Subscription: %v", subscriptionRequest)
if strings.Contains(channelToSubscribe.Channel, "orders.") ||
strings.Contains(channelToSubscribe.Channel, "accounts") {
return h.wsAuthenticatedSubscribe("sub", wsAccountsOrdersEndPoint+channelToSubscribe.Channel, channelToSubscribe.Channel)
}
subscription, err := common.JSONEncode(subscriptionRequest)
subscription, err := common.JSONEncode(WsRequest{Subscribe: channelToSubscribe.Channel})
if err != nil {
return err
}
@@ -252,6 +404,10 @@ func (h *HUOBI) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscripti
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (h *HUOBI) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error {
if strings.Contains(channelToSubscribe.Channel, "orders.") ||
strings.Contains(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
@@ -268,3 +424,125 @@ func (h *HUOBI) wsSend(data []byte) error {
}
return h.WebsocketConn.WriteMessage(websocket.TextMessage, data)
}
func (h *HUOBI) wsLogin() error {
if !h.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", h.Name)
}
h.Websocket.SetCanUseAuthenticatedEndpoints(true)
timestamp := time.Now().UTC().Format(wsDateTimeFormatting)
request := WsAuthenticationRequest{
Op: authOp,
AccessKeyID: h.API.Credentials.Key,
SignatureMethod: signatureMethod,
SignatureVersion: signatureVersion,
Timestamp: timestamp,
}
hmac := h.wsGenerateSignature(timestamp, wsAccountsOrdersEndPoint)
request.Signature = crypto.Base64Encode(hmac)
err := h.wsAuthenticatedSend(request)
if err != nil {
h.Websocket.SetCanUseAuthenticatedEndpoints(false)
return err
}
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.API.Credentials.Key)
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 crypto.GetHMAC(crypto.HashSHA256, []byte(payload), []byte(h.API.Credentials.Secret))
}
func (h *HUOBI) wsAuthenticatedSubscribe(operation, endpoint, topic string) error {
timestamp := time.Now().UTC().Format(wsDateTimeFormatting)
request := WsAuthenticatedSubscriptionRequest{
Op: operation,
AccessKeyID: h.API.Credentials.Key,
SignatureMethod: signatureMethod,
SignatureVersion: signatureVersion,
Timestamp: timestamp,
Topic: topic,
}
hmac := h.wsGenerateSignature(timestamp, endpoint)
request.Signature = crypto.Base64Encode(hmac)
return h.wsAuthenticatedSend(request)
}
func (h *HUOBI) wsGetAccountsList(pair currency.Pair) error {
if !h.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authenticated cannot get accounts list", h.Name)
}
timestamp := time.Now().UTC().Format(wsDateTimeFormatting)
request := WsAuthenticatedAccountsListRequest{
Op: requestOp,
AccessKeyID: h.API.Credentials.Key,
SignatureMethod: signatureMethod,
SignatureVersion: signatureVersion,
Timestamp: timestamp,
Topic: wsAccountsList,
Symbol: pair,
}
hmac := h.wsGenerateSignature(timestamp, wsAccountListEndpoint)
request.Signature = crypto.Base64Encode(hmac)
return h.wsAuthenticatedSend(request)
}
func (h *HUOBI) wsGetOrdersList(accountID int64, pair currency.Pair) error {
if !h.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authenticated cannot get orders list", h.Name)
}
timestamp := time.Now().UTC().Format(wsDateTimeFormatting)
request := WsAuthenticatedOrdersListRequest{
Op: requestOp,
AccessKeyID: h.API.Credentials.Key,
SignatureMethod: signatureMethod,
SignatureVersion: signatureVersion,
Timestamp: timestamp,
Topic: wsOrdersList,
AccountID: accountID,
Symbol: pair.Lower(),
States: "submitted,partial-filled",
}
hmac := h.wsGenerateSignature(timestamp, wsOrdersListEndpoint)
request.Signature = crypto.Base64Encode(hmac)
return h.wsAuthenticatedSend(request)
}
func (h *HUOBI) wsGetOrderDetails(orderID string) error {
if !h.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authenticated cannot get order details", h.Name)
}
timestamp := time.Now().UTC().Format(wsDateTimeFormatting)
request := WsAuthenticatedOrderDetailsRequest{
Op: requestOp,
AccessKeyID: h.API.Credentials.Key,
SignatureMethod: signatureMethod,
SignatureVersion: signatureVersion,
Timestamp: timestamp,
Topic: wsOrdersDetail,
OrderID: orderID,
}
hmac := h.wsGenerateSignature(timestamp, wsOrdersDetailEndpoint)
request.Signature = crypto.Base64Encode(hmac)
return h.wsAuthenticatedSend(request)
}

View File

@@ -117,7 +117,7 @@ func (h *HUOBI) Setup(exch *config.ExchangeConfig) error {
exch.Name,
exch.Features.Enabled.Websocket,
exch.Verbose,
huobiSocketIOAddress,
wsMarketURL,
exch.API.Endpoints.WebsocketURL)
}
@@ -133,7 +133,7 @@ func (h *HUOBI) Start(wg *sync.WaitGroup) {
// Run implements the HUOBI wrapper
func (h *HUOBI) Run() {
if h.Verbose {
log.Debugf("%s Websocket: %s (url: %s).\n", h.GetName(), common.IsEnabled(h.Websocket.IsEnabled()), huobiSocketIOAddress)
log.Debugf("%s Websocket: %s (url: %s).\n", h.GetName(), common.IsEnabled(h.Websocket.IsEnabled()), wsMarketURL)
h.PrintEnabledPairs()
}
@@ -635,3 +635,13 @@ func (h *HUOBI) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChan
h.Websocket.UnsubscribeToChannels(channels)
return nil
}
// GetSubscriptions returns a copied list of subscriptions
func (h *HUOBI) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return h.Websocket.GetSubscriptions(), nil
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (h *HUOBI) AuthenticateWebsocket() error {
return h.wsLogin()
}

View File

@@ -60,7 +60,8 @@ const (
// HUOBIHADAX is the overarching type across this package
type HUOBIHADAX struct {
WebsocketConn *websocket.Conn
WebsocketConn *websocket.Conn
AuthenticatedWebsocketConn *websocket.Conn
exchange.Base
wsRequestMtx sync.Mutex
}

View File

@@ -4,12 +4,15 @@ import (
"fmt"
"strconv"
"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/asset"
"github.com/thrasher-/gocryptotrader/exchanges/sharedtestvalues"
)
// Please supply your own APIKEYS here for due diligence testing
@@ -22,6 +25,7 @@ const (
)
var h HUOBIHADAX
var wsSetupRan bool
// getDefaultConfig returns a default hadax config
func getDefaultConfig() config.ExchangeConfig {
@@ -89,12 +93,54 @@ func TestSetup(t *testing.T) {
t.Error("Test Failed - HuobiHadax Setup() init error")
}
hadaxConfig.API.AuthenticatedSupport = true
hadaxConfig.API.AuthenticatedWebsocketSupport = true
hadaxConfig.API.Credentials.Key = apiKey
hadaxConfig.API.Credentials.Secret = apiSecret
h.Setup(hadaxConfig)
}
func setupWsTests(t *testing.T) {
if wsSetupRan {
return
}
TestSetDefaults(t)
TestSetup(t)
if !h.Websocket.IsEnabled() && !h.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() {
t.Skip(exchange.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)
if err != nil {
t.Error(err)
}
err = h.wsLogin()
if err != nil {
t.Error(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
}
func TestGetSpotKline(t *testing.T) {
t.Parallel()
_, err := h.GetSpotKline(KlinesRequestParams{
@@ -627,3 +673,50 @@ func TestGetDepositAddress(t *testing.T) {
t.Error("Test Failed - GetDepositAddress() error cannot be nil")
}
}
// 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")
}
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")
}
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")
}
timer.Stop()
}

View File

@@ -1,5 +1,9 @@
package huobihadax
import (
"github.com/thrasher-/gocryptotrader/currency"
)
// Response stores the Huobi response information
type Response struct {
Status string `json:"status"`
@@ -263,13 +267,13 @@ type WsRequest struct {
// WsResponse defines a response from the websocket connection when there
// is an error
type WsResponse struct {
TS int64 `json:"ts"`
Status string `json:"status"`
ErrorCode string `json:"err-code"`
ErrorMessage string `json:"err-msg"`
Ping int64 `json:"ping"`
Channel string `json:"ch"`
Subscribed string `json:"subbed"`
TS int64 `json:"ts"`
Status string `json:"status"`
ErrorCode interface{} `json:"err-code"`
ErrorMessage string `json:"err-msg"`
Ping int64 `json:"ping"`
Channel string `json:"ch"`
Subscribed string `json:"subbed"`
}
// WsHeartBeat defines a heartbeat request
@@ -313,11 +317,203 @@ type WsTrade struct {
ID int64 `json:"id"`
Timestamp int64 `json:"ts"`
Data []struct {
ID float64 `json:"id"`
Timestamp int64 `json:"ts"`
Amount float64 `json:"amount"`
Timestamp int64 `json:"ts"`
ID float64 `json:"id"`
Price float64 `json:"price"`
Direction string `json:"direction"`
} `json:"data"`
}
}
// WsAuthenticationRequest data for login
type WsAuthenticationRequest struct {
Op string `json:"op"`
AccessKeyID string `json:"AccessKeyId"`
SignatureMethod string `json:"SignatureMethod"`
SignatureVersion string `json:"SignatureVersion"`
Timestamp string `json:"Timestamp"`
Signature string `json:"Signature"`
}
// WsMessage defines read data from the websocket connection
type WsMessage struct {
Raw []byte
URL string
}
// WsAuthenticatedSubscriptionRequest request for subscription on authenticated connection
type WsAuthenticatedSubscriptionRequest struct {
Op string `json:"op"`
AccessKeyID string `json:"AccessKeyId"`
SignatureMethod string `json:"SignatureMethod"`
SignatureVersion string `json:"SignatureVersion"`
Timestamp string `json:"Timestamp"`
Signature string `json:"Signature"`
Topic string `json:"topic"`
}
// WsAuthenticatedAccountsListRequest request for account list authenticated connection
type WsAuthenticatedAccountsListRequest struct {
Op string `json:"op"`
AccessKeyID string `json:"AccessKeyId"`
SignatureMethod string `json:"SignatureMethod"`
SignatureVersion string `json:"SignatureVersion"`
Timestamp string `json:"Timestamp"`
Signature string `json:"Signature"`
Topic string `json:"topic"`
Symbol currency.Pair `json:"symbol"`
}
// WsAuthenticatedOrderDetailsRequest request for order details authenticated connection
type WsAuthenticatedOrderDetailsRequest struct {
Op string `json:"op"`
AccessKeyID string `json:"AccessKeyId"`
SignatureMethod string `json:"SignatureMethod"`
SignatureVersion string `json:"SignatureVersion"`
Timestamp string `json:"Timestamp"`
Signature string `json:"Signature"`
Topic string `json:"topic"`
OrderID string `json:"order-id"`
}
// WsAuthenticatedOrdersListRequest request for orderslist authenticated connection
type WsAuthenticatedOrdersListRequest struct {
Op string `json:"op"`
AccessKeyID string `json:"AccessKeyId"`
SignatureMethod string `json:"SignatureMethod"`
SignatureVersion string `json:"SignatureVersion"`
Timestamp string `json:"Timestamp"`
Signature string `json:"Signature"`
Topic string `json:"topic"`
States string `json:"states"`
AccountID int64 `json:"account-id"`
Symbol currency.Pair `json:"symbol"`
}
// WsAuthenticatedDataResponse response from authenticated connection
type WsAuthenticatedDataResponse struct {
Op string `json:"op,omitempty"`
Ts int64 `json:"ts,omitempty"`
Topic string `json:"topic,omitempty"`
ErrorCode int64 `json:"err-code,omitempty"`
ErrorMessage string `json:"err-msg,omitempty"`
Ping int64 `json:"ping,omitempty"`
CID string `json:"cid,omitempty"`
}
// WsAuthenticatedAccountsResponse response from Accounts authenticated subscription
type WsAuthenticatedAccountsResponse struct {
WsAuthenticatedDataResponse
Data WsAuthenticatedAccountsResponseData `json:"data"`
}
// WsAuthenticatedAccountsResponseData account data
type WsAuthenticatedAccountsResponseData struct {
Event string `json:"event"`
List []WsAuthenticatedAccountsResponseDataList `json:"list"`
}
// WsAuthenticatedAccountsResponseDataList detailed account data
type WsAuthenticatedAccountsResponseDataList struct {
AccountID int64 `json:"account-id"`
Currency string `json:"currency"`
Type string `json:"type"`
Balance float64 `json:"balance,string"`
}
// WsAuthenticatedOrdersUpdateResponse response from OrdersUpdate authenticated subscription
type WsAuthenticatedOrdersUpdateResponse struct {
WsAuthenticatedDataResponse
Data WsAuthenticatedOrdersUpdateResponseData `json:"data"`
}
// WsAuthenticatedOrdersUpdateResponseData order updatedata
type WsAuthenticatedOrdersUpdateResponseData struct {
UnfilledAmount float64 `json:"unfilled-amount,string"`
FilledAmount float64 `json:"filled-amount,string"`
Price float64 `json:"price,string"`
OrderID int64 `json:"order-id"`
Symbol currency.Pair `json:"symbol"`
MatchID int64 `json:"match-id"`
FilledCashAmount float64 `json:"filled-cash-amount,string"`
Role string `json:"role"`
OrderState string `json:"order-state"`
}
// WsAuthenticatedOrdersResponse response from Orders authenticated subscription
type WsAuthenticatedOrdersResponse struct {
WsAuthenticatedDataResponse
Data []WsAuthenticatedOrdersResponseData `json:"data"`
}
// WsAuthenticatedOrdersResponseData order data
type WsAuthenticatedOrdersResponseData struct {
SeqID int64 `json:"seq-id"`
OrderID int64 `json:"order-id"`
Symbol currency.Pair `json:"symbol"`
AccountID int64 `json:"account-id"`
OrderAmount float64 `json:"order-amount,string"`
OrderPrice float64 `json:"order-price,string"`
CreatedAt int64 `json:"created-at"`
OrderType string `json:"order-type"`
OrderSource string `json:"order-source"`
OrderState string `json:"order-state"`
Role string `json:"role"`
Price float64 `json:"price,string"`
FilledAmount float64 `json:"filled-amount,string"`
UnfilledAmount float64 `json:"unfilled-amount,string"`
FilledCashAmount float64 `json:"filled-cash-amount,string"`
FilledFees float64 `json:"filled-fees,string"`
}
// WsAuthenticatedAccountsListResponse response from AccountsList authenticated endpoint
type WsAuthenticatedAccountsListResponse struct {
WsAuthenticatedDataResponse
Data []WsAuthenticatedAccountsListResponseData `json:"data"`
}
// WsAuthenticatedAccountsListResponseData account data
type WsAuthenticatedAccountsListResponseData struct {
ID int64 `json:"id"`
Type string `json:"type"`
State string `json:"state"`
List []WsAuthenticatedAccountsListResponseDataList `json:"list"`
}
// WsAuthenticatedAccountsListResponseDataList detailed account data
type WsAuthenticatedAccountsListResponseDataList struct {
Currency string `json:"currency"`
Type string `json:"type"`
Balance float64 `json:"balance,string"`
}
// WsAuthenticatedOrdersListResponse response from OrdersList authenticated endpoint
type WsAuthenticatedOrdersListResponse struct {
WsAuthenticatedDataResponse
Data []WsAuthenticatedOrdersListResponseData `json:"data"`
}
// WsAuthenticatedOrdersListResponseData contains order details
type WsAuthenticatedOrdersListResponseData struct {
ID int64 `json:"id"`
Symbol currency.Pair `json:"symbol"`
AccountID int64 `json:"account-id"`
Amount float64 `json:"amount,string"`
Price float64 `json:"price,string"`
CreatedAt int64 `json:"created-at"`
Type string `json:"type"`
FilledAmount float64 `json:"filled-amount,string"`
FilledCashAmount float64 `json:"filled-cash-amount,string"`
FilledFees float64 `json:"filled-fees,string"`
FinishedAt int64 `json:"finished-at"`
Source string `json:"source"`
State string `json:"state"`
CanceledAt int64 `json:"canceled-at"`
}
// WsAuthenticatedOrderDetailResponse response from OrderDetail authenticated endpoint
type WsAuthenticatedOrderDetailResponse struct {
WsAuthenticatedDataResponse
Data WsAuthenticatedOrdersListResponseData `json:"data"`
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/gorilla/websocket"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/common/crypto"
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/asset"
@@ -20,15 +21,34 @@ import (
log "github.com/thrasher-/gocryptotrader/logger"
)
// WS URL values
const (
huobiGlobalWebsocketEndpoint = "wss://api.huobi.pro/ws"
huobiGlobalAssetWebsocketEndpoint = "wss://api.huobi.pro/ws/v1"
huobiGlobalContractWebsocketEndpoint = "wss://www.hbdm.com/ws"
wsMarketKline = "market.%s.kline.1min"
wsMarketDepth = "market.%s.depth.step0"
wsMarketTrade = "market.%s.trade.detail"
HuobiHadaxSocketIOAddress = "wss://api.hadax.com/ws"
wsMarketKline = "market.%s.kline.1min"
wsMarketDepth = "market.%s.depth.step0"
wsMarketTrade = "market.%s.trade.detail"
wsAccountsOrdersBaseURL = "wss://api.huobi.pro"
wsAccountsOrdersEndPoint = "/ws/v1"
wsAccountsList = "accounts.list"
wsOrdersList = "orders.list"
wsOrdersDetail = "orders.detail"
wsAccountsOrdersURL = wsAccountsOrdersBaseURL + wsAccountsOrdersEndPoint
wsAccountListEndpoint = wsAccountsOrdersEndPoint + "/" + wsAccountsList
wsOrdersListEndpoint = wsAccountsOrdersEndPoint + "/" + wsOrdersList
wsOrdersDetailEndpoint = wsAccountsOrdersEndPoint + "/" + wsOrdersDetail
wsDateTimeFormatting = "2006-01-02T15:04:05"
signatureMethod = "HmacSHA256"
signatureVersion = "2"
requestOp = "req"
authOp = "auth"
)
// Instantiates a communications channel between websocket connections
var comms = make(chan WsMessage, 1)
// WsConnect initiates a new websocket connection
func (h *HUOBIHADAX) WsConnect() error {
if !h.Websocket.IsEnabled() || !h.IsEnabled() {
@@ -46,141 +66,264 @@ func (h *HUOBIHADAX) WsConnect() error {
dialer.Proxy = http.ProxyURL(proxy)
}
var err error
h.WebsocketConn, _, err = dialer.Dial(h.Websocket.GetWebsocketURL(), http.Header{})
err := h.wsDial(&dialer)
if err != nil {
return err
}
err = h.wsAuthenticatedDial(&dialer)
if err != nil {
log.Errorf("%v - authenticated dial failed: %v", h.Name, err)
}
err = h.wsLogin()
if err != nil {
log.Errorf("%v - authentication failed: %v", h.Name, err)
}
go h.WsHandleData()
h.GenerateDefaultSubscriptions()
return nil
}
// WsReadData reads data from the websocket connection
func (h *HUOBIHADAX) WsReadData() (exchange.WebsocketResponse, error) {
_, resp, err := h.WebsocketConn.ReadMessage()
func (h *HUOBIHADAX) wsDial(dialer *websocket.Dialer) error {
var err error
var conStatus *http.Response
h.WebsocketConn, conStatus, err = dialer.Dial(HuobiHadaxSocketIOAddress, http.Header{})
if err != nil {
return exchange.WebsocketResponse{}, err
return fmt.Errorf("%v %v %v Error: %v", HuobiHadaxSocketIOAddress, conStatus, conStatus.StatusCode, err)
}
go h.wsMultiConnectionFunnel(h.WebsocketConn, HuobiHadaxSocketIOAddress)
return nil
}
h.Websocket.TrafficAlert <- struct{}{}
b := bytes.NewReader(resp)
gReader, err := gzip.NewReader(b)
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{})
if err != nil {
return exchange.WebsocketResponse{}, err
return fmt.Errorf("%v %v %v Error: %v", wsAccountsOrdersURL, conStatus, conStatus.StatusCode, err)
}
go h.wsMultiConnectionFunnel(h.AuthenticatedWebsocketConn, wsAccountsOrdersURL)
return nil
}
unzipped, err := ioutil.ReadAll(gReader)
if err != nil {
return exchange.WebsocketResponse{}, err
// wsMultiConnectionFunnel manages data from multiple endpoints and passes it to a channel
func (h *HUOBIHADAX) wsMultiConnectionFunnel(ws *websocket.Conn, url string) {
h.Websocket.Wg.Add(1)
defer h.Websocket.Wg.Done()
for {
select {
case <-h.Websocket.ShutdownC:
return
default:
_, 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}
}
}
gReader.Close()
return exchange.WebsocketResponse{Raw: unzipped}, nil
}
// WsHandleData handles data read from the websocket connection
func (h *HUOBIHADAX) WsHandleData() {
h.Websocket.Wg.Add(1)
defer func() {
h.Websocket.Wg.Done()
}()
defer h.Websocket.Wg.Done()
for {
select {
case <-h.Websocket.ShutdownC:
return
default:
resp, err := h.WsReadData()
if err != nil {
h.Websocket.DataHandler <- err
return
case resp := <-comms:
if h.Verbose {
log.Debugf("%v: %v: %v", h.Name, resp.URL, string(resp.Raw))
}
var init WsResponse
err = common.JSONDecode(resp.Raw, &init)
if err != nil {
h.Websocket.DataHandler <- err
continue
switch resp.URL {
case HuobiHadaxSocketIOAddress:
h.wsHandleMarketData(resp)
case wsAccountsOrdersURL:
h.wsHandleAuthenticatedData(resp)
}
}
}
}
if init.Status == "error" {
h.Websocket.DataHandler <- fmt.Errorf("huobi.go Websocker error %s %s",
init.ErrorCode,
init.ErrorMessage)
continue
}
func (h *HUOBIHADAX) wsHandleAuthenticatedData(resp WsMessage) {
var init WsAuthenticatedDataResponse
err := common.JSONDecode(resp.Raw, &init)
if err != nil {
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}`)
if err != nil {
log.Error(err)
}
return
}
if init.Subscribed != "" {
continue
}
if init.Op == "sub" {
if h.Verbose {
log.Debugf("%v: %v: Successfully subscribed to %v", h.Name, resp.URL, init.Topic)
}
return
}
if init.Ping != 0 {
err = h.WebsocketConn.WriteJSON(`{"pong":1337}`)
if err != nil {
h.Websocket.DataHandler <- err
continue
}
continue
}
switch {
case strings.EqualFold(init.Op, authOp):
h.Websocket.SetCanUseAuthenticatedEndpoints(true)
var response WsAuthenticatedDataResponse
err := common.JSONDecode(resp.Raw, &response)
if err != nil {
h.Websocket.DataHandler <- err
}
h.Websocket.DataHandler <- response
case strings.EqualFold(init.Topic, "accounts"):
var response WsAuthenticatedAccountsResponse
err := common.JSONDecode(resp.Raw, &response)
if err != nil {
h.Websocket.DataHandler <- err
}
h.Websocket.DataHandler <- response
case strings.Contains(init.Topic, "orders") &&
strings.Contains(init.Topic, "update"):
var response WsAuthenticatedOrdersUpdateResponse
err := common.JSONDecode(resp.Raw, &response)
if err != nil {
h.Websocket.DataHandler <- err
}
h.Websocket.DataHandler <- response
case strings.Contains(init.Topic, "orders"):
var response WsAuthenticatedOrdersResponse
err := common.JSONDecode(resp.Raw, &response)
if err != nil {
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
}
}
switch {
case strings.Contains(init.Channel, "depth"):
var depth WsDepth
err := common.JSONDecode(resp.Raw, &depth)
if err != nil {
h.Websocket.DataHandler <- err
continue
}
func (h *HUOBIHADAX) wsHandleMarketData(resp WsMessage) {
var init WsResponse
err := common.JSONDecode(resp.Raw, &init)
if err != nil {
h.Websocket.DataHandler <- err
return
}
if init.Status == "error" {
h.Websocket.DataHandler <- fmt.Errorf("%v %v Websocket error %s %s",
h.Name,
resp.URL,
init.ErrorCode,
init.ErrorMessage)
return
}
if init.Subscribed != "" {
return
}
if init.Ping != 0 {
err = h.WebsocketConn.WriteJSON(`{"pong":1337}`)
if err != nil {
log.Error(err)
}
return
}
data := strings.Split(depth.Channel, ".")
h.WsProcessOrderbook(&depth, data[1])
case strings.Contains(init.Channel, "kline"):
var kline WsKline
err := common.JSONDecode(resp.Raw, &kline)
if err != nil {
h.Websocket.DataHandler <- err
continue
}
data := strings.Split(kline.Channel, ".")
h.Websocket.DataHandler <- exchange.KlineData{
Timestamp: time.Unix(0, kline.Timestamp),
Exchange: h.GetName(),
AssetType: asset.Spot,
Pair: currency.NewPairFromString(data[1]),
OpenPrice: kline.Tick.Open,
ClosePrice: kline.Tick.Close,
HighPrice: kline.Tick.High,
LowPrice: kline.Tick.Low,
Volume: kline.Tick.Volume,
}
case strings.Contains(init.Channel, "trade"):
var trade WsTrade
err := common.JSONDecode(resp.Raw, &trade)
if err != nil {
h.Websocket.DataHandler <- err
continue
}
data := strings.Split(trade.Channel, ".")
h.Websocket.DataHandler <- exchange.TradeData{
Exchange: h.GetName(),
AssetType: asset.Spot,
CurrencyPair: currency.NewPairFromString(data[1]),
Timestamp: time.Unix(0, trade.Tick.Timestamp),
}
}
switch {
case strings.Contains(init.Channel, "depth"):
var depth WsDepth
err := common.JSONDecode(resp.Raw, &depth)
if err != nil {
h.Websocket.DataHandler <- err
return
}
data := strings.Split(depth.Channel, ".")
h.WsProcessOrderbook(&depth, data[1])
case strings.Contains(init.Channel, "kline"):
var kline WsKline
err := common.JSONDecode(resp.Raw, &kline)
if err != nil {
h.Websocket.DataHandler <- err
return
}
data := strings.Split(kline.Channel, ".")
h.Websocket.DataHandler <- exchange.KlineData{
Timestamp: time.Unix(0, kline.Timestamp),
Exchange: h.GetName(),
AssetType: "SPOT",
Pair: currency.NewPairFromString(data[1]),
OpenPrice: kline.Tick.Open,
ClosePrice: kline.Tick.Close,
HighPrice: kline.Tick.High,
LowPrice: kline.Tick.Low,
Volume: kline.Tick.Volume,
}
case strings.Contains(init.Channel, "trade"):
var trade WsTrade
err := common.JSONDecode(resp.Raw, &trade)
if err != nil {
h.Websocket.DataHandler <- err
return
}
data := strings.Split(trade.Channel, ".")
h.Websocket.DataHandler <- exchange.TradeData{
Exchange: h.GetName(),
AssetType: "SPOT",
CurrencyPair: currency.NewPairFromString(data[1]),
Timestamp: time.Unix(0, trade.Tick.Timestamp),
}
}
}
@@ -225,8 +368,14 @@ 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
if h.Websocket.CanUseAuthenticatedEndpoints() {
channels = append(channels, "orders.%v", "orders.%v.update")
subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{
Channel: "accounts",
})
}
enabledCurrencies := h.GetEnabledPairs(asset.Spot)
subscriptions := []exchange.WebsocketChannelSubscription{}
for i := range channels {
for j := range enabledCurrencies {
enabledCurrencies[j].Delimiter = ""
@@ -242,6 +391,10 @@ func (h *HUOBIHADAX) GenerateDefaultSubscriptions() {
// Subscribe sends a websocket message to receive data from the channel
func (h *HUOBIHADAX) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error {
if strings.Contains(channelToSubscribe.Channel, "orders.") ||
strings.Contains(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
@@ -251,6 +404,10 @@ func (h *HUOBIHADAX) Subscribe(channelToSubscribe exchange.WebsocketChannelSubsc
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (h *HUOBIHADAX) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error {
if strings.Contains(channelToSubscribe.Channel, "orders.") ||
strings.Contains(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
@@ -267,3 +424,125 @@ func (h *HUOBIHADAX) wsSend(data []byte) error {
}
return h.WebsocketConn.WriteMessage(websocket.TextMessage, data)
}
func (h *HUOBIHADAX) wsLogin() error {
if !h.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", h.Name)
}
h.Websocket.SetCanUseAuthenticatedEndpoints(true)
timestamp := time.Now().UTC().Format(wsDateTimeFormatting)
request := WsAuthenticationRequest{
Op: authOp,
AccessKeyID: h.API.Credentials.Key,
SignatureMethod: signatureMethod,
SignatureVersion: signatureVersion,
Timestamp: timestamp,
}
hmac := h.wsGenerateSignature(timestamp, wsAccountsOrdersEndPoint)
request.Signature = crypto.Base64Encode(hmac)
err := h.wsAuthenticatedSend(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)
}
func (h *HUOBIHADAX) wsGenerateSignature(timestamp, endpoint string) []byte {
values := url.Values{}
values.Set("AccessKeyId", h.API.Credentials.Key)
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 crypto.GetHMAC(crypto.HashSHA256, []byte(payload), []byte(h.API.Credentials.Secret))
}
func (h *HUOBIHADAX) wsAuthenticatedSubscribe(operation, endpoint, topic string) error {
timestamp := time.Now().UTC().Format(wsDateTimeFormatting)
request := WsAuthenticatedSubscriptionRequest{
Op: operation,
AccessKeyID: h.API.Credentials.Key,
SignatureMethod: signatureMethod,
SignatureVersion: signatureVersion,
Timestamp: timestamp,
Topic: topic,
}
hmac := h.wsGenerateSignature(timestamp, endpoint)
request.Signature = crypto.Base64Encode(hmac)
return h.wsAuthenticatedSend(request)
}
func (h *HUOBIHADAX) wsGetAccountsList(pair currency.Pair) error {
if !h.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authenticated cannot get accounts list", h.Name)
}
timestamp := time.Now().UTC().Format(wsDateTimeFormatting)
request := WsAuthenticatedAccountsListRequest{
Op: requestOp,
AccessKeyID: h.API.Credentials.Key,
SignatureMethod: signatureMethod,
SignatureVersion: signatureVersion,
Timestamp: timestamp,
Topic: wsAccountsList,
Symbol: pair,
}
hmac := h.wsGenerateSignature(timestamp, wsAccountListEndpoint)
request.Signature = crypto.Base64Encode(hmac)
return h.wsAuthenticatedSend(request)
}
func (h *HUOBIHADAX) wsGetOrdersList(accountID int64, pair currency.Pair) error {
if !h.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authenticated cannot get orders list", h.Name)
}
timestamp := time.Now().UTC().Format(wsDateTimeFormatting)
request := WsAuthenticatedOrdersListRequest{
Op: requestOp,
AccessKeyID: h.API.Credentials.Key,
SignatureMethod: signatureMethod,
SignatureVersion: signatureVersion,
Timestamp: timestamp,
Topic: wsOrdersList,
AccountID: accountID,
Symbol: pair.Lower(),
States: "submitted,partial-filled",
}
hmac := h.wsGenerateSignature(timestamp, wsOrdersListEndpoint)
request.Signature = crypto.Base64Encode(hmac)
return h.wsAuthenticatedSend(request)
}
func (h *HUOBIHADAX) wsGetOrderDetails(orderID string) error {
if !h.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authenticated cannot get order details", h.Name)
}
timestamp := time.Now().UTC().Format(wsDateTimeFormatting)
request := WsAuthenticatedOrderDetailsRequest{
Op: requestOp,
AccessKeyID: h.API.Credentials.Key,
SignatureMethod: signatureMethod,
SignatureVersion: signatureVersion,
Timestamp: timestamp,
Topic: wsOrdersDetail,
OrderID: orderID,
}
hmac := h.wsGenerateSignature(timestamp, wsOrdersDetailEndpoint)
request.Signature = crypto.Base64Encode(hmac)
return h.wsAuthenticatedSend(request)
}

View File

@@ -114,7 +114,7 @@ func (h *HUOBIHADAX) Setup(exch *config.ExchangeConfig) error {
exch.Name,
exch.Features.Enabled.Websocket,
exch.Verbose,
huobiGlobalWebsocketEndpoint,
HuobiHadaxSocketIOAddress,
exch.API.Endpoints.WebsocketURL)
}
@@ -577,3 +577,13 @@ func (h *HUOBIHADAX) UnsubscribeToWebsocketChannels(channels []exchange.Websocke
h.Websocket.UnsubscribeToChannels(channels)
return nil
}
// GetSubscriptions returns a copied list of subscriptions
func (h *HUOBIHADAX) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return h.Websocket.GetSubscriptions(), nil
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (h *HUOBIHADAX) AuthenticateWebsocket() error {
return h.wsLogin()
}

View File

@@ -28,7 +28,7 @@ type IBotExchange interface {
GetEnabledPairs(assetType asset.Item) currency.Pairs
GetAvailablePairs(assetType asset.Item) currency.Pairs
GetAccountInfo() (AccountInfo, error)
GetAuthenticatedAPISupport() bool
GetAuthenticatedAPISupport(endpoint uint8) bool
SetPairs(pairs currency.Pairs, assetType asset.Item, enabled bool) error
GetAssetTypes() asset.Items
GetExchangeHistory(currencyPair currency.Pair, assetType asset.Item) ([]TradeHistory, error)
@@ -61,4 +61,6 @@ type IBotExchange interface {
SubscribeToWebsocketChannels(channels []WebsocketChannelSubscription) error
UnsubscribeToWebsocketChannels(channels []WebsocketChannelSubscription) error
GetDefaultConfig() (*config.ExchangeConfig, error)
GetSubscriptions() ([]WebsocketChannelSubscription, error)
AuthenticateWebsocket() error
}

View File

@@ -509,3 +509,13 @@ func (i *ItBit) SubscribeToWebsocketChannels(channels []exchange.WebsocketChanne
func (i *ItBit) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error {
return common.ErrFunctionNotSupported
}
// GetSubscriptions returns a copied list of subscriptions
func (i *ItBit) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return nil, common.ErrFunctionNotSupported
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (i *ItBit) AuthenticateWebsocket() error {
return common.ErrFunctionNotSupported
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/thrasher-/gocryptotrader/config"
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/sharedtestvalues"
)
var k Kraken
@@ -666,7 +667,7 @@ func TestOrderbookBufferReset(t *testing.T) {
for i := 1; i < orderbookBufferLimit+2; i++ {
obUpdates = append(obUpdates, fmt.Sprintf(`[0,{"a":[["5541.30000","2.50700000","%v"]],"b":[["5541.30000","1.00000000","%v"]]}]`, i, i))
}
k.Websocket.DataHandler = make(chan interface{}, 10)
k.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
var dataResponse WebsocketDataResponse
err := common.JSONDecode([]byte(obpartial), &dataResponse)
if err != nil {
@@ -715,7 +716,7 @@ func TestOrderBookOutOfOrder(t *testing.T) {
obupdate1 := `[0,{"a":[["5541.30000","0.00000000","1"]],"b":[["5541.30000","0.00000000","3"]]}]`
obupdate2 := `[0,{"a":[["5541.30000","2.50700000","2"]],"b":[["5541.30000","0.00000000","1"]]}]`
k.Websocket.DataHandler = make(chan interface{}, 10)
k.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
var dataResponse WebsocketDataResponse
err := common.JSONDecode([]byte(obpartial), &dataResponse)
if err != nil {

View File

@@ -752,7 +752,7 @@ 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.GetEnabledPairs(asset.Spot)
subscriptions := []exchange.WebsocketChannelSubscription{}
var subscriptions []exchange.WebsocketChannelSubscription
for i := range defaultSubscribedChannels {
for j := range enabledCurrencies {
enabledCurrencies[j].Delimiter = "/"

View File

@@ -525,3 +525,13 @@ func (k *Kraken) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketCha
k.Websocket.UnsubscribeToChannels(channels)
return nil
}
// GetSubscriptions returns a copied list of subscriptions
func (k *Kraken) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return k.Websocket.GetSubscriptions(), nil
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (k *Kraken) AuthenticateWebsocket() error {
return common.ErrFunctionNotSupported
}

View File

@@ -465,3 +465,13 @@ func (l *LakeBTC) SubscribeToWebsocketChannels(channels []exchange.WebsocketChan
func (l *LakeBTC) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error {
return common.ErrFunctionNotSupported
}
// GetSubscriptions returns a copied list of subscriptions
func (l *LakeBTC) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return nil, common.ErrFunctionNotSupported
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (l *LakeBTC) AuthenticateWebsocket() error {
return common.ErrFunctionNotSupported
}

View File

@@ -539,3 +539,13 @@ func (l *LocalBitcoins) SubscribeToWebsocketChannels(channels []exchange.Websock
func (l *LocalBitcoins) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error {
return common.ErrFunctionNotSupported
}
// GetSubscriptions returns a copied list of subscriptions
func (l *LocalBitcoins) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return nil, common.ErrFunctionNotSupported
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (l *LocalBitcoins) AuthenticateWebsocket() error {
return common.ErrFunctionNotSupported
}

View File

@@ -12,7 +12,9 @@ import (
"github.com/thrasher-/gocryptotrader/config"
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/asset"
"github.com/thrasher-/gocryptotrader/exchanges/okgroup"
"github.com/thrasher-/gocryptotrader/exchanges/sharedtestvalues"
)
// Please supply you own test keys here for due diligence testing.
@@ -69,13 +71,15 @@ func TestSetup(t *testing.T) {
}
okcoinConfig.API.AuthenticatedSupport = true
okcoinConfig.API.AuthenticatedWebsocketSupport = true
okcoinConfig.API.Credentials.Key = apiKey
okcoinConfig.API.Credentials.Secret = apiSecret
okcoinConfig.API.Credentials.ClientID = passphrase
okcoinConfig.API.Endpoints.WebsocketURL = o.API.Endpoints.WebsocketURL
o.Setup(okcoinConfig)
testSetupRan = true
o.Websocket.DataHandler = make(chan interface{}, 999)
o.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
o.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride()
}
func areTestAPIKeysSet() bool {
@@ -796,13 +800,12 @@ func TestGetMarginTransactionDetails(t *testing.T) {
// Will log in if credentials are present
func TestSendWsMessages(t *testing.T) {
TestSetDefaults(t)
if !websocketEnabled {
t.Skip("Websocket not enabled, skipping")
if !o.Websocket.IsEnabled() && !o.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() {
t.Skip(exchange.WebsocketNotEnabled)
}
var dialer websocket.Dialer
var err error
var ok bool
o.Websocket.TrafficAlert = make(chan struct{}, 99)
o.WebsocketConn, _, err = dialer.Dial(o.Websocket.GetWebsocketURL(),
http.Header{})
if err != nil {
@@ -826,16 +829,12 @@ func TestSendWsMessages(t *testing.T) {
t.Error("Expecting OKEX error - 30040 message: Channel badChannel doesn't exist")
}
}
if !areTestAPIKeysSet() {
return
}
err = o.WsLogin()
if err != nil {
t.Error(err)
}
response = <-o.Websocket.DataHandler
if err, ok := response.(error); ok && err != nil {
responseTwo := <-o.Websocket.DataHandler
if err, ok := responseTwo.(error); ok && err != nil {
t.Error(err)
}
}
@@ -844,7 +843,7 @@ func TestSendWsMessages(t *testing.T) {
func TestGetAssetTypeFromTableName(t *testing.T) {
str := "spot/candle300s:BTC-USDT"
spot := o.GetAssetTypeFromTableName(str)
if spot != "SPOT" {
if !strings.EqualFold(spot.String(), asset.Spot.String()) {
t.Errorf("Error, expected 'SPOT', received: '%v'", spot)
}
}

View File

@@ -13,7 +13,9 @@ import (
"github.com/thrasher-/gocryptotrader/config"
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/asset"
"github.com/thrasher-/gocryptotrader/exchanges/okgroup"
"github.com/thrasher-/gocryptotrader/exchanges/sharedtestvalues"
)
// Please supply you own test keys here for due diligence testing.
@@ -70,13 +72,15 @@ func TestSetup(t *testing.T) {
websocketEnabled = true
}
okexConfig.API.AuthenticatedSupport = true
okexConfig.API.AuthenticatedWebsocketSupport = true
okexConfig.API.Credentials.Key = apiKey
okexConfig.API.Credentials.Secret = apiSecret
okexConfig.API.Credentials.ClientID = passphrase
okexConfig.API.Endpoints.WebsocketURL = o.API.Endpoints.WebsocketURL
o.Setup(okexConfig)
testSetupRan = true
o.Websocket.DataHandler = make(chan interface{}, 999)
o.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
o.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride()
}
func areTestAPIKeysSet() bool {
@@ -1557,13 +1561,12 @@ func TestGetETTSettlementPriceHistory(t *testing.T) {
// Will log in if credentials are present
func TestSendWsMessages(t *testing.T) {
TestSetDefaults(t)
if !websocketEnabled {
t.Skip("Websocket not enabled, skipping")
if !o.Websocket.IsEnabled() && !o.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() {
t.Skip(exchange.WebsocketNotEnabled)
}
var dialer websocket.Dialer
var err error
var ok bool
o.Websocket.TrafficAlert = make(chan struct{}, 99)
o.WebsocketConn, _, err = dialer.Dial(o.Websocket.GetWebsocketURL(),
http.Header{})
if err != nil {
@@ -1587,16 +1590,12 @@ func TestSendWsMessages(t *testing.T) {
t.Error("Expecting OKEX error - 30040 message: Channel badChannel doesn't exist")
}
}
if !areTestAPIKeysSet() {
return
}
err = o.WsLogin()
if err != nil {
t.Error(err)
}
response = <-o.Websocket.DataHandler
if err, ok := response.(error); ok && err != nil {
responseTwo := <-o.Websocket.DataHandler
if err, ok := responseTwo.(error); ok && err != nil {
t.Error(err)
}
}
@@ -1605,7 +1604,7 @@ func TestSendWsMessages(t *testing.T) {
func TestGetAssetTypeFromTableName(t *testing.T) {
str := "spot/candle300s:BTC-USDT"
spot := o.GetAssetTypeFromTableName(str)
if spot != "SPOT" {
if !strings.EqualFold(spot.String(), asset.Spot.String()) {
t.Errorf("Error, expected 'SPOT', received: '%v'", spot)
}
}

View File

@@ -1303,7 +1303,8 @@ type WebsocketEventRequest struct {
// WebsocketEventResponse contains event data for a websocket channel
type WebsocketEventResponse struct {
Event string `json:"event"`
Channel string `json:"channel"`
Channel string `json:"channel,omitempty"`
Success bool `json:"success,omitempty"`
}
// WebsocketDataResponse formats all response data for a websocket event

View File

@@ -200,8 +200,14 @@ func (o *OKGroup) WsConnect() error {
wg.Add(2)
go o.WsHandleData(&wg)
go o.wsPingHandler(&wg)
o.GenerateDefaultSubscriptions()
if o.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
err = o.WsLogin()
if err != nil {
log.Errorf("%v - authentication failed: %v", o.Name, err)
}
}
o.GenerateDefaultSubscriptions()
// Ensures that we start the routines and we dont race when shutdown occurs
wg.Wait()
return nil
@@ -301,10 +307,14 @@ func (o *OKGroup) WsHandleData(wg *sync.WaitGroup) {
}
var eventResponse WebsocketEventResponse
err = common.JSONDecode(resp.Raw, &eventResponse)
if err == nil && len(eventResponse.Channel) > 0 {
if err == nil && eventResponse.Event != "" {
if eventResponse.Event == "login" {
o.Websocket.SetCanUseAuthenticatedEndpoints(eventResponse.Success)
}
if o.Verbose {
log.Debugf("WS Event: %v on Channel: %v", eventResponse.Event, eventResponse.Channel)
}
o.Websocket.DataHandler <- eventResponse
continue
}
}
@@ -313,6 +323,7 @@ func (o *OKGroup) WsHandleData(wg *sync.WaitGroup) {
// WsLogin sends a login request to websocket to enable access to authenticated endpoints
func (o *OKGroup) WsLogin() error {
o.Websocket.SetCanUseAuthenticatedEndpoints(true)
utcTime := time.Now().UTC()
unixTime := utcTime.Unix()
signPath := "/users/self/verify"
@@ -325,10 +336,12 @@ func (o *OKGroup) WsLogin() error {
}
json, err := common.JSONEncode(resp)
if err != nil {
o.Websocket.SetCanUseAuthenticatedEndpoints(false)
return err
}
err = o.writeToWebsocket(string(json))
if err != nil {
o.Websocket.SetCanUseAuthenticatedEndpoints(false)
return err
}
return nil
@@ -371,6 +384,7 @@ func (o *OKGroup) GetAssetTypeFromTableName(table string) asset.Item {
// WsHandleDataResponse classifies the WS response and sends to appropriate handler
func (o *OKGroup) WsHandleDataResponse(response *WebsocketDataResponse) {
switch o.GetWsChannelWithoutOrderType(response.Table) {
case okGroupWsCandle60s, okGroupWsCandle180s, okGroupWsCandle300s, okGroupWsCandle900s,
okGroupWsCandle1800s, okGroupWsCandle3600s, okGroupWsCandle7200s, okGroupWsCandle14400s,
okGroupWsCandle21600s, okGroupWsCandle43200s, okGroupWsCandle86400s, okGroupWsCandle604900s:
@@ -684,7 +698,10 @@ func (o *OKGroup) CalculateUpdateOrderbookChecksum(orderbookData *orderbook.Base
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (o *OKGroup) GenerateDefaultSubscriptions() {
enabledCurrencies := o.GetEnabledPairs(asset.Spot)
subscriptions := []exchange.WebsocketChannelSubscription{}
var subscriptions []exchange.WebsocketChannelSubscription
if o.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
defaultSubscribedChannels = append(defaultSubscribedChannels, okGroupWsSpotMarginAccount, okGroupWsSpotAccount, okGroupWsSpotOrder)
}
for i := range defaultSubscribedChannels {
for j := range enabledCurrencies {
enabledCurrencies[j].Delimiter = "-"
@@ -703,6 +720,10 @@ func (o *OKGroup) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscrip
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())}
}
json, err := common.JSONEncode(resp)
if err != nil {
if o.Verbose {

View File

@@ -446,3 +446,13 @@ func (o *OKGroup) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketCh
o.Websocket.UnsubscribeToChannels(channels)
return nil
}
// GetSubscriptions returns a copied list of subscriptions
func (o *OKGroup) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return o.Websocket.GetSubscriptions(), nil
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (o *OKGroup) AuthenticateWebsocket() error {
return o.WsLogin()
}

View File

@@ -1,18 +1,21 @@
package poloniex
import (
"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"
)
var p Poloniex
// Please supply your own APIKEYS here for due diligence testing
const (
apiKey = ""
apiSecret = ""
@@ -30,6 +33,7 @@ func TestSetup(t *testing.T) {
t.Error("Test Failed - Poloniex Setup() init error")
}
poloniexConfig.API.AuthenticatedSupport = true
poloniexConfig.API.AuthenticatedWebsocketSupport = true
poloniexConfig.API.Credentials.Key = apiKey
poloniexConfig.API.Credentials.Secret = apiSecret
p.SetDefaults()
@@ -410,3 +414,53 @@ func TestGetDepositAddress(t *testing.T) {
}
}
}
func TestWsHandleAccountData(t *testing.T) {
t.Parallel()
TestSetup(t)
p.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
jsons := []string{
`[["n",225,807230187,0,"1000.00000000","0.10000000","2018-11-07 16:42:42"],["b",267,"e","-0.10000000"]]`,
`[["o",807230187,"0.00000000"],["b",267,"e","0.10000000"]]`,
`[["t", 12345, "0.03000000", "0.50000000", "0.00250000", 0, 6083059, "0.00000375", "2018-09-08 05:54:09"]]`,
}
for i := range jsons {
var result [][]interface{}
err := common.JSONDecode([]byte(jsons[i]), &result)
if err != nil {
t.Error(err)
}
p.wsHandleAccountData(result)
}
}
// TestWsAuth dials websocket, sends login request.
// Will receive a message only on failure
func TestWsAuth(t *testing.T) {
TestSetup(t)
if !p.Websocket.IsEnabled() && !p.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() {
t.Skip(exchange.WebsocketNotEnabled)
}
var err error
var dialer websocket.Dialer
p.WebsocketConn, _, err = dialer.Dial(p.Websocket.GetWebsocketURL(),
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)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case response := <-p.Websocket.DataHandler:
t.Error(response)
case <-timer.C:
}
timer.Stop()
}

View File

@@ -1,6 +1,10 @@
package poloniex
import "github.com/thrasher-/gocryptotrader/currency"
import (
"time"
"github.com/thrasher-/gocryptotrader/currency"
)
// Ticker holds ticker data
type Ticker struct {
@@ -402,3 +406,47 @@ var WithdrawalFees = map[currency.Code]float64{
currency.VIA: 0.01,
currency.ZEC: 0.001,
}
// WsAccountBalanceUpdateResponse Authenticated Ws Account data
type WsAccountBalanceUpdateResponse struct {
currencyID float64
wallet string
amount float64
}
// WsNewLimitOrderResponse Authenticated Ws Account data
type WsNewLimitOrderResponse struct {
currencyID float64
orderNumber float64
orderType float64
rate float64
amount float64
date time.Time
}
// WsOrderUpdateResponse Authenticated Ws Account data
type WsOrderUpdateResponse struct {
OrderNumber float64
NewAmount string
}
// WsTradeNotificationResponse Authenticated Ws Account data
type WsTradeNotificationResponse struct {
TradeID float64
Rate float64
Amount float64
FeeMultiplier float64
FundingType float64
OrderNumber float64
TotalFee float64
Date time.Time
}
// WsAuthorisationRequest Authenticated Ws Account data request
type WsAuthorisationRequest struct {
Command string `json:"command"`
Channel int64 `json:"channel"`
Sign string `json:"sign"`
Key string `json:"key"`
Payload string `json:"payload"`
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/gorilla/websocket"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/common/crypto"
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/asset"
@@ -131,43 +132,16 @@ func (p *Poloniex) WsHandleData() {
log.Debugf("poloniex websocket subscribed to channel successfully. %d", chanID)
}
} else {
if p.Verbose {
log.Debugf("poloniex websocket subscription to channel failed. %d", chanID)
}
p.Websocket.DataHandler <- fmt.Errorf("poloniex websocket subscription to channel failed. %d", chanID)
}
continue
}
switch chanID {
case wsAccountNotificationID:
p.wsHandleAccountData(data[2].([][]interface{}))
case wsTickerDataID:
tickerData := data[2].([]interface{})
var t WsTicker
currencyPair := currencyIDMap[int(tickerData[0].(float64))]
t.LastPrice, _ = strconv.ParseFloat(tickerData[1].(string), 64)
t.LowestAsk, _ = strconv.ParseFloat(tickerData[2].(string), 64)
t.HighestBid, _ = strconv.ParseFloat(tickerData[3].(string), 64)
t.PercentageChange, _ = strconv.ParseFloat(tickerData[4].(string), 64)
t.BaseCurrencyVolume24H, _ = strconv.ParseFloat(tickerData[5].(string), 64)
t.QuoteCurrencyVolume24H, _ = strconv.ParseFloat(tickerData[6].(string), 64)
isFrozen := false
if tickerData[7].(float64) == 1 {
isFrozen = true
}
t.IsFrozen = isFrozen
t.HighestTradeIn24H, _ = strconv.ParseFloat(tickerData[8].(string), 64)
t.LowestTradePrice24H, _ = strconv.ParseFloat(tickerData[9].(string), 64)
p.Websocket.DataHandler <- exchange.TickerData{
Timestamp: time.Now(),
Pair: currency.NewPairDelimiter(currencyPair, "_"),
Exchange: p.GetName(),
AssetType: asset.Spot,
ClosePrice: t.LastPrice,
LowPrice: t.LowestAsk,
HighPrice: t.HighestBid,
}
p.wsHandleTickerData(data)
case ws24HourExchangeVolumeID:
case wsHeartbeat:
default:
@@ -248,6 +222,90 @@ func (p *Poloniex) WsHandleData() {
}
}
func (p *Poloniex) wsHandleTickerData(data []interface{}) {
tickerData := data[2].([]interface{})
var t WsTicker
currencyPair := currencyIDMap[int(tickerData[0].(float64))]
t.LastPrice, _ = strconv.ParseFloat(tickerData[1].(string), 64)
t.LowestAsk, _ = strconv.ParseFloat(tickerData[2].(string), 64)
t.HighestBid, _ = strconv.ParseFloat(tickerData[3].(string), 64)
t.PercentageChange, _ = strconv.ParseFloat(tickerData[4].(string), 64)
t.BaseCurrencyVolume24H, _ = strconv.ParseFloat(tickerData[5].(string), 64)
t.QuoteCurrencyVolume24H, _ = strconv.ParseFloat(tickerData[6].(string), 64)
isFrozen := false
if tickerData[7].(float64) == 1 {
isFrozen = true
}
t.IsFrozen = isFrozen
t.HighestTradeIn24H, _ = strconv.ParseFloat(tickerData[8].(string), 64)
t.LowestTradePrice24H, _ = strconv.ParseFloat(tickerData[9].(string), 64)
p.Websocket.DataHandler <- exchange.TickerData{
Timestamp: time.Now(),
Pair: currency.NewPairDelimiter(currencyPair, "_"),
Exchange: p.GetName(),
AssetType: asset.Spot,
ClosePrice: t.LastPrice,
LowPrice: t.LowestAsk,
HighPrice: t.HighestBid,
Quantity: t.QuoteCurrencyVolume24H,
}
}
// wsHandleAccountData Parses account data and sends to datahandler
func (p *Poloniex) wsHandleAccountData(accountData [][]interface{}) {
for i := range accountData {
switch accountData[i][0].(string) {
case "b":
amount, _ := strconv.ParseFloat(accountData[i][3].(string), 64)
response := WsAccountBalanceUpdateResponse{
currencyID: accountData[i][1].(float64),
wallet: accountData[i][2].(string),
amount: amount,
}
p.Websocket.DataHandler <- response
case "n":
timeParse, _ := time.Parse("2006-01-02 15:04:05", accountData[i][6].(string))
rate, _ := strconv.ParseFloat(accountData[i][4].(string), 64)
amount, _ := strconv.ParseFloat(accountData[i][5].(string), 64)
response := WsNewLimitOrderResponse{
currencyID: accountData[i][1].(float64),
orderNumber: accountData[i][2].(float64),
orderType: accountData[i][3].(float64),
rate: rate,
amount: amount,
date: timeParse,
}
p.Websocket.DataHandler <- response
case "o":
response := WsOrderUpdateResponse{
OrderNumber: accountData[i][1].(float64),
NewAmount: accountData[i][2].(string),
}
p.Websocket.DataHandler <- response
case "t":
timeParse, _ := time.Parse("2006-01-02 15:04:05", accountData[i][8].(string))
rate, _ := strconv.ParseFloat(accountData[i][2].(string), 64)
amount, _ := strconv.ParseFloat(accountData[i][3].(string), 64)
feeMultiplier, _ := strconv.ParseFloat(accountData[i][4].(string), 64)
totalFee, _ := strconv.ParseFloat(accountData[i][7].(string), 64)
response := WsTradeNotificationResponse{
TradeID: accountData[i][1].(float64),
Rate: rate,
Amount: amount,
FeeMultiplier: feeMultiplier,
FundingType: accountData[i][5].(float64),
OrderNumber: accountData[i][6].(float64),
TotalFee: totalFee,
Date: timeParse,
}
p.Websocket.DataHandler <- response
}
}
}
// WsProcessOrderbookSnapshot processes a new orderbook snapshot into a local
// of orderbooks
func (p *Poloniex) WsProcessOrderbookSnapshot(ob []interface{}, symbol string) error {
@@ -334,12 +392,18 @@ func (p *Poloniex) WsProcessOrderbookUpdate(target []interface{}, symbol string)
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (p *Poloniex) GenerateDefaultSubscriptions() {
subscriptions := []exchange.WebsocketChannelSubscription{}
var subscriptions []exchange.WebsocketChannelSubscription
// Tickerdata is its own channel
subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{
Channel: fmt.Sprintf("%v", wsTickerDataID),
})
if p.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{
Channel: fmt.Sprintf("%v", wsAccountNotificationID),
})
}
enabledCurrencies := p.GetEnabledPairs(asset.Spot)
for j := range enabledCurrencies {
enabledCurrencies[j].Delimiter = "_"
@@ -356,9 +420,12 @@ func (p *Poloniex) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscri
subscriptionRequest := WsCommand{
Command: "subscribe",
}
if strings.EqualFold(fmt.Sprintf("%v", wsTickerDataID), channelToSubscribe.Channel) {
switch {
case strings.EqualFold(fmt.Sprintf("%v", wsAccountNotificationID), channelToSubscribe.Channel):
return p.wsSendAuthorisedCommand("subscribe")
case strings.EqualFold(fmt.Sprintf("%v", wsTickerDataID), channelToSubscribe.Channel):
subscriptionRequest.Channel = wsTickerDataID
} else {
default:
subscriptionRequest.Channel = channelToSubscribe.Currency.String()
}
return p.wsSend(subscriptionRequest)
@@ -369,9 +436,12 @@ func (p *Poloniex) Unsubscribe(channelToSubscribe exchange.WebsocketChannelSubsc
unsubscriptionRequest := WsCommand{
Command: "unsubscribe",
}
if strings.EqualFold(fmt.Sprintf("%v", wsTickerDataID), channelToSubscribe.Channel) {
switch {
case strings.EqualFold(fmt.Sprintf("%v", wsAccountNotificationID), channelToSubscribe.Channel):
return p.wsSendAuthorisedCommand("unsubscribe")
case strings.EqualFold(fmt.Sprintf("%v", wsTickerDataID), channelToSubscribe.Channel):
unsubscriptionRequest.Channel = wsTickerDataID
} else {
default:
unsubscriptionRequest.Channel = channelToSubscribe.Currency.String()
}
return p.wsSend(unsubscriptionRequest)
@@ -390,3 +460,16 @@ func (p *Poloniex) wsSend(data interface{}) error {
}
return p.WebsocketConn.WriteMessage(websocket.TextMessage, json)
}
func (p *Poloniex) wsSendAuthorisedCommand(command string) error {
nonce := fmt.Sprintf("nonce=%v", time.Now().UnixNano())
hmac := crypto.GetHMAC(crypto.HashSHA512, []byte(nonce), []byte(p.API.Credentials.Secret))
request := WsAuthorisationRequest{
Command: command,
Channel: 1000,
Sign: crypto.HexEncodeToString(hmac),
Key: p.API.Credentials.Key,
Payload: nonce,
}
return p.wsSend(request)
}

View File

@@ -529,3 +529,13 @@ func (p *Poloniex) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketC
p.Websocket.UnsubscribeToChannels(channels)
return nil
}
// GetSubscriptions returns a copied list of subscriptions
func (p *Poloniex) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return p.Websocket.GetSubscriptions(), nil
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (p *Poloniex) AuthenticateWebsocket() error {
return common.ErrFunctionNotSupported
}

View File

@@ -0,0 +1,28 @@
package sharedtestvalues
import "time"
// This package is only to be referenced in test files
const (
// WebsocketResponseDefaultTimeout used in websocket testing
// Defines wait time for receiving websocket response before cancelling
WebsocketResponseDefaultTimeout = (3 * time.Second)
// WebsocketResponseExtendedTimeout used in websocket testing
// Defines wait time for receiving websocket response before cancelling
WebsocketResponseExtendedTimeout = (15 * time.Second)
// WebsocketChannelOverrideCapacity used in websocket testing
// Defines channel capacity as defaults size can block tests
WebsocketChannelOverrideCapacity = 5
)
// GetWebsocketInterfaceChannelOverride returns a new interface based channel
// with the capacity set to WebsocketChannelOverrideCapacity
func GetWebsocketInterfaceChannelOverride() chan interface{} {
return make(chan interface{}, WebsocketChannelOverrideCapacity)
}
// GetWebsocketStructChannelOverride returns a new struct based channel
// with the capacity set to WebsocketChannelOverrideCapacity
func GetWebsocketStructChannelOverride() chan struct{} {
return make(chan struct{}, WebsocketChannelOverrideCapacity)
}

View File

@@ -51,6 +51,7 @@ func (e *Base) WebsocketSetup(connector func() error,
e.Websocket.SetConnector(connector)
e.Websocket.SetWebsocketURL(runningURL)
e.Websocket.SetExchangeName(exchangeName)
e.Websocket.SetCanUseAuthenticatedEndpoints(e.API.AuthenticatedWebsocketSupport)
e.Websocket.init = false
e.Websocket.noConnectionCheckLimit = 5
@@ -674,6 +675,21 @@ func (w *Websocket) FormatFunctionality() string {
case WebsocketUnsubscribeSupported:
functionality = append(functionality, WebsocketUnsubscribeSupportedText)
case WebsocketAuthenticatedEndpointsSupported:
functionality = append(functionality, WebsocketAuthenticatedEndpointsSupportedText)
case WebsocketAccountDataSupported:
functionality = append(functionality, WebsocketAccountDataSupportedText)
case WebsocketSubmitOrderSupported:
functionality = append(functionality, WebsocketSubmitOrderSupportedText)
case WebsocketCancelOrderSupported:
functionality = append(functionality, WebsocketCancelOrderSupportedText)
case WebsocketWithdrawSupported:
functionality = append(functionality, WebsocketWithdrawSupportedText)
default:
functionality = append(functionality,
fmt.Sprintf("%s[1<<%v]", UnknownWebsocketFunctionality, i))
@@ -839,7 +855,15 @@ func (w *Websocket) ResubscribeToChannel(subscribedChannel WebsocketChannelSubsc
// SubscribeToChannels appends supplied channels to channelsToSubscribe
func (w *Websocket) SubscribeToChannels(channels []WebsocketChannelSubscription) {
for i := range channels {
w.channelsToSubscribe = append(w.channelsToSubscribe, channels[i])
channelFound := false
for j := range w.channelsToSubscribe {
if w.channelsToSubscribe[j].Equal(&channels[i]) {
channelFound = true
}
}
if !channelFound {
w.channelsToSubscribe = append(w.channelsToSubscribe, channels[i])
}
}
w.noConnectionChecks = 0
}
@@ -856,3 +880,25 @@ func (w *WebsocketChannelSubscription) Equal(subscribedChannel *WebsocketChannel
return strings.EqualFold(w.Channel, subscribedChannel.Channel) &&
strings.EqualFold(w.Currency.String(), subscribedChannel.Currency.String())
}
// GetSubscriptions returns a copied list of subscriptions
// subscriptions is a private member and cannot be manipulated
func (w *Websocket) GetSubscriptions() []WebsocketChannelSubscription {
return append(w.subscribedChannels[:0:0], w.subscribedChannels...)
}
// SetCanUseAuthenticatedEndpoints sets canUseAuthenticatedEndpoints val in
// a thread safe manner
func (w *Websocket) SetCanUseAuthenticatedEndpoints(val bool) {
w.subscriptionLock.Lock()
defer w.subscriptionLock.Unlock()
w.canUseAuthenticatedEndpoints = val
}
// CanUseAuthenticatedEndpoints gets canUseAuthenticatedEndpoints val in
// a thread safe manner
func (w *Websocket) CanUseAuthenticatedEndpoints() bool {
w.subscriptionLock.Lock()
defer w.subscriptionLock.Unlock()
return w.canUseAuthenticatedEndpoints
}

View File

@@ -567,3 +567,17 @@ func TestSliceCopyDoesntImpactBoth(t *testing.T) {
t.Errorf("Slice has not been copies appropriately")
}
}
// TestSetCanUseAuthenticatedEndpoints logic test
func TestSetCanUseAuthenticatedEndpoints(t *testing.T) {
w := Websocket{}
result := w.CanUseAuthenticatedEndpoints()
if result {
t.Error("expected `canUseAuthenticatedEndpoints` to be false")
}
w.SetCanUseAuthenticatedEndpoints(true)
result = w.CanUseAuthenticatedEndpoints()
if !result {
t.Error("expected `canUseAuthenticatedEndpoints` to be true")
}
}

View File

@@ -20,17 +20,27 @@ const (
WebsocketAllowsRequests
WebsocketSubscribeSupported
WebsocketUnsubscribeSupported
WebsocketAuthenticatedEndpointsSupported
WebsocketAccountDataSupported
WebsocketSubmitOrderSupported
WebsocketCancelOrderSupported
WebsocketWithdrawSupported
WebsocketTickerSupportedText = "TICKER STREAMING SUPPORTED"
WebsocketOrderbookSupportedText = "ORDERBOOK STREAMING SUPPORTED"
WebsocketKlineSupportedText = "KLINE STREAMING SUPPORTED"
WebsocketTradeDataSupportedText = "TRADE STREAMING SUPPORTED"
WebsocketAccountSupportedText = "ACCOUNT STREAMING SUPPORTED"
WebsocketAllowsRequestsText = "WEBSOCKET REQUESTS SUPPORTED"
NoWebsocketSupportText = "WEBSOCKET NOT SUPPORTED"
UnknownWebsocketFunctionality = "UNKNOWN FUNCTIONALITY BITMASK"
WebsocketSubscribeSupportedText = "WEBSOCKET SUBSCRIBE SUPPORTED"
WebsocketUnsubscribeSupportedText = "WEBSOCKET UNSUBSCRIBE SUPPORTED"
WebsocketTickerSupportedText = "TICKER STREAMING SUPPORTED"
WebsocketOrderbookSupportedText = "ORDERBOOK STREAMING SUPPORTED"
WebsocketKlineSupportedText = "KLINE STREAMING SUPPORTED"
WebsocketTradeDataSupportedText = "TRADE STREAMING SUPPORTED"
WebsocketAccountSupportedText = "ACCOUNT STREAMING SUPPORTED"
WebsocketAllowsRequestsText = "WEBSOCKET REQUESTS SUPPORTED"
NoWebsocketSupportText = "WEBSOCKET NOT SUPPORTED"
UnknownWebsocketFunctionality = "UNKNOWN FUNCTIONALITY BITMASK"
WebsocketSubscribeSupportedText = "WEBSOCKET SUBSCRIBE SUPPORTED"
WebsocketUnsubscribeSupportedText = "WEBSOCKET UNSUBSCRIBE SUPPORTED"
WebsocketAuthenticatedEndpointsSupportedText = "WEBSOCKET AUTHENTICATED ENDPOINTS SUPPORTED"
WebsocketAccountDataSupportedText = "WEBSOCKET ACCOUNT DATA SUPPORTED"
WebsocketSubmitOrderSupportedText = "WEBSOCKET SUBMIT ORDER SUPPORTED"
WebsocketCancelOrderSupportedText = "WEBSOCKET CANCEL ORDER SUPPORTED"
WebsocketWithdrawSupportedText = "WEBSOCKET WITHDRAW SUPPORTED"
// WebsocketNotEnabled alerts of a disabled websocket
WebsocketNotEnabled = "exchange_websocket_not_enabled"
@@ -89,7 +99,8 @@ type Websocket struct {
// TrafficAlert monitors if there is a halt in traffic throughput
TrafficAlert chan struct{}
// Functionality defines websocket stream capabilities
Functionality uint32
Functionality uint32
canUseAuthenticatedEndpoints bool
}
// WebsocketChannelSubscription container for websocket subscriptions

View File

@@ -500,3 +500,13 @@ func (y *Yobit) SubscribeToWebsocketChannels(channels []exchange.WebsocketChanne
func (y *Yobit) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error {
return common.ErrFunctionNotSupported
}
// GetSubscriptions returns a copied list of subscriptions
func (y *Yobit) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return nil, common.ErrFunctionNotSupported
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (y *Yobit) AuthenticateWebsocket() error {
return common.ErrFunctionNotSupported
}

View File

@@ -2,12 +2,16 @@ package zb
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"
)
// Please supply you own test keys here for due diligence testing.
@@ -18,6 +22,7 @@ const (
)
var z ZB
var wsSetupRan bool
func TestSetDefaults(t *testing.T) {
z.SetDefaults()
@@ -31,12 +36,35 @@ func TestSetup(t *testing.T) {
t.Error("Test Failed - ZB Setup() init error")
}
zbConfig.API.AuthenticatedSupport = true
zbConfig.API.AuthenticatedWebsocketSupport = true
zbConfig.API.Credentials.Key = apiKey
zbConfig.API.Credentials.Secret = apiSecret
z.Setup(zbConfig)
}
func setupWsAuth(t *testing.T) {
if wsSetupRan {
return
}
z.SetDefaults()
TestSetup(t)
if !z.Websocket.IsEnabled() && !z.API.AuthenticatedWebsocketSupport || !areTestAPIKeysSet() || !canManipulateRealOrders {
t.Skip(exchange.WebsocketNotEnabled)
}
var err error
var dialer websocket.Dialer
z.WebsocketConn, _, err = dialer.Dial(z.Websocket.GetWebsocketURL(),
http.Header{})
if err != nil {
t.Fatal(err)
}
z.Websocket.DataHandler = make(chan interface{}, 11)
z.Websocket.TrafficAlert = make(chan struct{}, 11)
go z.WsHandleData()
wsSetupRan = true
}
func TestSpotNewOrder(t *testing.T) {
t.Parallel()
@@ -462,3 +490,233 @@ func TestGetDepositAddress(t *testing.T) {
}
}
}
// TestZBInvalidJSON ZB sends poorly formed JSON. this tests the JSON fixer
// Then JSON decode it to test if successful
func TestZBInvalidJSON(t *testing.T) {
json := `{"success":true,"code":1000,"channel":"getSubUserList","message":"[{"isOpenApi":false,"memo":"Memo","userName":"hello@imgoodthanksandyou.com@good","userId":1337,"isFreez":false}]","no":"0"}`
fixedJSON := z.wsFixInvalidJSON([]byte(json))
var response WsGetSubUserListResponse
err := common.JSONDecode(fixedJSON, &response)
if err != nil {
t.Log(err)
}
if response.Message[0].UserID != 1337 {
t.Error("Expected extracted JSON USERID to equal 1337")
}
json = `{"success":true,"code":1000,"channel":"createSubUserKey","message":"{"apiKey":"thisisnotareallykeyyousillybilly","apiSecret":"lol"}","no":"14728151154382111746154"}`
fixedJSON = z.wsFixInvalidJSON([]byte(json))
var response2 WsRequestResponse
err = common.JSONDecode(fixedJSON, &response2)
if err != nil {
t.Log(err)
}
}
// TestWsTransferFunds ws test
func TestWsTransferFunds(t *testing.T) {
setupWsAuth(t)
err := z.wsDoTransferFunds(currency.BTC,
0.0001,
"username1",
"username2",
)
if err != nil {
t.Error(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))
if err != nil {
t.Error(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()
}
// TestGetSubUserList ws test
func TestGetSubUserList(t *testing.T) {
setupWsAuth(t)
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!")
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)
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)
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()
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)
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)
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)
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()
}

View File

@@ -5,11 +5,13 @@ import (
"fmt"
"net/http"
"net/url"
"regexp"
"strings"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/common/crypto"
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/asset"
@@ -18,7 +20,8 @@ import (
)
const (
zbWebsocketAPI = "wss://api.zb.cn:9999/websocket"
zbWebsocketAPI = "wss://api.zb.cn:9999/websocket"
zWebsocketAddChannel = "addChannel"
)
// WsConnect initiates a websocket connection
@@ -82,9 +85,9 @@ func (z *ZB) WsHandleData() {
time.Sleep(time.Second)
continue
}
fixedJSON := z.wsFixInvalidJSON(resp.Raw)
var result Generic
err = common.JSONDecode(resp.Raw, &result)
err = common.JSONDecode(fixedJSON, &result)
if err != nil {
z.Websocket.DataHandler <- err
continue
@@ -108,7 +111,7 @@ func (z *ZB) WsHandleData() {
var ticker WsTicker
err := common.JSONDecode(resp.Raw, &ticker)
err := common.JSONDecode(fixedJSON, &ticker)
if err != nil {
z.Websocket.DataHandler <- err
continue
@@ -126,7 +129,7 @@ func (z *ZB) WsHandleData() {
case strings.Contains(result.Channel, "depth"):
var depth WsDepth
err := common.JSONDecode(resp.Raw, &depth)
err := common.JSONDecode(fixedJSON, &depth)
if err != nil {
z.Websocket.DataHandler <- err
continue
@@ -175,13 +178,16 @@ func (z *ZB) WsHandleData() {
case strings.Contains(result.Channel, "trades"):
var trades WsTrades
err := common.JSONDecode(resp.Raw, &trades)
err := common.JSONDecode(fixedJSON, &trades)
if err != nil {
z.Websocket.DataHandler <- err
continue
}
// Most up to date trade
if len(trades.Data) == 0 {
continue
}
t := trades.Data[len(trades.Data)-1]
channelInfo := strings.Split(result.Channel, "_")
@@ -197,7 +203,86 @@ 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 strings.Contains(result.Channel, "_order"):
var response WsSubmitOrderResponse
err := common.JSONDecode(fixedJSON, &response)
if err != nil {
z.Websocket.DataHandler <- err
continue
}
z.Websocket.DataHandler <- response
case strings.Contains(result.Channel, "_cancelorder"):
var response WsCancelOrderResponse
err := common.JSONDecode(fixedJSON, &response)
if err != nil {
z.Websocket.DataHandler <- err
continue
}
z.Websocket.DataHandler <- response
case strings.Contains(result.Channel, "_getorders"):
var response WsGetOrdersResponse
err := common.JSONDecode(fixedJSON, &response)
if err != nil {
z.Websocket.DataHandler <- err
continue
}
z.Websocket.DataHandler <- response
case strings.Contains(result.Channel, "_getorder"):
var response WsGetOrderResponse
err := common.JSONDecode(fixedJSON, &response)
if err != nil {
z.Websocket.DataHandler <- err
continue
}
z.Websocket.DataHandler <- response
case strings.Contains(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
@@ -242,7 +327,7 @@ var wsErrCodes = map[int64]string{
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (z *ZB) GenerateDefaultSubscriptions() {
subscriptions := []exchange.WebsocketChannelSubscription{}
var subscriptions []exchange.WebsocketChannelSubscription
// Tickerdata is its own channel
subscriptions = append(subscriptions, exchange.WebsocketChannelSubscription{
Channel: "markets",
@@ -264,7 +349,7 @@ func (z *ZB) GenerateDefaultSubscriptions() {
// Subscribe sends a websocket message to receive data from the channel
func (z *ZB) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscription) error {
subscriptionRequest := Subscription{
Event: "addChannel",
Event: zWebsocketAddChannel,
Channel: channelToSubscribe.Channel,
}
return z.wsSend(subscriptionRequest)
@@ -279,7 +364,198 @@ func (z *ZB) wsSend(data interface{}) error {
return err
}
if z.Verbose {
log.Debugf("%v sending message to websocket %v", z.Name, data)
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.API.Credentials.Key
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.API.Credentials.Key
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.API.Credentials.Key
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.API.Credentials.Key
request.Sign = z.wsGenerateSignature(request)
return z.wsSend(request)
}
func (z *ZB) wsGenerateSignature(request interface{}) string {
jsonResponse, err := common.JSONEncode(request)
if err != nil {
log.Error(err)
}
hmac := crypto.GetHMAC(crypto.HashMD5,
jsonResponse,
[]byte(crypto.Sha1ToHex(z.API.Credentials.Secret)))
return fmt.Sprintf("%x", hmac)
}
func (z *ZB) wsFixInvalidJSON(json []byte) []byte {
invalidZbJSONRegex := `(\"\[|\"\{)(.*)(\]\"|\}\")`
regexChecker := regexp.MustCompile(invalidZbJSONRegex)
matchingResults := regexChecker.Find(json)
if matchingResults == nil {
return json
}
// Remove first quote character
capturedInvalidZBJSON := strings.Replace(string(matchingResults), "\"", "", 1)
// Remove last quote character
fixedJSON := capturedInvalidZBJSON[:len(capturedInvalidZBJSON)-1]
return []byte(strings.Replace(string(json), string(matchingResults), fixedJSON, 1))
}
func (z *ZB) wsSubmitOrder(pair currency.Pair, amount, price float64, tradeType int64) error {
if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name)
}
request := WsSubmitOrderRequest{
Amount: amount,
Price: price,
TradeType: tradeType,
No: fmt.Sprintf("%v", time.Now().Unix()),
}
request.Channel = fmt.Sprintf("%v_order", pair.String())
request.Event = zWebsocketAddChannel
request.Accesskey = z.API.Credentials.Key
request.Sign = z.wsGenerateSignature(request)
return z.wsSend(request)
}
func (z *ZB) wsCancelOrder(pair currency.Pair, orderID int64) error {
if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name)
}
request := WsCancelOrderRequest{
ID: orderID,
}
request.Channel = fmt.Sprintf("%v_cancelorder", pair.String())
request.Event = zWebsocketAddChannel
request.Accesskey = z.API.Credentials.Key
request.Sign = z.wsGenerateSignature(request)
return z.wsSend(request)
}
func (z *ZB) wsGetOrder(pair currency.Pair, orderID int64) error {
if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name)
}
request := WsGetOrderRequest{
ID: orderID,
}
request.Channel = fmt.Sprintf("%v_getorder", pair.String())
request.Event = zWebsocketAddChannel
request.Accesskey = z.API.Credentials.Key
request.Sign = z.wsGenerateSignature(request)
return z.wsSend(request)
}
func (z *ZB) wsGetOrders(pair currency.Pair, pageIndex, tradeType int64) error {
if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name)
}
request := WsGetOrdersRequest{
PageIndex: pageIndex,
TradeType: tradeType,
}
request.Channel = fmt.Sprintf("%v_getorders", pair.String())
request.Event = zWebsocketAddChannel
request.Accesskey = z.API.Credentials.Key
request.Sign = z.wsGenerateSignature(request)
return z.wsSend(request)
}
func (z *ZB) wsGetOrdersIgnoreTradeType(pair currency.Pair, pageIndex, pageSize int64) error {
if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name)
}
request := WsGetOrdersIgnoreTradeTypeRequest{
PageIndex: pageIndex,
PageSize: pageSize,
}
request.Channel = fmt.Sprintf("%v_getordersignoretradetype", pair.String())
request.Event = zWebsocketAddChannel
request.Accesskey = z.API.Credentials.Key
request.Sign = z.wsGenerateSignature(request)
return z.wsSend(request)
}
func (z *ZB) wsGetAccountInfoRequest() error {
if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name)
}
request := WsAuthenticatedRequest{
Channel: "getaccountinfo",
Event: zWebsocketAddChannel,
Accesskey: z.API.Credentials.Key,
No: fmt.Sprintf("%v", time.Now().Unix()),
}
request.Sign = z.wsGenerateSignature(request)
return z.wsSend(request)
}

View File

@@ -1,6 +1,10 @@
package zb
import "encoding/json"
import (
"encoding/json"
"github.com/thrasher-/gocryptotrader/currency"
)
// Subscription defines an initial subscription type to be sent
type Subscription struct {
@@ -13,7 +17,7 @@ type Generic struct {
Code int64 `json:"code"`
Success bool `json:"success"`
Channel string `json:"channel"`
Message string `json:"message"`
Message interface{} `json:"message"`
No string `json:"no"`
Data json.RawMessage `json:"data"`
}
@@ -55,3 +59,221 @@ type WsTrades struct {
TradeType string `json:"trade_type"`
} `json:"data"`
}
// WsAuthenticatedRequest base request type
type WsAuthenticatedRequest struct {
Accesskey string `json:"accesskey"`
Channel string `json:"channel"`
Event string `json:"event"`
No string `json:"no,omitempty"`
Sign string `json:"sign,omitempty"`
}
// WsAddSubUserRequest data to add sub users
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"`
}
// WsCreateSubUserKeyRequest data to add sub user keys
type WsCreateSubUserKeyRequest struct {
Accesskey string `json:"accesskey"`
AssetPerm bool `json:"assetPerm,string"`
Channel string `json:"channel"`
EntrustPerm bool `json:"entrustPerm,string"`
Event string `json:"event"`
KeyName string `json:"keyName"`
LeverPerm bool `json:"leverPerm,string"`
MoneyPerm bool `json:"moneyPerm,string"`
No string `json:"no"`
Sign string `json:"sign,omitempty"`
ToUserID string `json:"toUserId"`
}
// WsDoTransferFundsRequest data to transfer funds
type WsDoTransferFundsRequest struct {
Accesskey string `json:"accesskey"`
Amount float64 `json:"amount,string"`
Channel string `json:"channel"`
Currency currency.Code `json:"currency"`
Event string `json:"event"`
FromUserName string `json:"fromUserName"`
No string `json:"no"`
Sign string `json:"sign,omitempty"`
ToUserName string `json:"toUserName"`
}
// WsGetSubUserListResponse data response from GetSubUserList
type WsGetSubUserListResponse struct {
Success bool `json:"success"`
Code int64 `json:"code"`
Channel string `json:"channel"`
Message []WsGetSubUserListResponseData `json:"message"`
No string `json:"no"`
}
// WsGetSubUserListResponseData user data
type WsGetSubUserListResponseData struct {
IsOpenAPI bool `json:"isOpenApi,omitempty"`
Memo string `json:"memo,omitempty"`
UserName string `json:"userName,omitempty"`
UserID int64 `json:"userId,omitempty"`
IsFreez bool `json:"isFreez,omitempty"`
}
// WsRequestResponse generic response data
type WsRequestResponse struct {
Success bool `json:"success"`
Code int64 `json:"code"`
Channel string `json:"channel"`
Message interface{} `json:"message"`
No string `json:"no"`
}
// WsSubmitOrderRequest creates an order via ws
type WsSubmitOrderRequest struct {
Accesskey string `json:"accesskey"`
Amount float64 `json:"amount,string"`
Channel string `json:"channel"`
Event string `json:"event"`
No string `json:"no,omitempty"`
Price float64 `json:"price,string"`
Sign string `json:"sign,omitempty"`
TradeType int64 `json:"tradeType,string"`
}
// WsSubmitOrderResponse data about submitted order
type WsSubmitOrderResponse struct {
Message string `json:"message"`
No string `json:"no"`
Data struct {
EntrustID int64 `json:"intrustID"`
} `json:"data"`
Code int64 `json:"code"`
Channel string `json:"channel"`
Success bool `json:"success"`
}
// WsCancelOrderRequest order cancel request
type WsCancelOrderRequest struct {
Accesskey string `json:"accesskey"`
Channel string `json:"channel"`
Event string `json:"event"`
ID int64 `json:"id"`
Sign string `json:"sign,omitempty"`
}
// WsCancelOrderResponse order cancel response
type WsCancelOrderResponse struct {
Message string `json:"message"`
No string `json:"no"`
Code int64 `json:"code"`
Channel string `json:"channel"`
Success bool `json:"success"`
}
// WsGetOrderRequest Get specific order details
type WsGetOrderRequest struct {
Accesskey string `json:"accesskey"`
Channel string `json:"channel"`
Event string `json:"event"`
ID int64 `json:"id"`
Sign string `json:"sign,omitempty"`
}
// WsGetOrderResponse contains order data
type WsGetOrderResponse struct {
Message string `json:"message"`
No string `json:"no"`
Code int64 `json:"code"`
Channel string `json:"channel"`
Success bool `json:"success"`
Data WsGetOrderResponseData `json:"data"`
}
// WsGetOrderResponseData Detailed order data
type WsGetOrderResponseData struct {
Currency string `json:"currency"`
Fees float64 `json:"fees"`
ID string `json:"id"`
Price float64 `json:"price"`
Status int64 `json:"status"`
TotalAmount float64 `json:"total_amount"`
TradeAmount float64 `json:"trade_amount"`
TradePrice float64 `json:"trade_price"`
TradeDate int64 `json:"trade_date"`
TradeMoney float64 `json:"trade_money"`
Type int64 `json:"type"`
}
// WsGetOrdersRequest get more orders, with no orderID filtering
type WsGetOrdersRequest struct {
Accesskey string `json:"accesskey"`
Channel string `json:"channel"`
Event string `json:"event"`
PageIndex int64 `json:"pageIndex"`
TradeType int64 `json:"tradeType"`
Sign string `json:"sign,omitempty"`
}
// WsGetOrdersResponse contains orders data
type WsGetOrdersResponse struct {
Message string `json:"message"`
No string `json:"no"`
Code int64 `json:"code"`
Channel string `json:"channel"`
Success bool `json:"success"`
Data []WsGetOrderResponseData `json:"data"`
}
// WsGetOrdersIgnoreTradeTypeRequest ws request
type WsGetOrdersIgnoreTradeTypeRequest struct {
Accesskey string `json:"accesskey"`
Channel string `json:"channel"`
Event string `json:"event"`
ID int64 `json:"id"`
PageIndex int64 `json:"pageIndex"`
PageSize int64 `json:"pageSize"`
Sign string `json:"sign,omitempty"`
}
// WsGetOrdersIgnoreTradeTypeResponse contains orders data
type WsGetOrdersIgnoreTradeTypeResponse struct {
Message string `json:"message"`
No string `json:"no"`
Code int64 `json:"code"`
Channel string `json:"channel"`
Success bool `json:"success"`
Data []WsGetOrderResponseData `json:"data"`
}
// WsGetAccountInfoResponse contains account data
type WsGetAccountInfoResponse struct {
Message string `json:"message"`
No string `json:"no"`
Data struct {
Coins []struct {
Freez float64 `json:"freez,string"`
EnName string `json:"enName"`
UnitDecimal int `json:"unitDecimal"`
CnName string `json:"cnName"`
UnitTag string `json:"unitTag"`
Available float64 `json:"available,string"`
Key string `json:"key"`
} `json:"coins"`
Base struct {
Username string `json:"username"`
TradePasswordEnabled bool `json:"trade_password_enabled"`
AuthGoogleEnabled bool `json:"auth_google_enabled"`
AuthMobileEnabled bool `json:"auth_mobile_enabled"`
} `json:"base"`
} `json:"data"`
Code int `json:"code"`
Channel string `json:"channel"`
Success bool `json:"success"`
}

View File

@@ -541,3 +541,13 @@ func (z *ZB) SubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSu
func (z *ZB) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChannelSubscription) error {
return common.ErrFunctionNotSupported
}
// GetSubscriptions returns a copied list of subscriptions
func (z *ZB) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return z.Websocket.GetSubscriptions(), nil
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (z *ZB) AuthenticateWebsocket() error {
return common.ErrFunctionNotSupported
}

View File

@@ -173,6 +173,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -214,6 +215,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -255,6 +257,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -304,6 +307,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -347,6 +351,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -389,6 +394,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -430,6 +436,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -472,6 +479,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -514,6 +522,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -555,6 +564,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -596,6 +606,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -639,6 +650,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -681,6 +693,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -723,6 +736,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -763,6 +777,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -804,6 +819,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiAuthPemKey": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIPVSj8YkpXibCAL9HwpGkDNSEXR9jcpiCthdikJqipNooAoGCCqGSM49\nAwEHoUQDQgAEHiB7q/HCqUrCNqPeTtRmKjyi2T+2O2JgoU8Mjx2R4z1h81uOZHCk\nxbsDg1fb7ACRMpKWPs59QWpQxhqMQrNw8w==\n-----END EC PRIVATE KEY-----\n",
@@ -846,6 +862,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiAuthPemKey": "-----BEGIN EC PRIVATE KEY-----\nJUSTADUMMY\n-----END EC PRIVATE KEY-----\n",
@@ -888,6 +905,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -930,6 +948,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -972,6 +991,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -979,7 +999,7 @@
"proxyAddress": "",
"websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API",
"availablePairs": "ETHBTC,USDNGN,USDSGD,EURUSD,USDHKD,BACETH,BTCCHF,BTCGBP,BTCJPY,BTCCAD,BTCEUR,USDCAD,BTCNGN,AUDUSD,GBPUSD,USDJPY,LTCBTC,BCHBTC,USDCHF,NZDUSD,XRPBTC",
"enabledPairs": "BTCUSD,BTCAUD",
"enabledPairs": "ETHBTC",
"baseCurrencies": "USD,EUR,HKD,AUD,GBP,NZD,JPY,SGD,NGN,CHF,CAD",
"assetTypes": "SPOT",
"supportsAutoPairUpdates": true,
@@ -1012,6 +1032,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -1052,6 +1073,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -1094,6 +1116,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -1136,6 +1159,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -1178,6 +1202,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -1222,6 +1247,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -1264,6 +1290,7 @@
"httpUserAgent": "",
"httpDebugging": false,
"authenticatedApiSupport": false,
"authenticatedWebsocketApiSupport": false,
"apiKey": "Key",
"apiSecret": "Secret",
"apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API",
@@ -1271,7 +1298,7 @@
"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": "XRPH19",
"enabledPairs": "LTCM19",
"baseCurrencies": "USD",
"assetTypes": "SPOT",
"supportsAutoPairUpdates": true,