Gateio expand wrappers, websocket and bug fix (#291)

* Added fix for balances as GateIO returns object or array depending on if you have any balances

* Adds GetOrderInfo function for REST API

* Correctly assign WebsocketURL

* Adds websocket auth support to GateIO

* reverts Host changes

* unexported getbalance and signin

* Add WsGetOrderInfo to retreive information on a set order

* unexport wsGetOrderInfo

* renamed freeze to locked to match rest interface

* Removed old logging

* Added detailed error messages and testing disabling auth api support if WS auth fails

* Removed old code for nonce

* reworked GetOrder to return an error if no order is found

* gofmt tests

* removed unneeded event from websocketresponse

* fixed casing on websocket

* Fixed condition
This commit is contained in:
Andrew
2019-05-13 13:21:39 +10:00
committed by Adrian Gallagher
parent 8279a036c2
commit 07216a4422
5 changed files with 254 additions and 49 deletions

View File

@@ -97,6 +97,7 @@ func (g *Gateio) Setup(exch *config.ExchangeConfig) {
g.BaseCurrencies = exch.BaseCurrencies
g.AvailablePairs = exch.AvailablePairs
g.EnabledPairs = exch.EnabledPairs
g.WebsocketURL = gateioWebsocketEndpoint
err := g.SetCurrencyPairFormat()
if err != nil {
log.Fatal(err)
@@ -459,6 +460,10 @@ func (g *Gateio) GetTradeHistory(symbol string) (TradHistoryResponse, error) {
return result, nil
}
func (g *Gateio) GenerateSignature(message string) []byte {
return common.GetHMAC(common.HashSHA512, []byte(message), []byte(g.APISecret))
}
// SendAuthenticatedHTTPRequest sends authenticated requests to the Gateio API
// To use this you must setup an APIKey and APISecret from the exchange
func (g *Gateio) SendAuthenticatedHTTPRequest(method, endpoint, param string, result interface{}) error {
@@ -471,7 +476,7 @@ func (g *Gateio) SendAuthenticatedHTTPRequest(method, endpoint, param string, re
headers["Content-Type"] = "application/x-www-form-urlencoded"
headers["key"] = g.APIKey
hmac := common.GetHMAC(common.HashSHA512, []byte(param), []byte(g.APISecret))
hmac := g.GenerateSignature(param)
headers["sign"] = common.HexEncodeToString(hmac)
urlPath := fmt.Sprintf("%s/%s/%s", g.APIUrl, gateioAPIVersion, endpoint)

View File

@@ -56,7 +56,7 @@ func TestGetMarketInfo(t *testing.T) {
func TestSpotNewOrder(t *testing.T) {
t.Parallel()
if apiKey == "" || apiSecret == "" {
if !areTestAPIKeysSet() && !canManipulateRealOrders {
t.Skip()
}
@@ -74,7 +74,7 @@ func TestSpotNewOrder(t *testing.T) {
func TestCancelExistingOrder(t *testing.T) {
t.Parallel()
if apiKey == "" || apiSecret == "" {
if !areTestAPIKeysSet() && !canManipulateRealOrders {
t.Skip()
}
@@ -475,3 +475,18 @@ func TestGetDepositAddress(t *testing.T) {
}
}
}
func TestGetOrderInfo(t *testing.T) {
g.SetDefaults()
TestSetup(t)
if !areTestAPIKeysSet() {
t.Skip("no API keys set skipping test")
}
_, err := g.GetOrderInfo("917591554")
if err != nil {
if err.Error() != "no order found with id 917591554" && err.Error() != "failed to get open orders" {
t.Fatalf("GetOrderInfo() returned an error skipping test: %v", err)
}
}
}

View File

@@ -35,6 +35,13 @@ var (
TimeIntervalDay = TimeInterval(60 * 60 * 24)
)
const (
IDGeneric = 0000
IDSignIn = 1010
IDBalance = 2000
IDOrderQuery = 3001
)
// MarketInfoResponse holds the market info data
type MarketInfoResponse struct {
Result string `json:"result"`
@@ -54,9 +61,9 @@ type MarketInfoPairsResponse struct {
// BalancesResponse holds the user balances
type BalancesResponse struct {
Result string `json:"result"`
Available map[string]string `json:"available"`
Locked map[string]string `json:"locked"`
Result string `json:"result"`
Available interface{} `json:"available"`
Locked interface{} `json:"locked"`
}
// KlinesRequestParams represents Klines request data.
@@ -397,15 +404,13 @@ type WebsocketRequest struct {
// WebsocketResponse defines a websocket response from gateio
type WebsocketResponse struct {
Time int64 `json:"time"`
Channel string `json:"channel"`
Event string `json:""`
Error WebsocketError `json:"error"`
Result struct {
Status string `json:"status"`
} `json:"result"`
Method string `json:"method"`
Params []json.RawMessage `json:"params"`
Time int64 `json:"time"`
Channel string `json:"channel"`
Error WebsocketError `json:"error"`
Result json.RawMessage `json:"result"`
ID int64 `json:"id"`
Method string `json:"method"`
Params []json.RawMessage `json:"params"`
}
// WebsocketError defines a websocket error type
@@ -435,3 +440,40 @@ type WebsocketTrade struct {
Amount float64 `json:"amount,string"`
Type string `json:"type"`
}
// WebsocketBalance holds a slice of WebsocketBalanceCurrency
type WebsocketBalance struct {
Currency []WebsocketBalanceCurrency
}
// WebsocketBalanceCurrency contains currency name funds available and frozen
type WebsocketBalanceCurrency struct {
Currency string
Available string `json:"available"`
Locked string `json:"freeze"`
}
// WebSocketOrderQueryResult data returned from a websocket ordre query holds slice of WebSocketOrderQueryRecords
type WebSocketOrderQueryResult struct {
Limit int `json:"limit"`
Offset int `json:"offset"`
Total int `json:"total"`
WebSocketOrderQueryRecords []WebSocketOrderQueryRecords `json:"records"`
}
// WebSocketOrderQueryRecords contains order information from a order.query websocket request
type WebSocketOrderQueryRecords struct {
ID int `json:"id"`
Market string `json:"market"`
User int `json:"user"`
Ctime float64 `json:"ctime"`
Mtime float64 `json:"mtime"`
Price string `json:"price"`
Amount string `json:"amount"`
Left string `json:"left"`
DealFee string `json:"dealFee"`
OrderType int `json:"orderType"`
Type int `json:"type"`
FilledAmount string `json:"filledAmount"`
FilledTotal string `json:"filledTotal"`
}

View File

@@ -1,6 +1,7 @@
package gateio
import (
"encoding/json"
"errors"
"fmt"
"net/http"
@@ -13,6 +14,7 @@ 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 (
@@ -43,11 +45,31 @@ func (g *Gateio) WsConnect() error {
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()
return g.WsSubscribe()
}
func (g *Gateio) wsServerSignIn() error {
nonce := int(time.Now().Unix() * 1000)
sigTemp := g.GenerateSignature(strconv.Itoa(nonce))
signature := common.Base64Encode(sigTemp)
signinWsRequest := WebsocketRequest{
ID: IDSignIn,
Method: "server.sign",
Params: []interface{}{g.APIKey, signature, nonce},
}
return g.WebsocketConn.WriteJSON(signinWsRequest)
}
// WsSubscribe subscribes to the full websocket suite on ZB exchange
func (g *Gateio) WsSubscribe() error {
enabled := g.GetEnabledCurrencies()
@@ -98,6 +120,30 @@ func (g *Gateio) WsSubscribe() error {
}
}
if g.AuthenticatedAPISupport {
balance := WebsocketRequest{
ID: IDBalance,
Method: "balance.subscribe",
Params: []interface{}{},
}
err := g.WebsocketConn.WriteJSON(balance)
if err != nil {
return err
}
for _, c := range enabled {
orderNotification := WebsocketRequest{
ID: IDGeneric,
Method: "order.subscribe",
Params: []interface{}{c.String()},
}
err := g.WebsocketConn.WriteJSON(orderNotification)
if err != nil {
return err
}
}
}
return nil
}
@@ -147,11 +193,57 @@ func (g *Gateio) WsHandleData() {
}
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
continue
}
g.Websocket.DataHandler <- fmt.Errorf("gateio_websocket.go error %s",
result.Error.Message)
continue
}
switch result.ID {
case IDBalance:
var balance WebsocketBalance
var balanceInterface interface{}
err = json.Unmarshal(result.Result, &balanceInterface)
if err != nil {
g.Websocket.DataHandler <- err
}
var p WebsocketBalanceCurrency
switch x := balanceInterface.(type) {
case map[string]interface{}:
for xx := range x {
switch kk := x[xx].(type) {
case map[string]interface{}:
p = WebsocketBalanceCurrency{
Currency: xx,
Available: kk["available"].(string),
Locked: kk["freeze"].(string),
}
balance.Currency = append(balance.Currency, p)
default:
break
}
}
default:
break
}
g.Websocket.DataHandler <- balance
case IDOrderQuery:
var orderQuery WebSocketOrderQueryResult
err = common.JSONDecode(result.Result, &orderQuery)
if err != nil {
g.Websocket.DataHandler <- err
continue
}
g.Websocket.DataHandler <- orderQuery
default:
break
}
switch {
case common.StringContains(result.Method, "ticker"):
var ticker WebsocketTicker
@@ -323,3 +415,25 @@ func (g *Gateio) WsHandleData() {
}
}
}
func (g *Gateio) wsGetBalance() error {
balanceWsRequest := WebsocketRequest{
ID: IDBalance,
Method: "balance.query",
Params: []interface{}{},
}
return g.WebsocketConn.WriteJSON(balanceWsRequest)
}
func (g *Gateio) wsGetOrderInfo(market string, offset, limit int) error {
order := WebsocketRequest{
ID: IDOrderQuery,
Method: "order.query",
Params: []interface{}{
market,
offset,
limit,
},
}
return g.WebsocketConn.WriteJSON(order)
}

View File

@@ -137,46 +137,50 @@ func (g *Gateio) GetAccountInfo() (exchange.AccountInfo, error) {
return info, err
}
if len(balance.Available) == 0 && len(balance.Locked) == 0 {
return info, nil
}
var balances []exchange.AccountCurrencyInfo
for key, amountStr := range balance.Locked {
lockedF, err := strconv.ParseFloat(amountStr, 64)
if err != nil {
return info, err
}
balances = append(balances, exchange.AccountCurrencyInfo{
CurrencyName: currency.NewCode(key),
Hold: lockedF,
})
}
for key, amountStr := range balance.Available {
availAmount, err := strconv.ParseFloat(amountStr, 64)
if err != nil {
return info, err
}
var updated bool
for i := range balances {
if balances[i].CurrencyName == currency.NewCode(key) {
balances[i].TotalValue = balances[i].Hold + availAmount
updated = true
break
switch l := balance.Locked.(type) {
case map[string]interface{}:
for x := range l {
lockedF, err := strconv.ParseFloat(l[x].(string), 64)
if err != nil {
return info, err
}
}
if !updated {
balances = append(balances, exchange.AccountCurrencyInfo{
CurrencyName: currency.NewCode(key),
TotalValue: availAmount,
CurrencyName: currency.NewCode(x),
Hold: lockedF,
})
}
default:
break
}
switch v := balance.Available.(type) {
case map[string]interface{}:
for x := range v {
availAmount, err := strconv.ParseFloat(v[x].(string), 64)
if err != nil {
return info, err
}
var updated bool
for i := range balances {
if balances[i].CurrencyName == currency.NewCode(x) {
balances[i].TotalValue = balances[i].Hold + availAmount
updated = true
break
}
}
if !updated {
balances = append(balances, exchange.AccountCurrencyInfo{
CurrencyName: currency.NewCode(x),
TotalValue: availAmount,
})
}
}
default:
break
}
info.Accounts = append(info.Accounts, exchange.Account{
@@ -280,7 +284,32 @@ func (g *Gateio) CancelAllOrders(_ *exchange.OrderCancellation) (exchange.Cancel
// GetOrderInfo returns information on a current open order
func (g *Gateio) GetOrderInfo(orderID string) (exchange.OrderDetail, error) {
var orderDetail exchange.OrderDetail
return orderDetail, common.ErrNotYetImplemented
orders, err := g.GetOpenOrders("")
if err != nil {
return orderDetail, errors.New("failed to get open orders")
}
for x := range orders.Orders {
if orders.Orders[x].OrderNumber != orderID {
continue
}
orderDetail.Exchange = g.GetName()
orderDetail.ID = orders.Orders[x].OrderNumber
orderDetail.RemainingAmount = orders.Orders[x].InitialAmount - orders.Orders[x].FilledAmount
orderDetail.ExecutedAmount = orders.Orders[x].FilledAmount
orderDetail.Amount = orders.Orders[x].InitialAmount
orderDetail.OrderDate = time.Unix(orders.Orders[x].Timestamp, 0)
orderDetail.Status = orders.Orders[x].Status
orderDetail.Price = orders.Orders[x].Rate
orderDetail.CurrencyPair = currency.NewPairDelimiter(orders.Orders[x].CurrencyPair, g.ConfigCurrencyPairFormat.Delimiter)
if strings.EqualFold(orders.Orders[x].Type, exchange.AskOrderSide.ToString()) {
orderDetail.OrderSide = exchange.AskOrderSide
} else if strings.EqualFold(orders.Orders[x].Type, exchange.BidOrderSide.ToString()) {
orderDetail.OrderSide = exchange.BuyOrderSide
}
return orderDetail, nil
}
return orderDetail, fmt.Errorf("no order found with id %v", orderID)
}
// GetDepositAddress returns a deposit address for a specified currency