mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-14 07:26:47 +00:00
Merge branch 'master' into engine
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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()}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = "/"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
28
exchanges/sharedtestvalues/sharedtestvalues.go
Normal file
28
exchanges/sharedtestvalues/sharedtestvalues.go
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
31
testdata/configtest.json
vendored
31
testdata/configtest.json
vendored
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user