mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 23:16:45 +00:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
package coinut
|
||||
|
||||
import (
|
||||
"github.com/thrasher-/gocryptotrader/currency"
|
||||
exchange "github.com/thrasher-/gocryptotrader/exchanges"
|
||||
)
|
||||
|
||||
// GenericResponse is the generic response you will get from coinut
|
||||
type GenericResponse struct {
|
||||
Nonce int64 `json:"nonce"`
|
||||
@@ -111,8 +116,8 @@ type OrderResponse struct {
|
||||
|
||||
// Commission holds trade commission structure
|
||||
type Commission struct {
|
||||
Currency string `json:"currency"`
|
||||
Amount float64 `json:"amount,string"`
|
||||
Currency currency.Pair `json:"currency"`
|
||||
Amount float64 `json:"amount,string"`
|
||||
}
|
||||
|
||||
// OrderFilledResponse contains order filled response
|
||||
@@ -362,3 +367,248 @@ type WsSupportedCurrency struct {
|
||||
DecimalPlaces int64 `json:"decimal_places"`
|
||||
Quote string `json:"quote"`
|
||||
}
|
||||
|
||||
// WsRequest base request
|
||||
type WsRequest struct {
|
||||
Request string `json:"request"`
|
||||
Nonce int64 `json:"nonce"`
|
||||
}
|
||||
|
||||
// WsTradeHistoryRequest ws request
|
||||
type WsTradeHistoryRequest struct {
|
||||
InstID int64 `json:"inst_id"`
|
||||
Start int64 `json:"start,omitempty"`
|
||||
Limit int64 `json:"limit,omitempty"`
|
||||
WsRequest
|
||||
}
|
||||
|
||||
// WsCancelOrdersRequest ws request
|
||||
type WsCancelOrdersRequest struct {
|
||||
Entries []WsCancelOrdersRequestEntry `json:"entries"`
|
||||
WsRequest
|
||||
}
|
||||
|
||||
// WsCancelOrdersRequestEntry ws request entry
|
||||
type WsCancelOrdersRequestEntry struct {
|
||||
InstID int64 `json:"inst_id"`
|
||||
OrderID int64 `json:"order_id"`
|
||||
}
|
||||
|
||||
// WsCancelOrderParameters ws request parameters
|
||||
type WsCancelOrderParameters struct {
|
||||
Currency currency.Pair
|
||||
OrderID int64
|
||||
}
|
||||
|
||||
// WsCancelOrderRequest ws request
|
||||
type WsCancelOrderRequest struct {
|
||||
InstID int64 `json:"inst_id"`
|
||||
OrderID int64 `json:"order_id"`
|
||||
WsRequest
|
||||
}
|
||||
|
||||
// WsCancelOrderResponse ws response
|
||||
type WsCancelOrderResponse struct {
|
||||
Nonce int64 `json:"nonce"`
|
||||
Reply string `json:"reply"`
|
||||
OrderID int64 `json:"order_id"`
|
||||
ClientOrdID int64 `json:"client_ord_id"`
|
||||
Status []string `json:"status"`
|
||||
}
|
||||
|
||||
// WsCancelOrdersResponse ws response
|
||||
type WsCancelOrdersResponse struct {
|
||||
WsRequest
|
||||
Entries []WsCancelOrdersResponseEntry `json:"entries"`
|
||||
}
|
||||
|
||||
// WsCancelOrdersResponseEntry ws response entry
|
||||
type WsCancelOrdersResponseEntry struct {
|
||||
InstID int64 `json:"inst_id"`
|
||||
OrderID int64 `json:"order_id"`
|
||||
}
|
||||
|
||||
// WsGetOpenOrdersRequest ws request
|
||||
type WsGetOpenOrdersRequest struct {
|
||||
InstID int64 `json:"inst_id"`
|
||||
WsRequest
|
||||
}
|
||||
|
||||
// WsSubmitOrdersRequest ws request
|
||||
type WsSubmitOrdersRequest struct {
|
||||
Orders []WsSubmitOrdersRequestData `json:"orders"`
|
||||
WsRequest
|
||||
}
|
||||
|
||||
// WsSubmitOrdersRequestData ws request data
|
||||
type WsSubmitOrdersRequestData struct {
|
||||
InstID int64 `json:"inst_id"`
|
||||
Price float64 `json:"price,string"`
|
||||
Qty float64 `json:"qty,string"`
|
||||
ClientOrdID int `json:"client_ord_id"`
|
||||
Side string `json:"side"`
|
||||
}
|
||||
|
||||
// WsSubmitOrderRequest ws request
|
||||
type WsSubmitOrderRequest struct {
|
||||
InstID int64 `json:"inst_id"`
|
||||
Price float64 `json:"price,string"`
|
||||
Qty float64 `json:"qty,string"`
|
||||
OrderID int64 `json:"client_ord_id"`
|
||||
Side string `json:"side"`
|
||||
WsRequest
|
||||
}
|
||||
|
||||
// WsSubmitOrderParameters ws request parameters
|
||||
type WsSubmitOrderParameters struct {
|
||||
Currency currency.Pair
|
||||
Side exchange.OrderSide
|
||||
Amount, Price float64
|
||||
OrderID int64
|
||||
}
|
||||
|
||||
// WsUserBalanceResponse ws response
|
||||
type WsUserBalanceResponse struct {
|
||||
Nonce int64 `json:"nonce"`
|
||||
Status []string `json:"status"`
|
||||
Btc float64 `json:"BTC,string"`
|
||||
Ltc float64 `json:"LTC,string"`
|
||||
Etc float64 `json:"ETC,string"`
|
||||
Eth float64 `json:"ETH,string"`
|
||||
FloatingPl float64 `json:"floating_pl,string"`
|
||||
InitialMargin float64 `json:"initial_margin,string"`
|
||||
RealizedPl float64 `json:"realized_pl,string"`
|
||||
MaintenanceMargin float64 `json:"maintenance_margin,string"`
|
||||
Equity float64 `json:"equity,string"`
|
||||
Reply string `json:"reply"`
|
||||
TransID int64 `json:"trans_id"`
|
||||
}
|
||||
|
||||
// WsOrderAcceptedResponse ws response
|
||||
type WsOrderAcceptedResponse struct {
|
||||
Nonce int64 `json:"nonce"`
|
||||
Status []string `json:"status"`
|
||||
OrderID int64 `json:"order_id"`
|
||||
OpenQty float64 `json:"open_qty,string"`
|
||||
InstID int64 `json:"inst_id"`
|
||||
Qty float64 `json:"qty,string"`
|
||||
ClientOrdID int64 `json:"client_ord_id"`
|
||||
OrderPrice float64 `json:"order_price,string"`
|
||||
Reply string `json:"reply"`
|
||||
Side string `json:"side"`
|
||||
TransID int64 `json:"trans_id"`
|
||||
}
|
||||
|
||||
// WsOrderFilledResponse ws response
|
||||
type WsOrderFilledResponse struct {
|
||||
Commission WsOrderFilledCommissionData `json:"commission"`
|
||||
FillPrice float64 `json:"fill_price,string"`
|
||||
FillQty float64 `json:"fill_qty,string"`
|
||||
Nonce int64 `json:"nonce"`
|
||||
Order WsOrderData `json:"order"`
|
||||
Reply string `json:"reply"`
|
||||
Status []string `json:"status"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
TransID int64 `json:"trans_id"`
|
||||
}
|
||||
|
||||
// WsOrderData ws response data
|
||||
type WsOrderData struct {
|
||||
ClientOrdID int64 `json:"client_ord_id"`
|
||||
InstID int64 `json:"inst_id"`
|
||||
OpenQty float64 `json:"open_qty,string"`
|
||||
OrderID int64 `json:"order_id"`
|
||||
Price float64 `json:"price,string"`
|
||||
Qty float64 `json:"qty,string"`
|
||||
Side string `json:"side"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
// WsOrderFilledCommissionData ws response data
|
||||
type WsOrderFilledCommissionData struct {
|
||||
Amount float64 `json:"amount,string"`
|
||||
Currency currency.Pair `json:"currency"`
|
||||
}
|
||||
|
||||
// WsOrderRejectedResponse ws response
|
||||
type WsOrderRejectedResponse struct {
|
||||
Nonce int64 `json:"nonce"`
|
||||
Status []string `json:"status"`
|
||||
OrderID int64 `json:"order_id"`
|
||||
OpenQty float64 `json:"open_qty,string"`
|
||||
Price float64 `json:"price,string"`
|
||||
InstID int64 `json:"inst_id"`
|
||||
Reasons []string `json:"reasons"`
|
||||
ClientOrdID int64 `json:"client_ord_id"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Reply string `json:"reply"`
|
||||
Qty float64 `json:"qty,string"`
|
||||
Side string `json:"side"`
|
||||
TransID int64 `json:"trans_id"`
|
||||
}
|
||||
|
||||
// WsUserOpenOrdersResponse ws response
|
||||
type WsUserOpenOrdersResponse struct {
|
||||
Nonce int64 `json:"nonce"`
|
||||
Reply string `json:"reply"`
|
||||
Status []string `json:"status"`
|
||||
Orders []WsOrderData `json:"orders"`
|
||||
}
|
||||
|
||||
// WsTradeHistoryResponse ws response
|
||||
type WsTradeHistoryResponse struct {
|
||||
Nonce int64 `json:"nonce"`
|
||||
Reply string `json:"reply"`
|
||||
Status []string `json:"status"`
|
||||
TotalNumber int64 `json:"total_number"`
|
||||
Trades []WsOrderData `json:"trades"`
|
||||
}
|
||||
|
||||
// WsTradeHistoryCommissionData ws response data
|
||||
type WsTradeHistoryCommissionData struct {
|
||||
Amount float64 `json:"amount,string"`
|
||||
Currency currency.Pair `json:"currency"`
|
||||
}
|
||||
|
||||
// WsTradeHistoryTradeData ws response data
|
||||
type WsTradeHistoryTradeData struct {
|
||||
Commission WsTradeHistoryCommissionData `json:"commission"`
|
||||
Order WsOrderData `json:"order"`
|
||||
FillPrice float64 `json:"fill_price,string"`
|
||||
FillQty float64 `json:"fill_qty,string"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
TransID int64 `json:"trans_id"`
|
||||
}
|
||||
|
||||
// WsLoginResponse ws response data
|
||||
type WsLoginResponse struct {
|
||||
APIKey string `json:"api_key"`
|
||||
Country string `json:"country"`
|
||||
DepositEnabled bool `json:"deposit_enabled"`
|
||||
Deposited bool `json:"deposited"`
|
||||
Email string `json:"email"`
|
||||
FailedTimes int64 `json:"failed_times"`
|
||||
KycPassed bool `json:"kyc_passed"`
|
||||
Lang string `json:"lang"`
|
||||
Nonce int64 `json:"nonce"`
|
||||
OtpEnabled bool `json:"otp_enabled"`
|
||||
PhoneNumber string `json:"phone_number"`
|
||||
ProductsEnabled []string `json:"products_enabled"`
|
||||
Referred bool `json:"referred"`
|
||||
Reply string `json:"reply"`
|
||||
SessionID string `json:"session_id"`
|
||||
Status []string `json:"status"`
|
||||
Timezone string `json:"timezone"`
|
||||
Traded bool `json:"traded"`
|
||||
UnverifiedEmail string `json:"unverified_email"`
|
||||
Username string `json:"username"`
|
||||
WithdrawEnabled bool `json:"withdraw_enabled"`
|
||||
}
|
||||
|
||||
// WsNewOrderResponse returns if new_order response failes
|
||||
type WsNewOrderResponse struct {
|
||||
Msg string `json:"msg"`
|
||||
Nonce int64 `json:"nonce"`
|
||||
Reply string `json:"reply"`
|
||||
Status []string `json:"status"`
|
||||
}
|
||||
|
||||
@@ -2,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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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()}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = "/"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -56,5 +56,6 @@ func (o *OKCoin) SetDefaults() {
|
||||
exchange.WebsocketKlineSupported |
|
||||
exchange.WebsocketOrderbookSupported |
|
||||
exchange.WebsocketSubscribeSupported |
|
||||
exchange.WebsocketUnsubscribeSupported
|
||||
exchange.WebsocketUnsubscribeSupported |
|
||||
exchange.WebsocketAuthenticatedEndpointsSupported
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
28
exchanges/sharedtestvalues/sharedtestvalues.go
Normal file
28
exchanges/sharedtestvalues/sharedtestvalues.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package sharedtestvalues
|
||||
|
||||
import "time"
|
||||
|
||||
// This package is only to be referenced in test files
|
||||
const (
|
||||
// WebsocketResponseDefaultTimeout used in websocket testing
|
||||
// Defines wait time for receiving websocket response before cancelling
|
||||
WebsocketResponseDefaultTimeout = (3 * time.Second)
|
||||
// WebsocketResponseExtendedTimeout used in websocket testing
|
||||
// Defines wait time for receiving websocket response before cancelling
|
||||
WebsocketResponseExtendedTimeout = (15 * time.Second)
|
||||
// WebsocketChannelOverrideCapacity used in websocket testing
|
||||
// Defines channel capacity as defaults size can block tests
|
||||
WebsocketChannelOverrideCapacity = 5
|
||||
)
|
||||
|
||||
// GetWebsocketInterfaceChannelOverride returns a new interface based channel
|
||||
// with the capacity set to WebsocketChannelOverrideCapacity
|
||||
func GetWebsocketInterfaceChannelOverride() chan interface{} {
|
||||
return make(chan interface{}, WebsocketChannelOverrideCapacity)
|
||||
}
|
||||
|
||||
// GetWebsocketStructChannelOverride returns a new struct based channel
|
||||
// with the capacity set to WebsocketChannelOverrideCapacity
|
||||
func GetWebsocketStructChannelOverride() chan struct{} {
|
||||
return make(chan struct{}, WebsocketChannelOverrideCapacity)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user