mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-06-04 15:10:54 +00:00
* Websocket: Use ErrSubscribedAlready instead of errChannelAlreadySubscribed * Subscriptions: Replace Pair with Pairs Given that some subscriptions have multiple pairs, support that as the standard. * Docs: Update subscriptions in add new exch * RPC: Update Subscription Pairs * Linter: Disable testifylint.Len We deliberately use Equal over Len to avoid spamming the contents of large Slices * Websocket: Add suffix to state consts * Binance: Subscription Pairs support * Bitfinex: Subscription Pairs support * Bithumb: Subscription Pairs support * Bitmex: Subscription Pairs support * Bitstamp: Subscription Pairs support * BTCMarkets: Subscription Pairs support * BTSE: Subscription Pairs support * Coinbase: Subscription Pairs support * Coinut: Subscription Pairs support * GateIO: Subscription Pairs support * Gemini: Subscription Pairs support and improvement * Hitbtc: Subscription Pairs support * Huboi: Subscription Pairs support * Kucoin: Subscription Pairs support * Okcoin: Subscription Pairs support * Poloniex: Subscription Pairs support * Kraken: Add subscription Pairs support Note: This is a naieve implementation because we want to rebase the kraken websocket rewrite on top of this * Bybit: Subscription Pairs support * Okx: Subscription Pairs support * Bitmex: Subsription configuration * Fixes unauthenticated websocket left as CanUseAuth * Fixes auth subs happening privately * CoinbasePro: Subscription Configuration * Consolidate ProductIDs when all subscriptions are for the same list * Websocket: Log actual sent message when Verbose * Subscriptions: Improve clarity of which key is which in Match * Subscriptions: Lint fix for HugeParam * Subscriptions: Add AddPairs and move keys from test * Subscriptions: Simplify subscription keys and add key types * Subscriptions: Add List.GroupPairs Rename sub.AddPairs * Subscription: Fix ExactKey not matching 0 pairs * Subscriptions: Remove unused IdentityKey and HasPairKey * Subscriptions: Fix GetKey test * Subscriptions: Test coverage improvements * Websocket: Change State on Add/Remove * Subscriptions: Improve error context * Subscriptions: Fix Enable: false subs not ignored * Bitfinex: Fix WsAuth test failing on DataHandler DataHandler is eaten by dataMonitor now, so we need to use ToRoutine * Deribit: Subscription Pairs support * Websocket: Accept nil lists for checkSubscriptions If the user passes in a nil (implicitly empty) list, we would not panic. Therefore the burden of correctness about that data lies with them. The list of subscriptions is empty, and that's okay, and possibly convenient * Websocket: Add context to NilPointer errors * Subscriptions: Add context to nil errors * Exchange: Fix error expectations in UnsubToWSChans
813 lines
22 KiB
Go
813 lines
22 KiB
Go
package huobi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
"github.com/thrasher-corp/gocryptotrader/common"
|
|
"github.com/thrasher-corp/gocryptotrader/common/crypto"
|
|
"github.com/thrasher-corp/gocryptotrader/currency"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
|
|
"github.com/thrasher-corp/gocryptotrader/log"
|
|
)
|
|
|
|
const (
|
|
baseWSURL = "wss://api.huobi.pro"
|
|
futuresWSURL = "wss://api.hbdm.com/"
|
|
|
|
wsMarketURL = baseWSURL + "/ws"
|
|
wsMarketKline = "market.%s.kline.1min"
|
|
wsMarketDepth = "market.%s.depth.step0"
|
|
wsMarketTrade = "market.%s.trade.detail"
|
|
wsMarketTicker = "market.%s.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"
|
|
|
|
loginDelay = 50 * time.Millisecond
|
|
rateLimit = 20
|
|
)
|
|
|
|
// Instantiates a communications channel between websocket connections
|
|
var comms = make(chan WsMessage)
|
|
|
|
// WsConnect initiates a new websocket connection
|
|
func (h *HUOBI) WsConnect() error {
|
|
if !h.Websocket.IsEnabled() || !h.IsEnabled() {
|
|
return stream.ErrWebsocketNotEnabled
|
|
}
|
|
var dialer websocket.Dialer
|
|
err := h.wsDial(&dialer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if h.Websocket.CanUseAuthenticatedEndpoints() {
|
|
err = h.wsAuthenticatedDial(&dialer)
|
|
if err != nil {
|
|
log.Errorf(log.ExchangeSys,
|
|
"%v - authenticated dial failed: %v\n",
|
|
h.Name,
|
|
err)
|
|
}
|
|
err = h.wsLogin(context.TODO())
|
|
if err != nil {
|
|
log.Errorf(log.ExchangeSys,
|
|
"%v - authentication failed: %v\n",
|
|
h.Name,
|
|
err)
|
|
h.Websocket.SetCanUseAuthenticatedEndpoints(false)
|
|
}
|
|
}
|
|
|
|
h.Websocket.Wg.Add(1)
|
|
go h.wsReadData()
|
|
return nil
|
|
}
|
|
|
|
func (h *HUOBI) wsDial(dialer *websocket.Dialer) error {
|
|
err := h.Websocket.Conn.Dial(dialer, http.Header{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
h.Websocket.Wg.Add(1)
|
|
go h.wsFunnelConnectionData(h.Websocket.Conn, wsMarketURL)
|
|
return nil
|
|
}
|
|
|
|
func (h *HUOBI) wsAuthenticatedDial(dialer *websocket.Dialer) error {
|
|
if !h.IsWebsocketAuthenticationSupported() {
|
|
return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled",
|
|
h.Name)
|
|
}
|
|
err := h.Websocket.AuthConn.Dial(dialer, http.Header{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
h.Websocket.Wg.Add(1)
|
|
go h.wsFunnelConnectionData(h.Websocket.AuthConn, wsAccountsOrdersURL)
|
|
return nil
|
|
}
|
|
|
|
// wsFunnelConnectionData manages data from multiple endpoints and passes it to
|
|
// a channel
|
|
func (h *HUOBI) wsFunnelConnectionData(ws stream.Connection, url string) {
|
|
defer h.Websocket.Wg.Done()
|
|
for {
|
|
resp := ws.ReadMessage()
|
|
if resp.Raw == nil {
|
|
return
|
|
}
|
|
comms <- WsMessage{Raw: resp.Raw, URL: url}
|
|
}
|
|
}
|
|
|
|
// wsReadData receives and passes on websocket messages for processing
|
|
func (h *HUOBI) wsReadData() {
|
|
defer h.Websocket.Wg.Done()
|
|
for {
|
|
select {
|
|
case <-h.Websocket.ShutdownC:
|
|
select {
|
|
case resp := <-comms:
|
|
err := h.wsHandleData(resp.Raw)
|
|
if err != nil {
|
|
select {
|
|
case h.Websocket.DataHandler <- err:
|
|
default:
|
|
log.Errorf(log.WebsocketMgr,
|
|
"%s websocket handle data error: %v",
|
|
h.Name,
|
|
err)
|
|
}
|
|
}
|
|
default:
|
|
}
|
|
return
|
|
case resp := <-comms:
|
|
err := h.wsHandleData(resp.Raw)
|
|
if err != nil {
|
|
h.Websocket.DataHandler <- err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func stringToOrderStatus(status string) (order.Status, error) {
|
|
switch status {
|
|
case "submitted":
|
|
return order.New, nil
|
|
case "canceled":
|
|
return order.Cancelled, nil
|
|
case "partial-filled":
|
|
return order.PartiallyFilled, nil
|
|
case "partial-canceled":
|
|
return order.PartiallyCancelled, nil
|
|
default:
|
|
return order.UnknownStatus,
|
|
errors.New(status + " not recognised as order status")
|
|
}
|
|
}
|
|
|
|
func stringToOrderSide(side string) (order.Side, error) {
|
|
switch {
|
|
case strings.Contains(side, "buy"):
|
|
return order.Buy, nil
|
|
case strings.Contains(side, "sell"):
|
|
return order.Sell, nil
|
|
}
|
|
|
|
return order.UnknownSide,
|
|
errors.New(side + " not recognised as order side")
|
|
}
|
|
|
|
func stringToOrderType(oType string) (order.Type, error) {
|
|
switch {
|
|
case strings.Contains(oType, "limit"):
|
|
return order.Limit, nil
|
|
case strings.Contains(oType, "market"):
|
|
return order.Market, nil
|
|
}
|
|
|
|
return order.UnknownType,
|
|
errors.New(oType + " not recognised as order type")
|
|
}
|
|
|
|
func (h *HUOBI) wsHandleData(respRaw []byte) error {
|
|
var init WsResponse
|
|
err := json.Unmarshal(respRaw, &init)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if init.Subscribed != "" ||
|
|
init.UnSubscribed != "" ||
|
|
init.Op == "sub" ||
|
|
init.Op == "unsub" {
|
|
// TODO handle subs
|
|
return nil
|
|
}
|
|
if init.Ping != 0 {
|
|
h.sendPingResponse(init.Ping)
|
|
return nil
|
|
}
|
|
|
|
if init.Op == "ping" {
|
|
authPing := authenticationPing{
|
|
OP: "pong",
|
|
TS: init.TS,
|
|
}
|
|
err := h.Websocket.AuthConn.SendJSONMessage(authPing)
|
|
if err != nil {
|
|
log.Errorln(log.ExchangeSys, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if init.ErrorMessage != "" {
|
|
if init.ErrorMessage == "api-signature-not-valid" {
|
|
h.Websocket.SetCanUseAuthenticatedEndpoints(false)
|
|
return errors.New(h.Name +
|
|
" - invalid credentials. Authenticated requests disabled")
|
|
}
|
|
|
|
codes, _ := init.ErrorCode.(string)
|
|
return errors.New(h.Name + " Code:" + codes + " Message:" + init.ErrorMessage)
|
|
}
|
|
|
|
if init.ClientID > 0 {
|
|
if h.Websocket.Match.IncomingWithData(init.ClientID, respRaw) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
switch {
|
|
case strings.EqualFold(init.Op, authOp):
|
|
h.Websocket.SetCanUseAuthenticatedEndpoints(true)
|
|
// Auth captured
|
|
return nil
|
|
case strings.EqualFold(init.Topic, "accounts"):
|
|
var response WsAuthenticatedAccountsResponse
|
|
err := json.Unmarshal(respRaw, &response)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
h.Websocket.DataHandler <- response
|
|
|
|
case strings.Contains(init.Topic, "orders") &&
|
|
strings.Contains(init.Topic, "update"):
|
|
var response WsAuthenticatedOrdersUpdateResponse
|
|
err := json.Unmarshal(respRaw, &response)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
data := strings.Split(response.Topic, ".")
|
|
if len(data) < 2 {
|
|
return errors.New(h.Name +
|
|
" - currency could not be extracted from response")
|
|
}
|
|
orderID := strconv.FormatInt(response.Data.OrderID, 10)
|
|
var oSide order.Side
|
|
oSide, err = stringToOrderSide(response.Data.OrderType)
|
|
if err != nil {
|
|
h.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: h.Name,
|
|
OrderID: orderID,
|
|
Err: err,
|
|
}
|
|
}
|
|
var oType order.Type
|
|
oType, err = stringToOrderType(response.Data.OrderType)
|
|
if err != nil {
|
|
h.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: h.Name,
|
|
OrderID: orderID,
|
|
Err: err,
|
|
}
|
|
}
|
|
var oStatus order.Status
|
|
oStatus, err = stringToOrderStatus(response.Data.OrderState)
|
|
if err != nil {
|
|
h.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: h.Name,
|
|
OrderID: orderID,
|
|
Err: err,
|
|
}
|
|
}
|
|
var p currency.Pair
|
|
var a asset.Item
|
|
p, a, err = h.GetRequestFormattedPairAndAssetType(data[1])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
h.Websocket.DataHandler <- &order.Detail{
|
|
Price: response.Data.Price,
|
|
Amount: response.Data.UnfilledAmount + response.Data.FilledAmount,
|
|
ExecutedAmount: response.Data.FilledAmount,
|
|
RemainingAmount: response.Data.UnfilledAmount,
|
|
Exchange: h.Name,
|
|
OrderID: orderID,
|
|
Type: oType,
|
|
Side: oSide,
|
|
Status: oStatus,
|
|
AssetType: a,
|
|
LastUpdated: time.Unix(response.TS*1000, 0),
|
|
Pair: p,
|
|
}
|
|
|
|
case strings.Contains(init.Topic, "orders"):
|
|
var response WsOldOrderUpdate
|
|
err := json.Unmarshal(respRaw, &response)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
h.Websocket.DataHandler <- response
|
|
case strings.Contains(init.Channel, "depth"):
|
|
var depth WsDepth
|
|
err := json.Unmarshal(respRaw, &depth)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
data := strings.Split(depth.Channel, ".")
|
|
err = h.WsProcessOrderbook(&depth, data[1])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case strings.Contains(init.Channel, "kline"):
|
|
var kline WsKline
|
|
err := json.Unmarshal(respRaw, &kline)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
data := strings.Split(kline.Channel, ".")
|
|
var p currency.Pair
|
|
var a asset.Item
|
|
p, a, err = h.GetRequestFormattedPairAndAssetType(data[1])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
h.Websocket.DataHandler <- stream.KlineData{
|
|
Timestamp: time.UnixMilli(kline.Timestamp),
|
|
Exchange: h.Name,
|
|
AssetType: a,
|
|
Pair: p,
|
|
OpenPrice: kline.Tick.Open,
|
|
ClosePrice: kline.Tick.Close,
|
|
HighPrice: kline.Tick.High,
|
|
LowPrice: kline.Tick.Low,
|
|
Volume: kline.Tick.Volume,
|
|
Interval: data[3],
|
|
}
|
|
case strings.Contains(init.Channel, "trade.detail"):
|
|
if !h.IsSaveTradeDataEnabled() {
|
|
return nil
|
|
}
|
|
var t WsTrade
|
|
err := json.Unmarshal(respRaw, &t)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
data := strings.Split(t.Channel, ".")
|
|
var p currency.Pair
|
|
var a asset.Item
|
|
p, a, err = h.GetRequestFormattedPairAndAssetType(data[1])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var trades []trade.Data
|
|
for i := range t.Tick.Data {
|
|
side := order.Buy
|
|
if t.Tick.Data[i].Direction != "buy" {
|
|
side = order.Sell
|
|
}
|
|
trades = append(trades, trade.Data{
|
|
Exchange: h.Name,
|
|
AssetType: a,
|
|
CurrencyPair: p,
|
|
Timestamp: time.UnixMilli(t.Tick.Data[i].Timestamp),
|
|
Amount: t.Tick.Data[i].Amount,
|
|
Price: t.Tick.Data[i].Price,
|
|
Side: side,
|
|
TID: strconv.FormatFloat(t.Tick.Data[i].TradeID, 'f', -1, 64),
|
|
})
|
|
}
|
|
return trade.AddTradesToBuffer(h.Name, trades...)
|
|
case strings.Contains(init.Channel, "detail"),
|
|
strings.Contains(init.Rep, "detail"):
|
|
var wsTicker WsTick
|
|
err := json.Unmarshal(respRaw, &wsTicker)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var data []string
|
|
if wsTicker.Channel != "" {
|
|
data = strings.Split(wsTicker.Channel, ".")
|
|
}
|
|
if wsTicker.Rep != "" {
|
|
data = strings.Split(wsTicker.Rep, ".")
|
|
}
|
|
|
|
var p currency.Pair
|
|
var a asset.Item
|
|
p, a, err = h.GetRequestFormattedPairAndAssetType(data[1])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
h.Websocket.DataHandler <- &ticker.Price{
|
|
ExchangeName: h.Name,
|
|
Open: wsTicker.Tick.Open,
|
|
Close: wsTicker.Tick.Close,
|
|
Volume: wsTicker.Tick.Amount,
|
|
QuoteVolume: wsTicker.Tick.Volume,
|
|
High: wsTicker.Tick.High,
|
|
Low: wsTicker.Tick.Low,
|
|
LastUpdated: time.UnixMilli(wsTicker.Timestamp),
|
|
AssetType: a,
|
|
Pair: p,
|
|
}
|
|
default:
|
|
h.Websocket.DataHandler <- stream.UnhandledMessageWarning{
|
|
Message: h.Name + stream.UnhandledMessage + string(respRaw),
|
|
}
|
|
return nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (h *HUOBI) sendPingResponse(pong int64) {
|
|
err := h.Websocket.Conn.SendJSONMessage(WsPong{Pong: pong})
|
|
if err != nil {
|
|
log.Errorln(log.ExchangeSys, err)
|
|
}
|
|
}
|
|
|
|
// WsProcessOrderbook processes new orderbook data
|
|
func (h *HUOBI) WsProcessOrderbook(update *WsDepth, symbol string) error {
|
|
pairs, err := h.GetEnabledPairs(asset.Spot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
format, err := h.GetPairFormat(asset.Spot, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
p, err := currency.NewPairFromFormattedPairs(symbol,
|
|
pairs,
|
|
format)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
bids := make(orderbook.Tranches, len(update.Tick.Bids))
|
|
for i := range update.Tick.Bids {
|
|
price, ok := update.Tick.Bids[i][0].(float64)
|
|
if !ok {
|
|
return errors.New("unable to type assert bid price")
|
|
}
|
|
amount, ok := update.Tick.Bids[i][1].(float64)
|
|
if !ok {
|
|
return errors.New("unable to type assert bid amount")
|
|
}
|
|
bids[i] = orderbook.Tranche{
|
|
Price: price,
|
|
Amount: amount,
|
|
}
|
|
}
|
|
|
|
asks := make(orderbook.Tranches, len(update.Tick.Asks))
|
|
for i := range update.Tick.Asks {
|
|
price, ok := update.Tick.Asks[i][0].(float64)
|
|
if !ok {
|
|
return errors.New("unable to type assert ask price")
|
|
}
|
|
amount, ok := update.Tick.Asks[i][1].(float64)
|
|
if !ok {
|
|
return errors.New("unable to type assert ask amount")
|
|
}
|
|
asks[i] = orderbook.Tranche{
|
|
Price: price,
|
|
Amount: amount,
|
|
}
|
|
}
|
|
|
|
var newOrderBook orderbook.Base
|
|
newOrderBook.Asks = asks
|
|
newOrderBook.Bids = bids
|
|
newOrderBook.Pair = p
|
|
newOrderBook.Asset = asset.Spot
|
|
newOrderBook.Exchange = h.Name
|
|
newOrderBook.VerifyOrderbook = h.CanVerifyOrderbook
|
|
newOrderBook.LastUpdated = time.UnixMilli(update.Timestamp)
|
|
|
|
return h.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
|
|
}
|
|
|
|
// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions()
|
|
func (h *HUOBI) GenerateDefaultSubscriptions() (subscription.List, error) {
|
|
var channels = []string{wsMarketKline,
|
|
wsMarketDepth,
|
|
wsMarketTrade,
|
|
wsMarketTicker}
|
|
var subscriptions subscription.List
|
|
if h.Websocket.CanUseAuthenticatedEndpoints() {
|
|
channels = append(channels, "orders.%v", "orders.%v.update")
|
|
subscriptions = append(subscriptions, &subscription.Subscription{
|
|
Channel: "accounts",
|
|
})
|
|
}
|
|
enabledCurrencies, err := h.GetEnabledPairs(asset.Spot)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for i := range channels {
|
|
for j := range enabledCurrencies {
|
|
enabledCurrencies[j].Delimiter = ""
|
|
channel := fmt.Sprintf(channels[i],
|
|
enabledCurrencies[j].Lower().String())
|
|
subscriptions = append(subscriptions, &subscription.Subscription{
|
|
Channel: channel,
|
|
Pairs: currency.Pairs{enabledCurrencies[j]},
|
|
})
|
|
}
|
|
}
|
|
return subscriptions, nil
|
|
}
|
|
|
|
// Subscribe sends a websocket message to receive data from the channel
|
|
func (h *HUOBI) Subscribe(channelsToSubscribe subscription.List) error {
|
|
var creds *account.Credentials
|
|
if h.Websocket.CanUseAuthenticatedEndpoints() {
|
|
var err error
|
|
creds, err = h.GetCredentials(context.TODO())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
var errs error
|
|
for i := range channelsToSubscribe {
|
|
var err error
|
|
if (strings.Contains(channelsToSubscribe[i].Channel, "orders.") ||
|
|
strings.Contains(channelsToSubscribe[i].Channel, "accounts")) && creds != nil {
|
|
err = h.wsAuthenticatedSubscribe(creds,
|
|
"sub",
|
|
wsAccountsOrdersEndPoint+channelsToSubscribe[i].Channel,
|
|
channelsToSubscribe[i].Channel)
|
|
} else {
|
|
err = h.Websocket.Conn.SendJSONMessage(WsRequest{
|
|
Subscribe: channelsToSubscribe[i].Channel,
|
|
})
|
|
}
|
|
if err == nil {
|
|
err = h.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe[i])
|
|
}
|
|
if err != nil {
|
|
errs = common.AppendError(errs, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Unsubscribe sends a websocket message to stop receiving data from the channel
|
|
func (h *HUOBI) Unsubscribe(channelsToUnsubscribe subscription.List) error {
|
|
var creds *account.Credentials
|
|
if h.Websocket.CanUseAuthenticatedEndpoints() {
|
|
var err error
|
|
creds, err = h.GetCredentials(context.TODO())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
var errs error
|
|
for i := range channelsToUnsubscribe {
|
|
var err error
|
|
if (strings.Contains(channelsToUnsubscribe[i].Channel, "orders.") ||
|
|
strings.Contains(channelsToUnsubscribe[i].Channel, "accounts")) && creds != nil {
|
|
err = h.wsAuthenticatedSubscribe(creds,
|
|
"unsub",
|
|
wsAccountsOrdersEndPoint+channelsToUnsubscribe[i].Channel,
|
|
channelsToUnsubscribe[i].Channel)
|
|
} else {
|
|
err = h.Websocket.Conn.SendJSONMessage(WsRequest{
|
|
Unsubscribe: channelsToUnsubscribe[i].Channel,
|
|
})
|
|
}
|
|
if err == nil {
|
|
err = h.Websocket.RemoveSubscriptions(channelsToUnsubscribe[i])
|
|
}
|
|
if err != nil {
|
|
errs = common.AppendError(errs, err)
|
|
}
|
|
}
|
|
return errs
|
|
}
|
|
|
|
func (h *HUOBI) wsGenerateSignature(creds *account.Credentials, timestamp, endpoint string) ([]byte, error) {
|
|
values := url.Values{}
|
|
values.Set("AccessKeyId", creds.Key)
|
|
values.Set("SignatureMethod", signatureMethod)
|
|
values.Set("SignatureVersion", signatureVersion)
|
|
values.Set("Timestamp", timestamp)
|
|
host := "api.huobi.pro"
|
|
payload := fmt.Sprintf("%s\n%s\n%s\n%s",
|
|
http.MethodGet, host, endpoint, values.Encode())
|
|
return crypto.GetHMAC(crypto.HashSHA256, []byte(payload), []byte(creds.Secret))
|
|
}
|
|
|
|
func (h *HUOBI) wsLogin(ctx context.Context) error {
|
|
if !h.IsWebsocketAuthenticationSupported() {
|
|
return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", h.Name)
|
|
}
|
|
creds, err := h.GetCredentials(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
h.Websocket.SetCanUseAuthenticatedEndpoints(true)
|
|
timestamp := time.Now().UTC().Format(wsDateTimeFormatting)
|
|
request := WsAuthenticationRequest{
|
|
Op: authOp,
|
|
AccessKeyID: creds.Key,
|
|
SignatureMethod: signatureMethod,
|
|
SignatureVersion: signatureVersion,
|
|
Timestamp: timestamp,
|
|
}
|
|
hmac, err := h.wsGenerateSignature(creds, timestamp, wsAccountsOrdersEndPoint)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
request.Signature = crypto.Base64Encode(hmac)
|
|
err = h.Websocket.AuthConn.SendJSONMessage(request)
|
|
if err != nil {
|
|
h.Websocket.SetCanUseAuthenticatedEndpoints(false)
|
|
return err
|
|
}
|
|
|
|
time.Sleep(loginDelay)
|
|
return nil
|
|
}
|
|
|
|
func (h *HUOBI) wsAuthenticatedSubscribe(creds *account.Credentials, operation, endpoint, topic string) error {
|
|
timestamp := time.Now().UTC().Format(wsDateTimeFormatting)
|
|
request := WsAuthenticatedSubscriptionRequest{
|
|
Op: operation,
|
|
AccessKeyID: creds.Key,
|
|
SignatureMethod: signatureMethod,
|
|
SignatureVersion: signatureVersion,
|
|
Timestamp: timestamp,
|
|
Topic: topic,
|
|
}
|
|
hmac, err := h.wsGenerateSignature(creds, timestamp, endpoint)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
request.Signature = crypto.Base64Encode(hmac)
|
|
return h.Websocket.AuthConn.SendJSONMessage(request)
|
|
}
|
|
|
|
func (h *HUOBI) wsGetAccountsList(ctx context.Context) (*WsAuthenticatedAccountsListResponse, error) {
|
|
if !h.Websocket.CanUseAuthenticatedEndpoints() {
|
|
return nil, fmt.Errorf("%v not authenticated cannot get accounts list", h.Name)
|
|
}
|
|
creds, err := h.GetCredentials(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
timestamp := time.Now().UTC().Format(wsDateTimeFormatting)
|
|
request := WsAuthenticatedAccountsListRequest{
|
|
Op: requestOp,
|
|
AccessKeyID: creds.Key,
|
|
SignatureMethod: signatureMethod,
|
|
SignatureVersion: signatureVersion,
|
|
Timestamp: timestamp,
|
|
Topic: wsAccountsList,
|
|
}
|
|
hmac, err := h.wsGenerateSignature(creds, timestamp, wsAccountListEndpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
request.Signature = crypto.Base64Encode(hmac)
|
|
request.ClientID = h.Websocket.AuthConn.GenerateMessageID(true)
|
|
resp, err := h.Websocket.AuthConn.SendMessageReturnResponse(request.ClientID, request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var response WsAuthenticatedAccountsListResponse
|
|
err = json.Unmarshal(resp, &response)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
code, _ := response.ErrorCode.(int)
|
|
if code != 0 {
|
|
return nil, errors.New(response.ErrorMessage)
|
|
}
|
|
return &response, nil
|
|
}
|
|
|
|
func (h *HUOBI) wsGetOrdersList(ctx context.Context, accountID int64, pair currency.Pair) (*WsAuthenticatedOrdersResponse, error) {
|
|
if !h.Websocket.CanUseAuthenticatedEndpoints() {
|
|
return nil, fmt.Errorf("%v not authenticated cannot get orders list", h.Name)
|
|
}
|
|
|
|
creds, err := h.GetCredentials(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fPair, err := h.FormatExchangeCurrency(pair, asset.Spot)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
timestamp := time.Now().UTC().Format(wsDateTimeFormatting)
|
|
request := WsAuthenticatedOrdersListRequest{
|
|
Op: requestOp,
|
|
AccessKeyID: creds.Key,
|
|
SignatureMethod: signatureMethod,
|
|
SignatureVersion: signatureVersion,
|
|
Timestamp: timestamp,
|
|
Topic: wsOrdersList,
|
|
AccountID: accountID,
|
|
Symbol: fPair.String(),
|
|
States: "submitted,partial-filled",
|
|
}
|
|
|
|
hmac, err := h.wsGenerateSignature(creds, timestamp, wsOrdersListEndpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
request.Signature = crypto.Base64Encode(hmac)
|
|
request.ClientID = h.Websocket.AuthConn.GenerateMessageID(true)
|
|
|
|
resp, err := h.Websocket.AuthConn.SendMessageReturnResponse(request.ClientID, request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var response WsAuthenticatedOrdersResponse
|
|
err = json.Unmarshal(resp, &response)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
code, _ := response.ErrorCode.(int)
|
|
if code != 0 {
|
|
return nil, errors.New(response.ErrorMessage)
|
|
}
|
|
return &response, nil
|
|
}
|
|
|
|
func (h *HUOBI) wsGetOrderDetails(ctx context.Context, orderID string) (*WsAuthenticatedOrderDetailResponse, error) {
|
|
if !h.Websocket.CanUseAuthenticatedEndpoints() {
|
|
return nil, fmt.Errorf("%v not authenticated cannot get order details", h.Name)
|
|
}
|
|
creds, err := h.GetCredentials(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
timestamp := time.Now().UTC().Format(wsDateTimeFormatting)
|
|
request := WsAuthenticatedOrderDetailsRequest{
|
|
Op: requestOp,
|
|
AccessKeyID: creds.Key,
|
|
SignatureMethod: signatureMethod,
|
|
SignatureVersion: signatureVersion,
|
|
Timestamp: timestamp,
|
|
Topic: wsOrdersDetail,
|
|
OrderID: orderID,
|
|
}
|
|
hmac, err := h.wsGenerateSignature(creds, timestamp, wsOrdersDetailEndpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
request.Signature = crypto.Base64Encode(hmac)
|
|
request.ClientID = h.Websocket.AuthConn.GenerateMessageID(true)
|
|
resp, err := h.Websocket.AuthConn.SendMessageReturnResponse(request.ClientID, request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var response WsAuthenticatedOrderDetailResponse
|
|
err = json.Unmarshal(resp, &response)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
code, _ := response.ErrorCode.(int)
|
|
if code != 0 {
|
|
return nil, errors.New(response.ErrorMessage)
|
|
}
|
|
return &response, nil
|
|
}
|