Authenticated Websocket support (#315)

* Improves subscribing by not allowing duplicates. Adds bitmex auth support

* Adds coinbase pro support. Partial BTCC support. Adds WebsocketAuthenticatedEndpointsSupported websocket feature. Adds GateIO support

* Adds Coinut support

* Moves Coinut WS types to file. Implements Gemini's secure WS endpoint

* Adds HitBTC ws authenticated support. Fixes var names

* Adds huobi and hadax authenticated websocket support

* Adds auth to okgroup (okex, okcoin). Fixes some linting

* Adds Poloniex support

* Adds ZB support

* Adds proper bitmex support

* Improves bitfinex support, improves websocket functionality definitions

* Fixes coinbasepro auth

* Tests all endpoints

* go formatting, importing, linting run

* Adds wrapper supports

* General clean up. Data race destruction

* Improves testing on all exchanges except ZB

* Fixes ZB hashing, parsing and tests

* minor nits before someone else sees them <_<

* Fixes some nits pertaining to variable usage, comments, typos and rate limiting

* Addresses nits regarding types and test responses where applicable

* fmt import

* Fixes linting issues

* No longer returns an error on failure to authenticate, just logs. Adds new AuthenticatedWebsocketAPISupport config value to allow a user to seperate auth from REST and WS. Prevents WS auth if AuthenticatedWebsocketAPISupport is false, adds additional login check 'CanUseAuthenticatedEndpoints' for when login only occurs once (not per request). Removes unnecessary time.Sleeps from code. Moves WS auth error logic to auth function so that wrappers can get involved in all the auth fun. New-fandangled shared test package, used exclusively in testing, will be the store of all the constant boilerplate things like timeout values. Moves WS test setup function to only run once when there are multiple WS endpoint tests. Cleans up some struct types

* Increases test coverage with tests for config.areAuthenticatedCredentialsValid config.CheckExchangeConfigValues, exchange.SetAPIKeys, exchange.GetAuthenticatedAPISupport, exchange_websocket.CanUseAuthenticatedEndpoitns and exchange_websocket.SetCanUseAuthenticatedEndpoints. Adds b.Websocket.SetCanUseAuthenticatedEndpoints(false) when bitfinex fails to authenticate
Fixes a typo. gofmt and goimport

* Trim Test Typos

* Reformats various websocket types. Adds more specific error messaging to config.areAuthenticatedCredentialsValid
This commit is contained in:
Scott
2019-06-19 13:19:01 +10:00
committed by Adrian Gallagher
parent 67a58a10bd
commit 3a66e99899
94 changed files with 5794 additions and 1058 deletions

View File

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

View File

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

View File

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

View File

@@ -118,7 +118,8 @@ func (b *Bitfinex) SetDefaults() {
exchange.WebsocketTradeDataSupported |
exchange.WebsocketOrderbookSupported |
exchange.WebsocketSubscribeSupported |
exchange.WebsocketUnsubscribeSupported
exchange.WebsocketUnsubscribeSupported |
exchange.WebsocketAuthenticatedEndpointsSupported
}
// Setup takes in the supplied exchange configuration details and sets params
@@ -128,6 +129,7 @@ func (b *Bitfinex) Setup(exch *config.ExchangeConfig) {
} else {
b.Enabled = true
b.AuthenticatedAPISupport = exch.AuthenticatedAPISupport
b.AuthenticatedWebsocketAPISupport = exch.AuthenticatedWebsocketAPISupport
b.SetAPIKeys(exch.APIKey, exch.APISecret, "", false)
b.SetHTTPClientTimeout(exch.HTTPTimeout)
b.SetHTTPClientUserAgent(exch.HTTPUserAgent)

View File

@@ -1,15 +1,18 @@
package bitfinex
import (
"net/http"
"net/url"
"reflect"
"testing"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/config"
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/sharedtestvalues"
)
// Please supply your own keys here to do better tests
@@ -37,6 +40,7 @@ func TestSetup(t *testing.T) {
len(b.AvailablePairs) < 1 || len(b.EnabledPairs) < 1 {
t.Error("Test Failed - Bitfinex Setup values not set correctly")
}
b.AuthenticatedWebsocketAPISupport = true
b.AuthenticatedAPISupport = true
// custom rate limit for testing
b.Requester.SetRateLimit(true, time.Millisecond*300, 1)
@@ -951,3 +955,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.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() {
t.Skip(exchange.WebsocketNotEnabled)
}
var err error
var dialer websocket.Dialer
b.WebsocketConn, _, err = dialer.Dial(b.Websocket.GetWebsocketURL(),
http.Header{})
if err != nil {
t.Fatal(err)
}
b.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
b.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride()
go b.WsDataHandler()
defer b.WebsocketConn.Close()
err = b.WsSendAuth()
if err != nil {
t.Error(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case resp := <-b.Websocket.DataHandler:
if resp.(map[string]interface{})["event"] != "auth" && resp.(map[string]interface{})["status"] != "OK" {
t.Error("expected successful login")
}
case <-timer.C:
t.Error("Have not received a response")
}
timer.Stop()
}

View File

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

View File

@@ -18,28 +18,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
@@ -76,6 +78,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"
@@ -89,7 +94,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
@@ -149,6 +159,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 {
@@ -156,13 +171,6 @@ func (b *Bitfinex) WsConnect() error {
}
}
if b.AuthenticatedAPISupport {
err = b.WsSendAuth()
if err != nil {
return err
}
}
pongReceive = make(chan struct{}, 1)
go b.WsDataHandler()
@@ -224,15 +232,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.AuthenticatedAPISupport = false
}
}
@@ -416,6 +422,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
}
@@ -601,7 +620,7 @@ func (b *Bitfinex) WsUpdateOrderbook(p currency.Pair, assetType string, book Web
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
func (b *Bitfinex) GenerateDefaultSubscriptions() {
var channels = []string{"book", "trades", "ticker"}
subscriptions := []exchange.WebsocketChannelSubscription{}
var subscriptions []exchange.WebsocketChannelSubscription
for i := range channels {
for j := range b.EnabledPairs {
params := make(map[string]interface{})
@@ -623,7 +642,9 @@ func (b *Bitfinex) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscri
req := make(map[string]interface{})
req["event"] = "subscribe"
req["channel"] = channelToSubscribe.Channel
req["pair"] = channelToSubscribe.Currency.String()
if channelToSubscribe.Currency.String() != "" {
req["pair"] = channelToSubscribe.Currency.String()
}
if len(channelToSubscribe.Params) > 0 {
for k, v := range channelToSubscribe.Params {
req[k] = v

View File

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

View File

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

View File

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

View File

@@ -139,7 +139,9 @@ func (b *Bitmex) SetDefaults() {
b.Websocket.Functionality = exchange.WebsocketTradeDataSupported |
exchange.WebsocketOrderbookSupported |
exchange.WebsocketSubscribeSupported |
exchange.WebsocketUnsubscribeSupported
exchange.WebsocketUnsubscribeSupported |
exchange.WebsocketAuthenticatedEndpointsSupported |
exchange.WebsocketAccountDataSupported
}
// Setup takes in the supplied exchange configuration details and sets params
@@ -149,6 +151,7 @@ func (b *Bitmex) Setup(exch *config.ExchangeConfig) {
} else {
b.Enabled = true
b.AuthenticatedAPISupport = exch.AuthenticatedAPISupport
b.AuthenticatedWebsocketAPISupport = exch.AuthenticatedWebsocketAPISupport
b.SetAPIKeys(exch.APIKey, exch.APISecret, "", false)
b.RESTPollingDelay = exch.RESTPollingDelay
b.Verbose = exch.Verbose

View File

@@ -1,14 +1,17 @@
package bitmex
import (
"net/http"
"sync"
"testing"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/config"
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/sharedtestvalues"
)
// Please supply your own keys here for due diligence testing
@@ -31,6 +34,7 @@ func TestSetup(t *testing.T) {
if err != nil {
t.Error("Test Failed - Bitmex Setup() init error")
}
bitmexConfig.AuthenticatedWebsocketAPISupport = true
bitmexConfig.AuthenticatedAPISupport = true
bitmexConfig.APIKey = apiKey
bitmexConfig.APISecret = apiSecret
@@ -679,3 +683,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.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() {
t.Skip(exchange.WebsocketNotEnabled)
}
var err error
var dialer websocket.Dialer
b.WebsocketConn, _, err = dialer.Dial(b.Websocket.GetWebsocketURL(),
http.Header{})
if err != nil {
t.Fatal(err)
}
b.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
b.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride()
go b.wsHandleIncomingData()
defer b.WebsocketConn.Close()
err = b.websocketSendAuth()
if err != nil {
t.Error(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case resp := <-b.Websocket.DataHandler:
if !resp.(WebsocketSubscribeResp).Success {
t.Error("Expected successful subscription")
}
case <-timer.C:
t.Error("Have not received a response")
}
timer.Stop()
}

View File

@@ -107,12 +107,11 @@ func (b *Bitmex) WsConnector() error {
go b.wsHandleIncomingData()
b.GenerateDefaultSubscriptions()
if b.AuthenticatedAPISupport {
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
}
@@ -190,11 +189,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)
}
@@ -264,7 +267,6 @@ func (b *Bitmex) wsHandleIncomingData() {
case bitmexWSAnnouncement:
var announcement AnnouncementData
err = common.JSONDecode(resp.Raw, &announcement)
if err != nil {
b.Websocket.DataHandler <- err
@@ -276,7 +278,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)
@@ -393,6 +458,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.GetEnabledCurrencies()
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{
@@ -424,18 +530,26 @@ 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 := common.GetHMAC(common.HashSHA256,
[]byte("GET/realtime"+newTimestamp),
[]byte(b.APISecret))
signature := common.HexEncodeToString(hmac)
var sendAuth WebsocketRequest
sendAuth.Command = "authKeyExpires"
sendAuth.Arguments = append(sendAuth.Arguments, b.APIKey, timestamp,
signature)
return b.wsSend(sendAuth)
err := b.wsSend(sendAuth)
if err != nil {
b.Websocket.SetCanUseAuthenticatedEndpoints(false)
return err
}
return nil
}
// WsSend sends data to the websocket server

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -92,7 +92,8 @@ func (c *CoinbasePro) SetDefaults() {
c.Websocket.Functionality = exchange.WebsocketTickerSupported |
exchange.WebsocketOrderbookSupported |
exchange.WebsocketSubscribeSupported |
exchange.WebsocketUnsubscribeSupported
exchange.WebsocketUnsubscribeSupported |
exchange.WebsocketAuthenticatedEndpointsSupported
}
// Setup initialises the exchange parameters with the current configuration
@@ -102,6 +103,7 @@ func (c *CoinbasePro) Setup(exch *config.ExchangeConfig) {
} else {
c.Enabled = true
c.AuthenticatedAPISupport = exch.AuthenticatedAPISupport
c.AuthenticatedWebsocketAPISupport = exch.AuthenticatedWebsocketAPISupport
c.SetAPIKeys(exch.APIKey, exch.APISecret, exch.ClientID, true)
c.SetHTTPClientTimeout(exch.HTTPTimeout)
c.SetHTTPClientUserAgent(exch.HTTPUserAgent)
@@ -823,7 +825,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 := common.GetHMAC(common.HashSHA256, []byte(message), []byte(c.APISecret))
headers := make(map[string]string)

View File

@@ -1,12 +1,15 @@
package coinbasepro
import (
"net/http"
"testing"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-/gocryptotrader/config"
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/sharedtestvalues"
)
var c CoinbasePro
@@ -33,7 +36,9 @@ func TestSetup(t *testing.T) {
}
gdxConfig.APIKey = apiKey
gdxConfig.APISecret = apiSecret
gdxConfig.ClientID = clientID
gdxConfig.AuthenticatedAPISupport = true
gdxConfig.AuthenticatedWebsocketAPISupport = true
c.Setup(&gdxConfig)
}
@@ -87,137 +92,85 @@ func TestGetServerTime(t *testing.T) {
}
func TestAuthRequests(t *testing.T) {
if c.APIKey != "" && c.APISecret != "" && c.ClientID != "" {
_, 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, "buy", "", "", "BTC-USD", "", false)
if err == nil {
t.Error("Test failed - PlaceLimitOrder() error", err)
}
_, err = c.PlaceMarketOrder("", 1, 0, "buy", "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)
}
}
@@ -626,3 +579,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.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() {
t.Skip(exchange.WebsocketNotEnabled)
}
var err error
var dialer websocket.Dialer
c.WebsocketConn, _, err = dialer.Dial(c.Websocket.GetWebsocketURL(),
http.Header{})
if err != nil {
t.Fatal(err)
}
c.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
c.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride()
go c.WsHandleData()
defer c.WebsocketConn.Close()
err = c.Subscribe(exchange.WebsocketChannelSubscription{
Channel: "user",
Currency: currency.NewPairFromString("BTC-USD"),
})
if err != nil {
t.Error(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case badResponse := <-c.Websocket.DataHandler:
t.Error(badResponse)
case <-timer.C:
}
timer.Stop()
}

View File

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

View File

@@ -148,6 +148,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
}
}
}
@@ -241,10 +286,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.GetEnabledCurrencies()
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{
@@ -269,6 +317,15 @@ 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 := common.GetHMAC(common.HashSHA256, []byte(message), []byte(c.APISecret))
subscribe.Signature = common.Base64Encode(hmac)
subscribe.Key = c.APIKey
subscribe.Passphrase = c.ClientID
subscribe.Timestamp = n
}
return c.wsSend(subscribe)
}

View File

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

View File

@@ -81,7 +81,10 @@ func (c *COINUT) SetDefaults() {
exchange.WebsocketOrderbookSupported |
exchange.WebsocketTradeDataSupported |
exchange.WebsocketSubscribeSupported |
exchange.WebsocketUnsubscribeSupported
exchange.WebsocketUnsubscribeSupported |
exchange.WebsocketAuthenticatedEndpointsSupported |
exchange.WebsocketSubmitOrderSupported |
exchange.WebsocketCancelOrderSupported
}
// Setup sets the current exchange configuration
@@ -91,6 +94,7 @@ func (c *COINUT) Setup(exch *config.ExchangeConfig) {
} else {
c.Enabled = true
c.AuthenticatedAPISupport = exch.AuthenticatedAPISupport
c.AuthenticatedWebsocketAPISupport = exch.AuthenticatedWebsocketAPISupport
c.SetAPIKeys(exch.APIKey, exch.APISecret, exch.ClientID, false)
c.SetHTTPClientTimeout(exch.HTTPTimeout)
c.SetHTTPClientUserAgent(exch.HTTPUserAgent)

View File

@@ -1,16 +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 (
@@ -31,6 +35,7 @@ func TestSetup(t *testing.T) {
t.Error("Test Failed - Coinut Setup() init error")
}
bConfig.AuthenticatedAPISupport = true
bConfig.AuthenticatedWebsocketAPISupport = true
bConfig.APIKey = apiKey
c.Setup(&bConfig)
c.ClientID = clientID
@@ -43,6 +48,46 @@ func TestSetup(t *testing.T) {
}
}
func setupWSTestAuth(t *testing.T) {
if wsSetupRan {
return
}
c.SetDefaults()
TestSetup(t)
if !c.Websocket.IsEnabled() && !c.AuthenticatedWebsocketAPISupport || !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 {
@@ -402,3 +447,101 @@ func TestGetDepositAddress(t *testing.T) {
t.Error("Test Failed - GetDepositAddress() function unsupported cannot be nil")
}
}
// TestWsAuthGetAccountBalance dials websocket, sends login request.
func TestWsAuthGetAccountBalance(t *testing.T) {
setupWSTestAuth(t)
err := c.wsGetAccountBalance()
if err != nil {
t.Error(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseExtendedTimeout)
select {
case resp := <-c.Websocket.DataHandler:
if resp.(WsUserBalanceResponse).Status[0] != "OK" {
t.Error("Expected successful response")
}
case <-timer.C:
t.Error("Expected response")
}
timer.Stop()
}
// TestWsAuthSubmitOrders dials websocket, sends login request.
func TestWsAuthSubmitOrders(t *testing.T) {
setupWSTestAuth(t)
order := WsSubmitOrderParameters{
Amount: 1,
Currency: currency.NewPair(currency.LTC, currency.BTC),
OrderID: 1,
Price: 1,
Side: exchange.BuyOrderSide,
}
err := c.wsSubmitOrders([]WsSubmitOrderParameters{order, order})
if err != nil {
t.Error(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseExtendedTimeout)
select {
case <-c.Websocket.DataHandler:
case <-timer.C:
t.Error("Expected response")
}
timer.Stop()
}
// TestWsAuthCancelOrders dials websocket, sends login request.
func TestWsAuthCancelOrders(t *testing.T) {
setupWSTestAuth(t)
order := WsCancelOrderParameters{
Currency: currency.NewPair(currency.LTC, currency.BTC),
OrderID: 1,
}
err := c.wsCancelOrders([]WsCancelOrderParameters{order, order})
if err != nil {
t.Error(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseExtendedTimeout)
select {
case <-c.Websocket.DataHandler:
case <-timer.C:
t.Error("Expected response")
}
timer.Stop()
}
// TestWsAuthCancelOrder dials websocket, sends login request.
func TestWsAuthCancelOrder(t *testing.T) {
setupWSTestAuth(t)
order := WsCancelOrderParameters{
Currency: currency.NewPair(currency.LTC, currency.BTC),
OrderID: 1,
}
err := c.wsCancelOrder(order)
if err != nil {
t.Error(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseExtendedTimeout)
select {
case <-c.Websocket.DataHandler:
case <-timer.C:
t.Error("Expected response")
}
timer.Stop()
}
// TestWsAuthGetOpenOrders dials websocket, sends login request.
func TestWsAuthGetOpenOrders(t *testing.T) {
setupWSTestAuth(t)
err := c.wsGetOpenOrders(currency.NewPair(currency.LTC, currency.BTC))
if err != nil {
t.Error(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseExtendedTimeout)
select {
case <-c.Websocket.DataHandler:
case <-timer.C:
t.Error("Expected response")
}
timer.Stop()
}

View File

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

View File

@@ -2,8 +2,10 @@ package coinut
import (
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"github.com/gorilla/websocket"
@@ -28,140 +30,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
}
c.Websocket.DataHandler <- exchange.TickerData{
Timestamp: time.Unix(0, ticker.Timestamp),
Exchange: c.GetName(),
AssetType: "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: "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: "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: "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() {
@@ -197,7 +65,7 @@ func (c *COINUT) WsConnect() error {
}
populatedList = true
}
c.wsAuthenticate()
c.GenerateDefaultSubscriptions()
// define bi-directional communication
@@ -205,10 +73,242 @@ 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.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
}
c.Websocket.DataHandler <- exchange.TickerData{
Timestamp: time.Unix(0, ticker.Timestamp),
Exchange: c.GetName(),
AssetType: "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: "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: "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: "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 {
@@ -309,7 +409,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.GetEnabledCurrencies()
for i := range channels {
for j := range enabledCurrencies {
@@ -360,3 +460,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.ClientID, timestamp, nonce)
hmac := common.GetHMAC(common.HashSHA256, []byte(payload), []byte(c.APIKey))
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.ClientID,
Nonce: nonce,
Hmac: common.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 := exchange.FormatExchangeCurrency(c.Name, order.Currency).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 := exchange.FormatExchangeCurrency(c.Name, orders[i].Currency).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 := exchange.FormatExchangeCurrency(c.Name, p).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 := exchange.FormatExchangeCurrency(c.Name, cancellation.Currency).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 := exchange.FormatExchangeCurrency(c.Name, cancellations[i].Currency).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 := exchange.FormatExchangeCurrency(c.Name, p).String()
var request WsTradeHistoryRequest
request.Request = "trade_history"
request.InstID = instrumentListByString[currency]
request.Nonce = c.GetNonce()
request.Start = start
request.Limit = limit
return c.wsSend(request)
}

View File

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

View File

@@ -181,6 +181,9 @@ const (
NoFiatWithdrawalsText string = "NO FIAT WITHDRAWAL"
UnknownWithdrawalTypeText string = "UNKNOWN"
RestAuthentication uint8 = 0
WebsocketAuthentication uint8 = 1
)
// AccountInfo is a Generic type to hold each exchange's holdings in
@@ -258,6 +261,7 @@ type Base struct {
Verbose bool
RESTPollingDelay time.Duration
AuthenticatedAPISupport bool
AuthenticatedWebsocketAPISupport bool
APIWithdrawPermissions uint32
APIAuthPEMKeySupport bool
APISecret, APIKey, APIAuthPEMKey, ClientID string
@@ -300,7 +304,7 @@ type IBotExchange interface {
GetAvailableCurrencies() currency.Pairs
GetAssetTypes() []string
GetAccountInfo() (AccountInfo, error)
GetAuthenticatedAPISupport() bool
GetAuthenticatedAPISupport(endpoint uint8) bool
SetCurrencies(pairs []currency.Pair, enabledPairs bool) error
GetExchangeHistory(p currency.Pair, assetType string) ([]TradeHistory, error)
SupportsAutoPairUpdates() bool
@@ -325,6 +329,8 @@ type IBotExchange interface {
GetWebsocket() (*Websocket, error)
SubscribeToWebsocketChannels(channels []WebsocketChannelSubscription) error
UnsubscribeToWebsocketChannels(channels []WebsocketChannelSubscription) error
AuthenticateWebsocket() error
GetSubscriptions() ([]WebsocketChannelSubscription, error)
}
// SupportsRESTTickerBatchUpdates returns whether or not the
@@ -573,8 +579,14 @@ func (e *Base) SetCurrencyPairFormat() error {
// GetAuthenticatedAPISupport returns whether the exchange supports
// authenticated API requests
func (e *Base) GetAuthenticatedAPISupport() bool {
return e.AuthenticatedAPISupport
func (e *Base) GetAuthenticatedAPISupport(endpoint uint8) bool {
switch endpoint {
case RestAuthentication:
return e.AuthenticatedAPISupport
case WebsocketAuthentication:
return e.AuthenticatedWebsocketAPISupport
}
return false
}
// GetName is a method that returns the name of the exchange base
@@ -672,17 +684,16 @@ func (e *Base) IsEnabled() bool {
// SetAPIKeys is a method that sets the current API keys for the exchange
func (e *Base) SetAPIKeys(apiKey, apiSecret, clientID string, b64Decode bool) {
if !e.AuthenticatedAPISupport {
if !e.AuthenticatedAPISupport && !e.AuthenticatedWebsocketAPISupport {
return
}
e.APIKey = apiKey
e.ClientID = clientID
if b64Decode {
result, err := common.Base64Decode(apiSecret)
if err != nil {
e.AuthenticatedAPISupport = false
e.AuthenticatedWebsocketAPISupport = false
log.Warnf(warningBase64DecryptSecretKeyFailed, e.Name)
}
e.APISecret = string(result)

View File

@@ -371,13 +371,25 @@ func TestSetCurrencyPairFormat(t *testing.T) {
}
}
// TestGetAuthenticatedAPISupport logic test
func TestGetAuthenticatedAPISupport(t *testing.T) {
base := Base{
AuthenticatedAPISupport: false,
AuthenticatedAPISupport: true,
AuthenticatedWebsocketAPISupport: false,
}
if base.GetAuthenticatedAPISupport() {
t.Fatal("Test failed. TestGetAuthenticatedAPISupport returned true when it should of been 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.AuthenticatedWebsocketAPISupport = 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")
}
}
@@ -669,11 +681,13 @@ func TestIsEnabled(t *testing.T) {
}
}
// TestSetAPIKeys logic test
func TestSetAPIKeys(t *testing.T) {
SetAPIKeys := Base{
Name: "TESTNAME",
Enabled: false,
AuthenticatedAPISupport: false,
Name: "TESTNAME",
Enabled: false,
AuthenticatedAPISupport: false,
AuthenticatedWebsocketAPISupport: false,
}
SetAPIKeys.SetAPIKeys("RocketMan", "Digereedoo", "007", false)
@@ -682,10 +696,26 @@ func TestSetAPIKeys(t *testing.T) {
}
SetAPIKeys.AuthenticatedAPISupport = true
SetAPIKeys.AuthenticatedWebsocketAPISupport = true
SetAPIKeys.SetAPIKeys("RocketMan", "Digereedoo", "007", false)
if SetAPIKeys.APIKey != "RocketMan" && SetAPIKeys.APISecret != "Digereedoo" && SetAPIKeys.ClientID != "007" {
t.Error("Test Failed - Exchange SetAPIKeys() did not set correct values")
}
SetAPIKeys.AuthenticatedAPISupport = false
SetAPIKeys.AuthenticatedWebsocketAPISupport = true
SetAPIKeys.SetAPIKeys("RocketMan", "Digereedoo", "007", false)
if SetAPIKeys.APIKey != "RocketMan" && SetAPIKeys.APISecret != "Digereedoo" && SetAPIKeys.ClientID != "007" {
t.Error("Test Failed - Exchange SetAPIKeys() did not set correct values")
}
SetAPIKeys.AuthenticatedAPISupport = true
SetAPIKeys.AuthenticatedWebsocketAPISupport = false
SetAPIKeys.SetAPIKeys("RocketMan", "Digereedoo", "007", false)
if SetAPIKeys.APIKey != "RocketMan" && SetAPIKeys.APISecret != "Digereedoo" && SetAPIKeys.ClientID != "007" {
t.Error("Test Failed - Exchange SetAPIKeys() did not set correct values")
}
SetAPIKeys.SetAPIKeys("RocketMan", "Digereedoo", "007", true)
}

View File

@@ -50,6 +50,7 @@ func (e *Base) WebsocketSetup(connector func() error,
e.Websocket.SetConnector(connector)
e.Websocket.SetWebsocketURL(runningURL)
e.Websocket.SetExchangeName(exchangeName)
e.Websocket.SetCanUseAuthenticatedEndpoints(e.AuthenticatedWebsocketAPISupport)
e.Websocket.init = false
e.Websocket.noConnectionCheckLimit = 5
@@ -673,6 +674,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))
@@ -838,7 +854,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
}
@@ -855,3 +879,25 @@ func (w *WebsocketChannelSubscription) Equal(subscribedChannel *WebsocketChannel
return strings.EqualFold(w.Channel, subscribedChannel.Channel) &&
strings.EqualFold(w.Currency.String(), subscribedChannel.Currency.String())
}
// GetSubscriptions returns a copied list of subscriptions
// subscriptions is a private member and cannot be manipulated
func (w *Websocket) GetSubscriptions() []WebsocketChannelSubscription {
return append(w.subscribedChannels[:0:0], w.subscribedChannels...)
}
// SetCanUseAuthenticatedEndpoints sets canUseAuthenticatedEndpoints val in
// a thread safe manner
func (w *Websocket) SetCanUseAuthenticatedEndpoints(val bool) {
w.subscriptionLock.Lock()
defer w.subscriptionLock.Unlock()
w.canUseAuthenticatedEndpoints = val
}
// CanUseAuthenticatedEndpoints gets canUseAuthenticatedEndpoints val in
// a thread safe manner
func (w *Websocket) CanUseAuthenticatedEndpoints() bool {
w.subscriptionLock.Lock()
defer w.subscriptionLock.Unlock()
return w.canUseAuthenticatedEndpoints
}

View File

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

View File

@@ -19,17 +19,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"
@@ -88,7 +98,8 @@ type Websocket struct {
// TrafficAlert monitors if there is a halt in traffic throughput
TrafficAlert chan struct{}
// Functionality defines websocket stream capabilities
Functionality uint32
Functionality uint32
canUseAuthenticatedEndpoints bool
}
// WebsocketChannelSubscription container for websocket subscriptions

View File

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

View File

@@ -82,7 +82,8 @@ func (g *Gateio) SetDefaults() {
exchange.WebsocketOrderbookSupported |
exchange.WebsocketKlineSupported |
exchange.WebsocketSubscribeSupported |
exchange.WebsocketUnsubscribeSupported
exchange.WebsocketUnsubscribeSupported |
exchange.WebsocketAuthenticatedEndpointsSupported
}
// Setup sets user configuration
@@ -92,6 +93,7 @@ func (g *Gateio) Setup(exch *config.ExchangeConfig) {
} else {
g.Enabled = true
g.AuthenticatedAPISupport = exch.AuthenticatedAPISupport
g.AuthenticatedWebsocketAPISupport = exch.AuthenticatedWebsocketAPISupport
g.SetAPIKeys(exch.APIKey, exch.APISecret, "", false)
g.APIAuthPEMKey = exch.APIAuthPEMKey
g.SetHTTPClientTimeout(exch.HTTPTimeout)

View File

@@ -1,12 +1,16 @@
package gateio
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"
)
// Please supply your own APIKEYS here for due diligence testing
@@ -30,6 +34,7 @@ func TestSetup(t *testing.T) {
if err != nil {
t.Error("Test Failed - GateIO Setup() init error")
}
gateioConfig.AuthenticatedWebsocketAPISupport = true
gateioConfig.AuthenticatedAPISupport = true
gateioConfig.APIKey = apiKey
gateioConfig.APISecret = apiSecret
@@ -490,3 +495,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.AuthenticatedWebsocketAPISupport || !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 !common.StringContains(resultString.(string), "success") {
t.Error("Authentication failed")
}
case <-timer.C:
t.Error("Expected response")
}
timer.Stop()
err = g.wsGetBalance()
if err != nil {
t.Error(err)
}
timer = time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case <-g.Websocket.DataHandler:
case <-timer.C:
t.Error("Expected response")
}
timer.Stop()
}

View File

@@ -46,22 +46,21 @@ func (g *Gateio) WsConnect() error {
if err != nil {
return err
}
if g.AuthenticatedAPISupport {
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 := common.Base64Encode(sigTemp)
@@ -70,7 +69,13 @@ func (g *Gateio) wsServerSignIn() error {
Method: "server.sign",
Params: []interface{}{g.APIKey, 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
@@ -114,20 +119,22 @@ func (g *Gateio) WsHandleData() {
g.Websocket.DataHandler <- err
continue
}
if result.Error.Code != 0 {
if common.StringContains(result.Error.Message, "authentication") {
g.Websocket.DataHandler <- fmt.Errorf("%v - WebSocket authentication failed ",
g.GetName())
g.AuthenticatedAPISupport = 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{}
@@ -339,14 +346,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.GetEnabledCurrencies()
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.AuthenticatedAPISupport {
channels = append(channels, "balance.subscribe", "order.subscribe")
}
subscriptions := []exchange.WebsocketChannelSubscription{}
var subscriptions []exchange.WebsocketChannelSubscription
enabledCurrencies := g.GetEnabledCurrencies()
for i := range channels {
for j := range enabledCurrencies {
@@ -399,6 +421,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",
@@ -408,6 +433,9 @@ func (g *Gateio) wsGetBalance() error {
}
func (g *Gateio) wsGetOrderInfo(market string, offset, limit int) error {
if !g.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authorised to get order info", g.Name)
}
order := WebsocketRequest{
ID: IDOrderQuery,
Method: "order.query",

View File

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

View File

@@ -123,7 +123,8 @@ func (g *Gemini) SetDefaults() {
g.APIUrl = g.APIUrlDefault
g.WebsocketInit()
g.Websocket.Functionality = exchange.WebsocketOrderbookSupported |
exchange.WebsocketTradeDataSupported
exchange.WebsocketTradeDataSupported |
exchange.WebsocketAuthenticatedEndpointsSupported
}
// Setup sets exchange configuration parameters
@@ -133,6 +134,7 @@ func (g *Gemini) Setup(exch *config.ExchangeConfig) {
} else {
g.Enabled = true
g.AuthenticatedAPISupport = exch.AuthenticatedAPISupport
g.AuthenticatedWebsocketAPISupport = exch.AuthenticatedWebsocketAPISupport
g.SetAPIKeys(exch.APIKey, exch.APISecret, "", false)
g.SetHTTPClientTimeout(exch.HTTPTimeout)
g.SetHTTPClientUserAgent(exch.HTTPUserAgent)
@@ -142,7 +144,7 @@ func (g *Gemini) Setup(exch *config.ExchangeConfig) {
g.BaseCurrencies = exch.BaseCurrencies
g.AvailablePairs = exch.AvailablePairs
g.EnabledPairs = exch.EnabledPairs
g.WebsocketURL = geminiWebsocketEndpoint
err := g.SetCurrencyPairFormat()
if err != nil {
log.Fatal(err)
@@ -161,6 +163,7 @@ func (g *Gemini) Setup(exch *config.ExchangeConfig) {
}
if exch.UseSandbox {
g.APIUrl = geminiSandboxAPIURL
g.WebsocketURL = geminiWebsocketSandboxEndpoint
}
err = g.SetClientProxyAddress(exch.ProxyAddress)
if err != nil {
@@ -172,8 +175,8 @@ func (g *Gemini) Setup(exch *config.ExchangeConfig) {
exch.Name,
exch.Websocket,
exch.Verbose,
geminiWebsocketEndpoint,
exch.WebsocketURL)
g.WebsocketURL,
g.WebsocketURL)
if err != nil {
log.Fatal(err)
}

View File

@@ -3,11 +3,14 @@ package gemini
import (
"net/url"
"testing"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/config"
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/sharedtestvalues"
)
// Please enter sandbox API keys & assigned roles for better testing procedures
@@ -61,8 +64,9 @@ func TestSetup(t *testing.T) {
t.Error("Test Failed - Gemini Setup() init error")
}
geminiConfig.AuthenticatedWebsocketAPISupport = true
geminiConfig.AuthenticatedAPISupport = true
geminiConfig.Websocket = true
Session[1].Setup(&geminiConfig)
Session[2].Setup(&geminiConfig)
@@ -554,3 +558,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.WebsocketURL = geminiWebsocketSandboxEndpoint
if !g.Websocket.IsEnabled() && !g.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() {
t.Skip(exchange.WebsocketNotEnabled)
}
var dialer websocket.Dialer
go g.WsHandleData()
err := g.WsSecureSubscribe(&dialer, geminiWsOrderEvents)
if err != nil {
t.Error(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case resp := <-g.Websocket.DataHandler:
if resp.(WsSubscriptionAcknowledgementResponse).Type != "subscription_ack" {
t.Error("Login failed")
}
case <-timer.C:
t.Error("Expected response")
}
timer.Stop()
}

View File

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

View File

@@ -14,12 +14,15 @@ import (
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"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
@@ -37,12 +40,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)
}
@@ -52,59 +57,75 @@ 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.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("%v %v %v Error: %v", endpoint, conStatus, conStatus.StatusCode, 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.WebsocketURL, url)
PayloadBase64 := common.Base64Encode(PayloadJSON)
hmac := common.GetHMAC(common.HashSHA512_384, []byte(PayloadBase64), []byte(g.APISecret))
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.APIKey)
headers.Add("X-GEMINI-SIGNATURE", common.HexEncodeToString(hmac))
headers.Add("Cache-Control", "no-cache")
conn, conStatus, err := dialer.Dial(endpoint, headers)
if err != nil {
return fmt.Errorf("%v %v %v Error: %v", endpoint, conStatus, conStatus.StatusCode, err)
}
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
@@ -112,119 +133,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 = "SPOT"
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: "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: "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(),
"SPOT")
if err != nil {
g.Websocket.DataHandler <- err
}
} else {
err := g.Websocket.Orderbook.Update([]orderbook.Item{i},
nil,
resp.Currency,
time.Now(),
g.GetName(),
"SPOT")
if err != nil {
g.Websocket.DataHandler <- err
}
}
}
}
g.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{Pair: resp.Currency,
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 = "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: "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: "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(),
"SPOT")
if err != nil {
g.Websocket.DataHandler <- err
}
} else {
err := g.Websocket.Orderbook.Update([]orderbook.Item{i},
nil,
pair,
time.Now(),
g.GetName(),
"SPOT")
if err != nil {
g.Websocket.DataHandler <- err
}
}
}
}
g.Websocket.DataHandler <- exchange.WebsocketOrderbookUpdate{Pair: pair,
Asset: "SPOT",
Exchange: g.GetName()}
}
}

View File

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

View File

@@ -84,7 +84,10 @@ func (h *HitBTC) SetDefaults() {
h.Websocket.Functionality = exchange.WebsocketTickerSupported |
exchange.WebsocketOrderbookSupported |
exchange.WebsocketSubscribeSupported |
exchange.WebsocketUnsubscribeSupported
exchange.WebsocketUnsubscribeSupported |
exchange.WebsocketAuthenticatedEndpointsSupported |
exchange.WebsocketSubmitOrderSupported |
exchange.WebsocketCancelOrderSupported
}
// Setup sets user exchange configuration settings
@@ -94,6 +97,7 @@ func (h *HitBTC) Setup(exch *config.ExchangeConfig) {
} else {
h.Enabled = true
h.AuthenticatedAPISupport = exch.AuthenticatedAPISupport
h.AuthenticatedWebsocketAPISupport = exch.AuthenticatedWebsocketAPISupport
h.SetAPIKeys(exch.APIKey, exch.APISecret, "", false)
h.SetHTTPClientTimeout(exch.HTTPTimeout)
h.SetHTTPClientUserAgent(exch.HTTPUserAgent)

View File

@@ -1,12 +1,16 @@
package hitbtc
import (
"net/http"
"testing"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/config"
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/sharedtestvalues"
)
var h HitBTC
@@ -29,6 +33,7 @@ func TestSetup(t *testing.T) {
if err != nil {
t.Error("Test Failed - HitBTC Setup() init error")
}
hitbtcConfig.AuthenticatedWebsocketAPISupport = true
hitbtcConfig.AuthenticatedAPISupport = true
hitbtcConfig.APIKey = apiKey
hitbtcConfig.APISecret = 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)
}
@@ -381,3 +386,107 @@ func TestGetDepositAddress(t *testing.T) {
}
}
}
func setupWsAuth(t *testing.T) {
TestSetDefaults(t)
TestSetup(t)
if !h.Websocket.IsEnabled() && !h.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() {
t.Skip(exchange.WebsocketNotEnabled)
}
var err error
var dialer websocket.Dialer
h.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
h.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride()
h.WebsocketConn, _, err = dialer.Dial(hitbtcWebsocketAddress, http.Header{})
if err != nil {
t.Fatal(err)
}
go h.WsHandleData()
h.wsLogin()
timer := time.NewTimer(time.Second)
select {
case loginError := <-h.Websocket.DataHandler:
t.Fatal(loginError)
case <-timer.C:
}
timer.Stop()
}
// TestWsCancelOrder dials websocket, sends cancel request.
func TestWsCancelOrder(t *testing.T) {
setupWsAuth(t)
err := h.wsCancelOrder("ImNotARealOrderID")
if err != nil {
t.Fatal(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case <-h.Websocket.DataHandler:
case <-timer.C:
t.Error("Expecting response")
}
timer.Stop()
}
// TestWsPlaceOrder dials websocket, sends order submission.
func TestWsPlaceOrder(t *testing.T) {
setupWsAuth(t)
err := h.wsPlaceOrder(currency.NewPair(currency.LTC, currency.BTC), exchange.BuyOrderSide.ToString(), 1, 1)
if err != nil {
t.Fatal(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case <-h.Websocket.DataHandler:
case <-timer.C:
t.Error("Expecting response")
}
timer.Stop()
}
// TestWsReplaceOrder dials websocket, sends replace order request.
func TestWsReplaceOrder(t *testing.T) {
setupWsAuth(t)
err := h.wsReplaceOrder("ImNotARealOrderID", 1, 1)
if err != nil {
t.Fatal(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case <-h.Websocket.DataHandler:
case <-timer.C:
t.Error("Expecting response")
}
timer.Stop()
}
// TestWsGetActiveOrders dials websocket, sends get active orders request.
func TestWsGetActiveOrders(t *testing.T) {
setupWsAuth(t)
err := h.wsGetActiveOrders()
if err != nil {
t.Fatal(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case <-h.Websocket.DataHandler:
case <-timer.C:
t.Error("Expecting response")
}
timer.Stop()
}
// TestWsGetTradingBalance dials websocket, sends get trading balance request.
func TestWsGetTradingBalance(t *testing.T) {
setupWsAuth(t)
err := h.wsGetTradingBalance()
if err != nil {
t.Fatal(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case <-h.Websocket.DataHandler:
case <-timer.C:
t.Error("Expecting response")
}
timer.Stop()
}

View File

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

View File

@@ -12,6 +12,7 @@ import (
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/nonce"
"github.com/thrasher-/gocryptotrader/exchanges/orderbook"
log "github.com/thrasher-/gocryptotrader/logger"
)
@@ -21,6 +22,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() {
@@ -45,6 +48,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
@@ -89,86 +97,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: "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: "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 {
@@ -240,7 +308,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.GetEnabledCurrencies()
for i := range channels {
for j := range enabledCurrencies {
@@ -257,11 +330,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{
@@ -314,7 +388,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 := common.GetHMAC(common.HashSHA256, []byte(nonce), []byte(h.APISecret))
request := WsLoginRequest{
Method: "login",
Params: WsLoginData{
Algo: "HS256",
PKey: h.APIKey,
Nonce: nonce,
Signature: common.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: common.StringToLower(side),
Price: price,
Quantity: quantity,
},
ID: int64(requestID.GetInc()),
}
return h.wsSend(request)
}
// wsCancelOrder sends a websocket message to cancel an order
func (h *HitBTC) wsCancelOrder(clientOrderID string) error {
if !h.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authenticated, cannot place order", h.Name)
}
request := WsCancelOrderRequest{
Method: "cancelOrder",
Params: WsCancelOrderRequestData{
ClientOrderID: clientOrderID,
},
ID: int64(requestID.GetInc()),
}
return h.wsSend(request)
}
// wsReplaceOrder sends a websocket message to replace an order
func (h *HitBTC) wsReplaceOrder(clientOrderID string, quantity, price float64) error {
if !h.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authenticated, cannot place order", h.Name)
}
request := WsReplaceOrderRequest{
Method: "cancelReplaceOrder",
Params: WsReplaceOrderRequestData{
ClientOrderID: clientOrderID,
RequestClientID: fmt.Sprintf("%v", time.Now().Unix()),
Quantity: quantity,
Price: price,
},
ID: int64(requestID.GetInc()),
}
return h.wsSend(request)
}
// wsGetActiveOrders sends a websocket message to get all active orders
func (h *HitBTC) wsGetActiveOrders() error {
if !h.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authenticated, cannot place order", h.Name)
}
request := WsReplaceOrderRequest{
Method: "getOrders",
Params: WsReplaceOrderRequestData{},
ID: int64(requestID.GetInc()),
}
return h.wsSend(request)
}
// wsGetTradingBalance sends a websocket message to get trading balance
func (h *HitBTC) wsGetTradingBalance() error {
if !h.Websocket.CanUseAuthenticatedEndpoints() {
return fmt.Errorf("%v not authenticated, cannot place order", h.Name)
}
request := WsReplaceOrderRequest{
Method: "getTradingBalance",
Params: WsReplaceOrderRequestData{},
ID: int64(requestID.GetInc()),
}
return h.wsSend(request)
}

View File

@@ -6,7 +6,6 @@ import (
"strconv"
"strings"
"sync"
"time"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/currency"
@@ -319,18 +318,12 @@ func (h *HitBTC) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([
symbol := currency.NewPairDelimiter(allOrders[i].Symbol,
h.ConfigCurrencyPairFormat.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,
})
@@ -364,18 +357,12 @@ func (h *HitBTC) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([
symbol := currency.NewPairDelimiter(allOrders[i].Symbol,
h.ConfigCurrencyPairFormat.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,
})
@@ -400,3 +387,13 @@ func (h *HitBTC) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketCha
h.Websocket.UnsubscribeToChannels(channels)
return nil
}
// GetSubscriptions returns a copied list of subscriptions
func (h *HitBTC) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return h.Websocket.GetSubscriptions(), nil
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (h *HitBTC) AuthenticateWebsocket() error {
return h.wsLogin()
}

View File

@@ -67,9 +67,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
}
// SetDefaults sets default values for the exchange
@@ -99,7 +100,9 @@ func (h *HUOBI) SetDefaults() {
exchange.WebsocketOrderbookSupported |
exchange.WebsocketTradeDataSupported |
exchange.WebsocketSubscribeSupported |
exchange.WebsocketUnsubscribeSupported
exchange.WebsocketUnsubscribeSupported |
exchange.WebsocketAuthenticatedEndpointsSupported |
exchange.WebsocketAccountDataSupported
}
// Setup sets user configuration
@@ -109,6 +112,7 @@ func (h *HUOBI) Setup(exch *config.ExchangeConfig) {
} else {
h.Enabled = true
h.AuthenticatedAPISupport = exch.AuthenticatedAPISupport
h.AuthenticatedWebsocketAPISupport = exch.AuthenticatedWebsocketAPISupport
h.SetAPIKeys(exch.APIKey, exch.APISecret, "", false)
h.APIAuthPEMKeySupport = exch.APIAuthPEMKeySupport
h.APIAuthPEMKey = exch.APIAuthPEMKey
@@ -147,7 +151,7 @@ func (h *HUOBI) Setup(exch *config.ExchangeConfig) {
exch.Name,
exch.Websocket,
exch.Verbose,
huobiSocketIOAddress,
wsMarketURL,
exch.WebsocketURL)
if err != nil {
log.Fatal(err)

View File

@@ -9,11 +9,14 @@ import (
"strconv"
"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 you own test keys here for due diligence testing.
@@ -25,35 +28,7 @@ const (
)
var h HUOBI
// getDefaultConfig returns a default huobi config
func getDefaultConfig() config.ExchangeConfig {
return config.ExchangeConfig{
Name: "Huobi",
Enabled: true,
Verbose: true,
Websocket: false,
UseSandbox: false,
RESTPollingDelay: 10,
HTTPTimeout: 15000000000,
AuthenticatedAPISupport: true,
APIKey: "",
APISecret: "",
ClientID: "",
AvailablePairs: currency.NewPairsFromStrings([]string{"BTC-USDT", "BCH-USDT"}),
EnabledPairs: currency.NewPairsFromStrings([]string{"BTC-USDT"}),
BaseCurrencies: currency.NewCurrenciesFromStringArray([]string{"USD"}),
AssetTypes: "SPOT",
SupportsAutoPairUpdates: false,
ConfigCurrencyPairFormat: &config.CurrencyPairFormatConfig{
Uppercase: true,
Delimiter: "-",
},
RequestCurrencyPairFormat: &config.CurrencyPairFormatConfig{
Uppercase: false,
},
}
}
var wsSetupRan bool
func TestSetDefaults(t *testing.T) {
h.SetDefaults()
@@ -67,12 +42,54 @@ func TestSetup(t *testing.T) {
t.Error("Test Failed - Huobi Setup() init error")
}
hConfig.AuthenticatedAPISupport = true
hConfig.AuthenticatedWebsocketAPISupport = true
hConfig.APIKey = apiKey
hConfig.APISecret = apiSecret
h.Setup(&hConfig)
}
func setupWsTests(t *testing.T) {
if wsSetupRan {
return
}
TestSetDefaults(t)
TestSetup(t)
if !h.Websocket.IsEnabled() && !h.AuthenticatedWebsocketAPISupport || !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{
@@ -622,3 +639,50 @@ func TestGetDepositAddress(t *testing.T) {
t.Error("Test Failed - GetDepositAddress() error cannot be nil")
}
}
// TestWsGetAccountsList connects to WS, logs in, gets account list
func TestWsGetAccountsList(t *testing.T) {
setupWsTests(t)
h.wsGetAccountsList(currency.NewPairFromString("ethbtc"))
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case response := <-h.Websocket.DataHandler:
switch respType := response.(type) {
case WsAuthenticatedAccountsListResponse:
if respType.ErrorCode > 0 {
t.Error(respType)
}
case error:
t.Error(respType)
}
case <-timer.C:
t.Error("Websocket did not receive a response")
}
timer.Stop()
}
// TestWsGetOrderList connects to WS, logs in, gets order list
func TestWsGetOrderList(t *testing.T) {
setupWsTests(t)
h.wsGetOrdersList(1, currency.NewPairFromString("ethbtc"))
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case <-h.Websocket.DataHandler:
case <-timer.C:
t.Error("Websocket did not receive a response")
}
timer.Stop()
}
// TestWsGetOrderDetails connects to WS, logs in, gets order details
func TestWsGetOrderDetails(t *testing.T) {
setupWsTests(t)
h.wsGetOrderDetails("123")
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case <-h.Websocket.DataHandler:
case <-timer.C:
t.Error("Websocket did not receive a response")
}
timer.Stop()
}

View File

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

View File

@@ -8,6 +8,7 @@ import (
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
"github.com/gorilla/websocket"
@@ -19,12 +20,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() {
@@ -42,140 +64,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 common.StringContains(init.Topic, "orders") &&
common.StringContains(init.Topic, "update"):
var response WsAuthenticatedOrdersUpdateResponse
err := common.JSONDecode(resp.Raw, &response)
if err != nil {
h.Websocket.DataHandler <- err
}
h.Websocket.DataHandler <- response
case common.StringContains(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 common.StringContains(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 := common.SplitStrings(depth.Channel, ".")
h.WsProcessOrderbook(&depth, data[1])
case common.StringContains(init.Channel, "kline"):
var kline WsKline
err := common.JSONDecode(resp.Raw, &kline)
if err != nil {
h.Websocket.DataHandler <- err
continue
}
data := common.SplitStrings(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 common.StringContains(init.Channel, "trade"):
var trade WsTrade
err := common.JSONDecode(resp.Raw, &trade)
if err != nil {
h.Websocket.DataHandler <- err
continue
}
data := common.SplitStrings(trade.Channel, ".")
h.Websocket.DataHandler <- exchange.TradeData{
Exchange: h.GetName(),
AssetType: "SPOT",
CurrencyPair: currency.NewPairFromString(data[1]),
Timestamp: time.Unix(0, trade.Tick.Timestamp),
}
}
switch {
case common.StringContains(init.Channel, "depth"):
var depth WsDepth
err := common.JSONDecode(resp.Raw, &depth)
if err != nil {
h.Websocket.DataHandler <- err
return
}
data := common.SplitStrings(depth.Channel, ".")
h.WsProcessOrderbook(&depth, data[1])
case common.StringContains(init.Channel, "kline"):
var kline WsKline
err := common.JSONDecode(resp.Raw, &kline)
if err != nil {
h.Websocket.DataHandler <- err
return
}
data := common.SplitStrings(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 common.StringContains(init.Channel, "trade"):
var trade WsTrade
err := common.JSONDecode(resp.Raw, &trade)
if err != nil {
h.Websocket.DataHandler <- err
return
}
data := common.SplitStrings(trade.Channel, ".")
h.Websocket.DataHandler <- exchange.TradeData{
Exchange: h.GetName(),
AssetType: "SPOT",
CurrencyPair: currency.NewPairFromString(data[1]),
Timestamp: time.Unix(0, trade.Tick.Timestamp),
}
}
}
@@ -220,8 +366,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.GetEnabledCurrencies()
subscriptions := []exchange.WebsocketChannelSubscription{}
for i := range channels {
for j := range enabledCurrencies {
enabledCurrencies[j].Delimiter = ""
@@ -237,11 +389,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 common.StringContains(channelToSubscribe.Channel, "orders.") ||
common.StringContains(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
}
@@ -250,6 +402,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 common.StringContains(channelToSubscribe.Channel, "orders.") ||
common.StringContains(channelToSubscribe.Channel, "accounts") {
return h.wsAuthenticatedSubscribe("unsub", wsAccountsOrdersEndPoint+channelToSubscribe.Channel, channelToSubscribe.Channel)
}
subscription, err := common.JSONEncode(WsRequest{Unsubscribe: channelToSubscribe.Channel})
if err != nil {
return err
@@ -266,3 +422,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.APIKey,
SignatureMethod: signatureMethod,
SignatureVersion: signatureVersion,
Timestamp: timestamp,
}
hmac := h.wsGenerateSignature(timestamp, wsAccountsOrdersEndPoint)
request.Signature = common.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.APIKey)
values.Set("SignatureMethod", signatureMethod)
values.Set("SignatureVersion", signatureVersion)
values.Set("Timestamp", timestamp)
host := "api.huobi.pro"
payload := fmt.Sprintf("%s\n%s\n%s\n%s",
"GET", host, endpoint, values.Encode())
return common.GetHMAC(common.HashSHA256, []byte(payload), []byte(h.APISecret))
}
func (h *HUOBI) wsAuthenticatedSubscribe(operation, endpoint, topic string) error {
timestamp := time.Now().UTC().Format(wsDateTimeFormatting)
request := WsAuthenticatedSubscriptionRequest{
Op: operation,
AccessKeyID: h.APIKey,
SignatureMethod: signatureMethod,
SignatureVersion: signatureVersion,
Timestamp: timestamp,
Topic: topic,
}
hmac := h.wsGenerateSignature(timestamp, endpoint)
request.Signature = common.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.APIKey,
SignatureMethod: signatureMethod,
SignatureVersion: signatureVersion,
Timestamp: timestamp,
Topic: wsAccountsList,
Symbol: pair,
}
hmac := h.wsGenerateSignature(timestamp, wsAccountListEndpoint)
request.Signature = common.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.APIKey,
SignatureMethod: signatureMethod,
SignatureVersion: signatureVersion,
Timestamp: timestamp,
Topic: wsOrdersList,
AccountID: accountID,
Symbol: pair.Lower(),
States: "submitted,partial-filled",
}
hmac := h.wsGenerateSignature(timestamp, wsOrdersListEndpoint)
request.Signature = common.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.APIKey,
SignatureMethod: signatureMethod,
SignatureVersion: signatureVersion,
Timestamp: timestamp,
Topic: wsOrdersDetail,
OrderID: orderID,
}
hmac := h.wsGenerateSignature(timestamp, wsOrdersDetailEndpoint)
request.Signature = common.Base64Encode(hmac)
return h.wsAuthenticatedSend(request)
}

View File

@@ -29,7 +29,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)
log.Debugf("%s polling delay: %ds.\n", h.GetName(), h.RESTPollingDelay)
log.Debugf("%s %d currencies enabled: %s.\n", h.GetName(), len(h.EnabledPairs), h.EnabledPairs)
}
@@ -523,3 +523,13 @@ func (h *HUOBI) UnsubscribeToWebsocketChannels(channels []exchange.WebsocketChan
h.Websocket.UnsubscribeToChannels(channels)
return nil
}
// GetSubscriptions returns a copied list of subscriptions
func (h *HUOBI) GetSubscriptions() ([]exchange.WebsocketChannelSubscription, error) {
return h.Websocket.GetSubscriptions(), nil
}
// AuthenticateWebsocket sends an authentication message to the websocket
func (h *HUOBI) AuthenticateWebsocket() error {
return h.wsLogin()
}

View File

@@ -62,7 +62,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
}
@@ -94,7 +95,9 @@ func (h *HUOBIHADAX) SetDefaults() {
exchange.WebsocketTradeDataSupported |
exchange.WebsocketOrderbookSupported |
exchange.WebsocketSubscribeSupported |
exchange.WebsocketUnsubscribeSupported
exchange.WebsocketUnsubscribeSupported |
exchange.WebsocketAuthenticatedEndpointsSupported |
exchange.WebsocketAccountDataSupported
}
// Setup sets user configuration
@@ -104,6 +107,7 @@ func (h *HUOBIHADAX) Setup(exch *config.ExchangeConfig) {
} else {
h.Enabled = true
h.AuthenticatedAPISupport = exch.AuthenticatedAPISupport
h.AuthenticatedWebsocketAPISupport = exch.AuthenticatedWebsocketAPISupport
h.SetAPIKeys(exch.APIKey, exch.APISecret, "", false)
h.APIAuthPEMKeySupport = exch.APIAuthPEMKeySupport
h.APIAuthPEMKey = exch.APIAuthPEMKey
@@ -141,7 +145,7 @@ func (h *HUOBIHADAX) Setup(exch *config.ExchangeConfig) {
exch.Name,
exch.Websocket,
exch.Verbose,
huobiGlobalWebsocketEndpoint,
HuobiHadaxSocketIOAddress,
exch.WebsocketURL)
if err != nil {
log.Fatal(err)

View File

@@ -4,11 +4,14 @@ 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/sharedtestvalues"
)
// Please supply your own APIKEYS here for due diligence testing
@@ -21,35 +24,7 @@ const (
)
var h HUOBIHADAX
// getDefaultConfig returns a default huobi config
func getDefaultConfig() config.ExchangeConfig {
return config.ExchangeConfig{
Name: "huobihadax",
Enabled: true,
Verbose: true,
Websocket: false,
UseSandbox: false,
RESTPollingDelay: 10,
HTTPTimeout: 15000000000,
AuthenticatedAPISupport: true,
APIKey: "",
APISecret: "",
ClientID: "",
AvailablePairs: currency.NewPairsFromStrings([]string{"BTC-USDT", "BCH-USDT"}),
EnabledPairs: currency.NewPairsFromStrings([]string{"BTC-USDT"}),
BaseCurrencies: currency.NewCurrenciesFromStringArray([]string{"USD"}),
AssetTypes: "SPOT",
SupportsAutoPairUpdates: false,
ConfigCurrencyPairFormat: &config.CurrencyPairFormatConfig{
Uppercase: true,
Delimiter: "-",
},
RequestCurrencyPairFormat: &config.CurrencyPairFormatConfig{
Uppercase: false,
},
}
}
var wsSetupRan bool
func TestSetDefaults(t *testing.T) {
h.SetDefaults()
@@ -63,12 +38,54 @@ func TestSetup(t *testing.T) {
t.Error("Test Failed - HuobiHadax Setup() init error")
}
hadaxConfig.AuthenticatedAPISupport = true
hadaxConfig.AuthenticatedWebsocketAPISupport = true
hadaxConfig.APIKey = apiKey
hadaxConfig.APISecret = apiSecret
h.Setup(&hadaxConfig)
}
func setupWsTests(t *testing.T) {
if wsSetupRan {
return
}
TestSetDefaults(t)
TestSetup(t)
if !h.Websocket.IsEnabled() && !h.AuthenticatedWebsocketAPISupport || !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{
@@ -603,3 +620,50 @@ func TestGetDepositAddress(t *testing.T) {
t.Error("Test Failed - GetDepositAddress() error cannot be nil")
}
}
// TestWsGetAccountsList connects to WS, logs in, gets account list
func TestWsGetAccountsList(t *testing.T) {
setupWsTests(t)
h.wsGetAccountsList(currency.NewPairFromString("ethbtc"))
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case response := <-h.Websocket.DataHandler:
switch respType := response.(type) {
case WsAuthenticatedAccountsListResponse:
if respType.ErrorCode > 0 {
t.Error(respType)
}
case error:
t.Error(respType)
}
case <-timer.C:
t.Error("Websocket did not receive a response")
}
timer.Stop()
}
// TestWsGetOrderList connects to WS, logs in, gets order list
func TestWsGetOrderList(t *testing.T) {
setupWsTests(t)
h.wsGetOrdersList(1, currency.NewPairFromString("ethbtc"))
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case <-h.Websocket.DataHandler:
case <-timer.C:
t.Error("Websocket did not receive a response")
}
timer.Stop()
}
// TestWsGetOrderDetails connects to WS, logs in, gets order details
func TestWsGetOrderDetails(t *testing.T) {
setupWsTests(t)
h.wsGetOrderDetails("123")
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case <-h.Websocket.DataHandler:
case <-timer.C:
t.Error("Websocket did not receive a response")
}
timer.Stop()
}

View File

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

View File

@@ -8,6 +8,7 @@ import (
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
"github.com/gorilla/websocket"
@@ -18,15 +19,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() {
@@ -44,141 +64,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 common.StringContains(init.Topic, "orders") &&
common.StringContains(init.Topic, "update"):
var response WsAuthenticatedOrdersUpdateResponse
err := common.JSONDecode(resp.Raw, &response)
if err != nil {
h.Websocket.DataHandler <- err
}
h.Websocket.DataHandler <- response
case common.StringContains(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 common.StringContains(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 := common.SplitStrings(depth.Channel, ".")
h.WsProcessOrderbook(&depth, data[1])
case common.StringContains(init.Channel, "kline"):
var kline WsKline
err := common.JSONDecode(resp.Raw, &kline)
if err != nil {
h.Websocket.DataHandler <- err
continue
}
data := common.SplitStrings(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 common.StringContains(init.Channel, "trade"):
var trade WsTrade
err := common.JSONDecode(resp.Raw, &trade)
if err != nil {
h.Websocket.DataHandler <- err
continue
}
data := common.SplitStrings(trade.Channel, ".")
h.Websocket.DataHandler <- exchange.TradeData{
Exchange: h.GetName(),
AssetType: "SPOT",
CurrencyPair: currency.NewPairFromString(data[1]),
Timestamp: time.Unix(0, trade.Tick.Timestamp),
}
}
switch {
case common.StringContains(init.Channel, "depth"):
var depth WsDepth
err := common.JSONDecode(resp.Raw, &depth)
if err != nil {
h.Websocket.DataHandler <- err
return
}
data := common.SplitStrings(depth.Channel, ".")
h.WsProcessOrderbook(&depth, data[1])
case common.StringContains(init.Channel, "kline"):
var kline WsKline
err := common.JSONDecode(resp.Raw, &kline)
if err != nil {
h.Websocket.DataHandler <- err
return
}
data := common.SplitStrings(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 common.StringContains(init.Channel, "trade"):
var trade WsTrade
err := common.JSONDecode(resp.Raw, &trade)
if err != nil {
h.Websocket.DataHandler <- err
return
}
data := common.SplitStrings(trade.Channel, ".")
h.Websocket.DataHandler <- exchange.TradeData{
Exchange: h.GetName(),
AssetType: "SPOT",
CurrencyPair: currency.NewPairFromString(data[1]),
Timestamp: time.Unix(0, trade.Tick.Timestamp),
}
}
}
@@ -223,8 +366,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.GetEnabledCurrencies()
subscriptions := []exchange.WebsocketChannelSubscription{}
for i := range channels {
for j := range enabledCurrencies {
enabledCurrencies[j].Delimiter = ""
@@ -240,6 +389,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 common.StringContains(channelToSubscribe.Channel, "orders.") ||
common.StringContains(channelToSubscribe.Channel, "accounts") {
return h.wsAuthenticatedSubscribe("sub", wsAccountsOrdersEndPoint+channelToSubscribe.Channel, channelToSubscribe.Channel)
}
subscription, err := common.JSONEncode(WsRequest{Subscribe: channelToSubscribe.Channel})
if err != nil {
return err
@@ -249,6 +402,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 common.StringContains(channelToSubscribe.Channel, "orders.") ||
common.StringContains(channelToSubscribe.Channel, "accounts") {
return h.wsAuthenticatedSubscribe("unsub", wsAccountsOrdersEndPoint+channelToSubscribe.Channel, channelToSubscribe.Channel)
}
subscription, err := common.JSONEncode(WsRequest{Unsubscribe: channelToSubscribe.Channel})
if err != nil {
return err
@@ -265,3 +422,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.APIKey,
SignatureMethod: signatureMethod,
SignatureVersion: signatureVersion,
Timestamp: timestamp,
}
hmac := h.wsGenerateSignature(timestamp, wsAccountsOrdersEndPoint)
request.Signature = common.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.APIKey)
values.Set("SignatureMethod", signatureMethod)
values.Set("SignatureVersion", signatureVersion)
values.Set("Timestamp", timestamp)
host := "api.huobi.pro"
payload := fmt.Sprintf("%s\n%s\n%s\n%s",
"GET", host, endpoint, values.Encode())
return common.GetHMAC(common.HashSHA256, []byte(payload), []byte(h.APISecret))
}
func (h *HUOBIHADAX) wsAuthenticatedSubscribe(operation, endpoint, topic string) error {
timestamp := time.Now().UTC().Format(wsDateTimeFormatting)
request := WsAuthenticatedSubscriptionRequest{
Op: operation,
AccessKeyID: h.APIKey,
SignatureMethod: signatureMethod,
SignatureVersion: signatureVersion,
Timestamp: timestamp,
Topic: topic,
}
hmac := h.wsGenerateSignature(timestamp, endpoint)
request.Signature = common.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.APIKey,
SignatureMethod: signatureMethod,
SignatureVersion: signatureVersion,
Timestamp: timestamp,
Topic: wsAccountsList,
Symbol: pair,
}
hmac := h.wsGenerateSignature(timestamp, wsAccountListEndpoint)
request.Signature = common.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.APIKey,
SignatureMethod: signatureMethod,
SignatureVersion: signatureVersion,
Timestamp: timestamp,
Topic: wsOrdersList,
AccountID: accountID,
Symbol: pair.Lower(),
States: "submitted,partial-filled",
}
hmac := h.wsGenerateSignature(timestamp, wsOrdersListEndpoint)
request.Signature = common.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.APIKey,
SignatureMethod: signatureMethod,
SignatureVersion: signatureVersion,
Timestamp: timestamp,
Topic: wsOrdersDetail,
OrderID: orderID,
}
hmac := h.wsGenerateSignature(timestamp, wsOrdersDetailEndpoint)
request.Signature = common.Base64Encode(hmac)
return h.wsAuthenticatedSend(request)
}

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ import (
"github.com/thrasher-/gocryptotrader/config"
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/sharedtestvalues"
)
var k Kraken
@@ -656,7 +657,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 {
@@ -705,7 +706,7 @@ func TestOrderBookOutOfOrder(t *testing.T) {
obupdate1 := `[0,{"a":[["5541.30000","0.00000000","1"]],"b":[["5541.30000","0.00000000","3"]]}]`
obupdate2 := `[0,{"a":[["5541.30000","2.50700000","2"]],"b":[["5541.30000","0.00000000","1"]]}]`
k.Websocket.DataHandler = make(chan interface{}, 10)
k.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
var dataResponse WebsocketDataResponse
err := common.JSONDecode([]byte(obpartial), &dataResponse)
if err != nil {

View File

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

View File

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

View File

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

View File

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

View File

@@ -56,5 +56,6 @@ func (o *OKCoin) SetDefaults() {
exchange.WebsocketKlineSupported |
exchange.WebsocketOrderbookSupported |
exchange.WebsocketSubscribeSupported |
exchange.WebsocketUnsubscribeSupported
exchange.WebsocketUnsubscribeSupported |
exchange.WebsocketAuthenticatedEndpointsSupported
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"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 +70,15 @@ func TestSetup(t *testing.T) {
}
okcoinConfig.AuthenticatedAPISupport = true
okcoinConfig.AuthenticatedWebsocketAPISupport = true
okcoinConfig.APIKey = apiKey
okcoinConfig.APISecret = apiSecret
okcoinConfig.ClientID = passphrase
okcoinConfig.WebsocketURL = o.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 {
@@ -800,13 +803,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.AuthenticatedWebsocketAPISupport || !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 {
@@ -830,16 +832,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)
}
}

View File

@@ -81,7 +81,8 @@ func (o *OKEX) SetDefaults() {
exchange.WebsocketKlineSupported |
exchange.WebsocketOrderbookSupported |
exchange.WebsocketSubscribeSupported |
exchange.WebsocketUnsubscribeSupported
exchange.WebsocketUnsubscribeSupported |
exchange.WebsocketAuthenticatedEndpointsSupported
}
// GetFuturesPostions Get the information of all holding positions in futures trading.

View File

@@ -14,6 +14,7 @@ import (
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"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 +71,15 @@ func TestSetup(t *testing.T) {
websocketEnabled = true
}
okexConfig.AuthenticatedAPISupport = true
okexConfig.AuthenticatedWebsocketAPISupport = true
okexConfig.APIKey = apiKey
okexConfig.APISecret = apiSecret
okexConfig.ClientID = passphrase
okexConfig.WebsocketURL = o.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 {
@@ -1565,13 +1568,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.AuthenticatedWebsocketAPISupport || !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 {
@@ -1595,16 +1597,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)
}
}

View File

@@ -110,6 +110,7 @@ func (o *OKGroup) Setup(exch *config.ExchangeConfig) {
o.Name = exch.Name
o.Enabled = true
o.AuthenticatedAPISupport = exch.AuthenticatedAPISupport
o.AuthenticatedWebsocketAPISupport = exch.AuthenticatedWebsocketAPISupport
o.SetAPIKeys(exch.APIKey, exch.APISecret, exch.ClientID, false)
o.SetHTTPClientTimeout(exch.HTTPTimeout)
o.SetHTTPClientUserAgent(exch.HTTPUserAgent)

View File

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

View File

@@ -197,8 +197,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
@@ -298,10 +304,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
}
}
@@ -310,6 +320,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"
@@ -321,10 +332,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
@@ -367,6 +380,7 @@ func (o *OKGroup) GetAssetTypeFromTableName(table string) string {
// 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:
@@ -680,7 +694,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.GetEnabledCurrencies()
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 = "-"
@@ -699,6 +716,10 @@ func (o *OKGroup) Subscribe(channelToSubscribe exchange.WebsocketChannelSubscrip
Operation: "subscribe",
Arguments: []string{fmt.Sprintf("%v:%v", channelToSubscribe.Channel, channelToSubscribe.Currency.String())},
}
if strings.EqualFold(channelToSubscribe.Channel, okGroupWsSpotAccount) {
resp.Arguments = []string{fmt.Sprintf("%v:%v", channelToSubscribe.Channel, channelToSubscribe.Currency.Base.String())}
}
json, err := common.JSONEncode(resp)
if err != nil {
if o.Verbose {

View File

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

View File

@@ -93,7 +93,8 @@ func (p *Poloniex) SetDefaults() {
exchange.WebsocketOrderbookSupported |
exchange.WebsocketTickerSupported |
exchange.WebsocketSubscribeSupported |
exchange.WebsocketUnsubscribeSupported
exchange.WebsocketUnsubscribeSupported |
exchange.WebsocketAuthenticatedEndpointsSupported
}
// Setup sets user exchange configuration settings
@@ -103,6 +104,7 @@ func (p *Poloniex) Setup(exch *config.ExchangeConfig) {
} else {
p.Enabled = true
p.AuthenticatedAPISupport = exch.AuthenticatedAPISupport
p.AuthenticatedWebsocketAPISupport = exch.AuthenticatedWebsocketAPISupport
p.SetAPIKeys(exch.APIKey, exch.APISecret, "", false)
p.SetHTTPClientTimeout(exch.HTTPTimeout)
p.SetHTTPClientUserAgent(exch.HTTPUserAgent)

View File

@@ -1,18 +1,21 @@
package poloniex
import (
"net/http"
"testing"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/config"
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/sharedtestvalues"
)
var p Poloniex
// Please supply your own APIKEYS here for due diligence testing
const (
apiKey = ""
apiSecret = ""
@@ -29,6 +32,7 @@ func TestSetup(t *testing.T) {
if err != nil {
t.Error("Test Failed - Poloniex Setup() init error")
}
poloniexConfig.AuthenticatedWebsocketAPISupport = true
poloniexConfig.AuthenticatedAPISupport = true
poloniexConfig.APIKey = apiKey
poloniexConfig.APISecret = apiSecret
@@ -414,3 +418,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.AuthenticatedWebsocketAPISupport || !areTestAPIKeysSet() {
t.Skip(exchange.WebsocketNotEnabled)
}
var err error
var dialer websocket.Dialer
p.WebsocketConn, _, err = dialer.Dial(p.Websocket.GetWebsocketURL(),
http.Header{})
if err != nil {
t.Fatal(err)
}
p.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
p.Websocket.TrafficAlert = sharedtestvalues.GetWebsocketStructChannelOverride()
go p.WsHandleData()
defer p.WebsocketConn.Close()
err = p.wsSendAuthorisedCommand("subscribe")
if err != nil {
t.Error(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case response := <-p.Websocket.DataHandler:
t.Error(response)
case <-timer.C:
}
timer.Stop()
}

View File

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

View File

@@ -130,39 +130,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
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(),
Exchange: p.GetName(),
AssetType: "SPOT",
LowPrice: t.LowestAsk,
HighPrice: t.HighestBid,
}
p.wsHandleTickerData(data)
case ws24HourExchangeVolumeID:
case wsHeartbeat:
default:
@@ -243,6 +220,86 @@ func (p *Poloniex) WsHandleData() {
}
}
func (p *Poloniex) wsHandleTickerData(data []interface{}) {
tickerData := data[2].([]interface{})
var t WsTicker
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(),
Exchange: p.GetName(),
AssetType: "SPOT",
LowPrice: t.LowestAsk,
HighPrice: t.HighestBid,
}
}
// 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 {
@@ -437,12 +494,18 @@ var CurrencyPairID = map[int]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.GetEnabledCurrencies()
for j := range enabledCurrencies {
enabledCurrencies[j].Delimiter = "_"
@@ -459,9 +522,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)
@@ -472,9 +538,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)
@@ -493,3 +562,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 := common.GetHMAC(common.HashSHA512, []byte(nonce), []byte(p.APISecret))
request := WsAuthorisationRequest{
Command: command,
Channel: 1000,
Sign: common.HexEncodeToString(hmac),
Key: p.APIKey,
Payload: nonce,
}
return p.wsSend(request)
}

View File

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

View File

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

View File

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

View File

@@ -81,7 +81,11 @@ func (z *ZB) SetDefaults() {
z.Websocket.Functionality = exchange.WebsocketTickerSupported |
exchange.WebsocketOrderbookSupported |
exchange.WebsocketTradeDataSupported |
exchange.WebsocketSubscribeSupported
exchange.WebsocketSubscribeSupported |
exchange.WebsocketAuthenticatedEndpointsSupported |
exchange.WebsocketAccountDataSupported |
exchange.WebsocketCancelOrderSupported |
exchange.WebsocketSubmitOrderSupported
}
// Setup sets user configuration
@@ -91,6 +95,7 @@ func (z *ZB) Setup(exch *config.ExchangeConfig) {
} else {
z.Enabled = true
z.AuthenticatedAPISupport = exch.AuthenticatedAPISupport
z.AuthenticatedWebsocketAPISupport = exch.AuthenticatedWebsocketAPISupport
z.SetAPIKeys(exch.APIKey, exch.APISecret, "", false)
z.APIAuthPEMKey = exch.APIAuthPEMKey
z.SetHTTPClientTimeout(exch.HTTPTimeout)

View File

@@ -2,12 +2,16 @@ package zb
import (
"fmt"
"net/http"
"testing"
"time"
"github.com/gorilla/websocket"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/config"
"github.com/thrasher-/gocryptotrader/currency"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
"github.com/thrasher-/gocryptotrader/exchanges/sharedtestvalues"
)
// Please supply you own test keys here for due diligence testing.
@@ -18,6 +22,7 @@ const (
)
var z ZB
var wsSetupRan bool
func TestSetDefaults(t *testing.T) {
z.SetDefaults()
@@ -31,12 +36,35 @@ func TestSetup(t *testing.T) {
t.Error("Test Failed - ZB Setup() init error")
}
zbConfig.AuthenticatedAPISupport = true
zbConfig.AuthenticatedWebsocketAPISupport = true
zbConfig.APIKey = apiKey
zbConfig.APISecret = apiSecret
z.Setup(&zbConfig)
}
func setupWsAuth(t *testing.T) {
if wsSetupRan {
return
}
z.SetDefaults()
TestSetup(t)
if !z.Websocket.IsEnabled() && !z.AuthenticatedWebsocketAPISupport || !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()
@@ -466,3 +494,233 @@ func TestGetDepositAddress(t *testing.T) {
}
}
}
// TestZBInvalidJSON ZB sends poorly formed JSON. this tests the JSON fixer
// Then JSON decode it to test if successful
func TestZBInvalidJSON(t *testing.T) {
json := `{"success":true,"code":1000,"channel":"getSubUserList","message":"[{"isOpenApi":false,"memo":"Memo","userName":"hello@imgoodthanksandyou.com@good","userId":1337,"isFreez":false}]","no":"0"}`
fixedJSON := z.wsFixInvalidJSON([]byte(json))
var response WsGetSubUserListResponse
err := common.JSONDecode(fixedJSON, &response)
if err != nil {
t.Log(err)
}
if response.Message[0].UserID != 1337 {
t.Error("Expected extracted JSON USERID to equal 1337")
}
json = `{"success":true,"code":1000,"channel":"createSubUserKey","message":"{"apiKey":"thisisnotareallykeyyousillybilly","apiSecret":"lol"}","no":"14728151154382111746154"}`
fixedJSON = z.wsFixInvalidJSON([]byte(json))
var response2 WsRequestResponse
err = common.JSONDecode(fixedJSON, &response2)
if err != nil {
t.Log(err)
}
}
// TestWsTransferFunds ws test
func TestWsTransferFunds(t *testing.T) {
setupWsAuth(t)
err := z.wsDoTransferFunds(currency.BTC,
0.0001,
"username1",
"username2",
)
if err != nil {
t.Error(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case resp := <-z.Websocket.DataHandler:
if resp.(WsRequestResponse).Code == 1002 || resp.(WsRequestResponse).Code == 1003 {
t.Error("Hash not calculated correctly")
}
case <-timer.C:
t.Error("Have not received a response")
}
timer.Stop()
}
// TestWsCreateSuUserKey ws test
func TestWsCreateSuUserKey(t *testing.T) {
setupWsAuth(t)
z.wsGetSubUserList()
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
var userID int64
select {
case resp := <-z.Websocket.DataHandler:
if len(resp.(WsGetSubUserListResponse).Message) == 0 {
t.Fatal("Expected a userID. Ensure you have made a subuserID before running this test")
}
userID = resp.(WsGetSubUserListResponse).Message[0].UserID
case <-timer.C:
t.Fatal("Have not received a response")
}
timer.Stop()
err := z.wsCreateSubUserKey(true, true, true, true, "subu", fmt.Sprintf("%v", userID))
if err != nil {
t.Error(err)
}
timer = time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case resp := <-z.Websocket.DataHandler:
if resp.(WsRequestResponse).Code == 1002 || resp.(WsRequestResponse).Code == 1003 {
t.Error("Hash not calculated correctly")
}
case <-timer.C:
t.Error("Have not received a response")
}
timer.Stop()
}
// TestGetSubUserList ws test
func TestGetSubUserList(t *testing.T) {
setupWsAuth(t)
err := z.wsGetSubUserList()
if err != nil {
t.Fatal(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case resp := <-z.Websocket.DataHandler:
if resp.(WsGetSubUserListResponse).Code == 1002 || resp.(WsGetSubUserListResponse).Code == 1003 {
t.Error("Hash not calculated correctly")
}
case <-timer.C:
t.Error("Have not received a response")
}
timer.Stop()
}
// TestAddSubUser ws test
func TestAddSubUser(t *testing.T) {
setupWsAuth(t)
err := z.wsAddSubUser("abcde", "123456789101112aA!")
if err != nil {
t.Fatal(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case resp := <-z.Websocket.DataHandler:
if resp.(WsRequestResponse).Code == 1002 || resp.(WsRequestResponse).Code == 1003 {
t.Error("Hash not calculated correctly")
}
case <-timer.C:
t.Error("Have not received a response")
}
timer.Stop()
}
// TestWsSubmitOrder ws test
func TestWsSubmitOrder(t *testing.T) {
setupWsAuth(t)
err := z.wsSubmitOrder(currency.NewPairWithDelimiter(currency.LTC.String(), currency.BTC.String(), "").Lower(), 1, 1, 1)
if err != nil {
t.Fatal(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case resp := <-z.Websocket.DataHandler:
if resp.(WsSubmitOrderResponse).Code == 1002 || resp.(WsSubmitOrderResponse).Code == 1003 {
t.Error("Hash not calculated correctly")
}
case <-timer.C:
t.Error("Have not received a response")
}
timer.Stop()
}
// TestWsCancelOrder ws test
func TestWsCancelOrder(t *testing.T) {
setupWsAuth(t)
err := z.wsCancelOrder(currency.NewPairWithDelimiter(currency.LTC.String(), currency.BTC.String(), "").Lower(), 1234)
if err != nil {
t.Fatal(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case resp := <-z.Websocket.DataHandler:
if resp.(WsCancelOrderResponse).Code == 1002 || resp.(WsCancelOrderResponse).Code == 1003 {
t.Error("Hash not calculated correctly")
}
case <-timer.C:
t.Error("Have not received a response")
}
timer.Stop()
}
// TestWsGetAccountInfo ws test
func TestWsGetAccountInfo(t *testing.T) {
setupWsAuth(t)
err := z.wsGetAccountInfoRequest()
if err != nil {
t.Fatal(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case resp := <-z.Websocket.DataHandler:
if resp.(WsGetAccountInfoResponse).Code == 1002 || resp.(WsGetAccountInfoResponse).Code == 1003 {
t.Error("Hash not calculated correctly")
}
case <-timer.C:
t.Error("Have not received a response")
}
timer.Stop()
}
// TestWsGetOrder ws test
func TestWsGetOrder(t *testing.T) {
setupWsAuth(t)
err := z.wsGetOrder(currency.NewPairWithDelimiter(currency.LTC.String(), currency.BTC.String(), "").Lower(), 1234)
if err != nil {
t.Fatal(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case resp := <-z.Websocket.DataHandler:
if resp.(WsGetOrderResponse).Code == 1002 || resp.(WsGetOrderResponse).Code == 1003 {
t.Error("Hash not calculated correctly")
}
case <-timer.C:
t.Error("Have not received a response")
}
timer.Stop()
}
// TestWsGetOrders ws test
func TestWsGetOrders(t *testing.T) {
setupWsAuth(t)
err := z.wsGetOrders(currency.NewPairWithDelimiter(currency.LTC.String(), currency.BTC.String(), "").Lower(), 1, 1)
if err != nil {
t.Fatal(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case resp := <-z.Websocket.DataHandler:
if resp.(WsGetOrdersResponse).Code == 1002 || resp.(WsGetOrdersResponse).Code == 1003 {
t.Error("Hash not calculated correctly")
}
case <-timer.C:
t.Error("Have not received a response")
}
timer.Stop()
}
// TestWsGetOrdersIgnoreTradeType ws test
func TestWsGetOrdersIgnoreTradeType(t *testing.T) {
setupWsAuth(t)
err := z.wsGetOrdersIgnoreTradeType(currency.NewPairWithDelimiter(currency.LTC.String(), currency.BTC.String(), "").Lower(), 1, 1)
if err != nil {
t.Fatal(err)
}
timer := time.NewTimer(sharedtestvalues.WebsocketResponseDefaultTimeout)
select {
case resp := <-z.Websocket.DataHandler:
if resp.(WsGetOrdersResponse).Code == 1002 || resp.(WsGetOrdersResponse).Code == 1003 {
t.Error("Hash not calculated correctly")
}
case <-timer.C:
t.Error("Have not received a response")
}
timer.Stop()
}

View File

@@ -5,6 +5,8 @@ import (
"fmt"
"net/http"
"net/url"
"regexp"
"strings"
"time"
"github.com/gorilla/websocket"
@@ -16,7 +18,8 @@ import (
)
const (
zbWebsocketAPI = "wss://api.zb.cn:9999/websocket"
zbWebsocketAPI = "wss://api.zb.cn:9999/websocket"
zWebsocketAddChannel = "addChannel"
)
// WsConnect initiates a websocket connection
@@ -80,9 +83,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
@@ -106,7 +109,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
@@ -124,7 +127,7 @@ func (z *ZB) WsHandleData() {
case common.StringContains(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
@@ -173,13 +176,16 @@ func (z *ZB) WsHandleData() {
case common.StringContains(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 := common.SplitStrings(result.Channel, "_")
@@ -195,7 +201,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 common.StringContains(result.Channel, "_order"):
var response WsSubmitOrderResponse
err := common.JSONDecode(fixedJSON, &response)
if err != nil {
z.Websocket.DataHandler <- err
continue
}
z.Websocket.DataHandler <- response
case common.StringContains(result.Channel, "_cancelorder"):
var response WsCancelOrderResponse
err := common.JSONDecode(fixedJSON, &response)
if err != nil {
z.Websocket.DataHandler <- err
continue
}
z.Websocket.DataHandler <- response
case common.StringContains(result.Channel, "_getorders"):
var response WsGetOrdersResponse
err := common.JSONDecode(fixedJSON, &response)
if err != nil {
z.Websocket.DataHandler <- err
continue
}
z.Websocket.DataHandler <- response
case common.StringContains(result.Channel, "_getorder"):
var response WsGetOrderResponse
err := common.JSONDecode(fixedJSON, &response)
if err != nil {
z.Websocket.DataHandler <- err
continue
}
z.Websocket.DataHandler <- response
case common.StringContains(result.Channel, "_getordersignoretradetype"):
var response WsGetOrdersIgnoreTradeTypeResponse
err := common.JSONDecode(fixedJSON, &response)
if err != nil {
z.Websocket.DataHandler <- err
continue
}
z.Websocket.DataHandler <- response
case strings.EqualFold(result.Channel, "getAccountInfo"):
var response WsGetAccountInfoResponse
err := common.JSONDecode(fixedJSON, &response)
if err != nil {
z.Websocket.DataHandler <- err
continue
}
z.Websocket.DataHandler <- response
default:
z.Websocket.DataHandler <- errors.New("zb_websocket.go error - unhandled websocket response")
continue
@@ -240,7 +325,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",
@@ -262,7 +347,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)
@@ -277,7 +362,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.APIKey
request.Sign = z.wsGenerateSignature(request)
return z.wsSend(request)
}
func (z *ZB) wsGetSubUserList() error {
if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name)
}
request := WsAuthenticatedRequest{}
request.Channel = "getSubUserList"
request.Event = zWebsocketAddChannel
request.Accesskey = z.APIKey
request.Sign = z.wsGenerateSignature(request)
return z.wsSend(request)
}
func (z *ZB) wsDoTransferFunds(pair currency.Code, amount float64, fromUserName, toUserName string) error {
if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name)
}
request := WsDoTransferFundsRequest{
Amount: amount,
Currency: pair,
FromUserName: fromUserName,
ToUserName: toUserName,
No: fmt.Sprintf("%v", time.Now().Unix()),
}
request.Channel = "doTransferFunds"
request.Event = zWebsocketAddChannel
request.Accesskey = z.APIKey
request.Sign = z.wsGenerateSignature(request)
return z.wsSend(request)
}
func (z *ZB) wsCreateSubUserKey(assetPerm, entrustPerm, leverPerm, moneyPerm bool, keyName, toUserID string) error {
if !z.GetAuthenticatedAPISupport(exchange.WebsocketAuthentication) {
return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", z.Name)
}
request := WsCreateSubUserKeyRequest{
AssetPerm: assetPerm,
EntrustPerm: entrustPerm,
KeyName: keyName,
LeverPerm: leverPerm,
MoneyPerm: moneyPerm,
No: fmt.Sprintf("%v", time.Now().Unix()),
ToUserID: toUserID,
}
request.Channel = "createSubUserKey"
request.Event = zWebsocketAddChannel
request.Accesskey = z.APIKey
request.Sign = z.wsGenerateSignature(request)
return z.wsSend(request)
}
func (z *ZB) wsGenerateSignature(request interface{}) string {
jsonResponse, err := common.JSONEncode(request)
if err != nil {
log.Error(err)
}
hmac := common.GetHMAC(common.HashMD5,
jsonResponse,
[]byte(common.Sha1ToHex(z.APISecret)))
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 := common.ReplaceString(string(matchingResults), "\"", "", 1)
// Remove last quote character
fixedJSON := capturedInvalidZBJSON[:len(capturedInvalidZBJSON)-1]
return []byte(common.ReplaceString(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.APIKey
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.APIKey
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.APIKey
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.APIKey
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.APIKey
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.APIKey,
No: fmt.Sprintf("%v", time.Now().Unix()),
}
request.Sign = z.wsGenerateSignature(request)
return z.wsSend(request)
}

View File

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

View File

@@ -424,3 +424,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
}