diff --git a/exchanges/gateio/gateio.go b/exchanges/gateio/gateio.go index a2c60ea9..cb56159e 100644 --- a/exchanges/gateio/gateio.go +++ b/exchanges/gateio/gateio.go @@ -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) diff --git a/exchanges/gateio/gateio_test.go b/exchanges/gateio/gateio_test.go index 38184e48..efd770f6 100644 --- a/exchanges/gateio/gateio_test.go +++ b/exchanges/gateio/gateio_test.go @@ -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) + } + } +} diff --git a/exchanges/gateio/gateio_types.go b/exchanges/gateio/gateio_types.go index 7d34ff0d..33a59db9 100644 --- a/exchanges/gateio/gateio_types.go +++ b/exchanges/gateio/gateio_types.go @@ -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"` +} diff --git a/exchanges/gateio/gateio_websocket.go b/exchanges/gateio/gateio_websocket.go index f1f0a6cd..c9ddc82d 100644 --- a/exchanges/gateio/gateio_websocket.go +++ b/exchanges/gateio/gateio_websocket.go @@ -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) +} diff --git a/exchanges/gateio/gateio_wrapper.go b/exchanges/gateio/gateio_wrapper.go index 68864f30..ba35dd33 100644 --- a/exchanges/gateio/gateio_wrapper.go +++ b/exchanges/gateio/gateio_wrapper.go @@ -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