Files
gocryptotrader/exchanges/hitbtc/hitbtc_websocket.go
Gareth Kirwan bda9bbec66 websocket: Remove GenerateMessageID (#2008)
* Exchanges: Remove example BespokeGenerateMessageID

* Okx: Replace conn.RequestIDGenerator with MesssageID

Continued overall direction to remove the closed-loop of e => conn => e
roundtrip for message ids

* Exchanges: Add MessageSequence

This method removes the either/or nature of message id generation.
We don't tie the message ids to connections, or to anything.
Consumers just call whichever they want, or even combine them as they
want.
Anything more complicated will need a separate installation anyway

* GateIO: Split usage of MessageID and MessageSequence

* Binance: Switch to UUID message IDs

* Kraken: Switch to e.MessageSequence

* Kucoin: Switch to MessageID

* HitBTC: Switch to UUIDv7 for ws message ID

* Bybit: Switch to UUIDv7 for ws message ID

* Bitfinex: Switch to UUIDv7 and MessageSequence

Tested CID - It accepts 53 bits only for an int, so MessageSequence
makes sense. Can't use MessageID

* Websocket: Remove now unused MessageID function

Moved all MessageID usage into funcs and onto base methods, to remove
the closed loop of message IDs

* Docs: Update guidance for message signatures
2025-10-24 11:14:24 +11:00

856 lines
24 KiB
Go

package hitbtc
import (
"context"
"encoding/hex"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"text/template"
"time"
"github.com/Masterminds/sprig/v3"
gws "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/encoding/json"
"github.com/thrasher-corp/gocryptotrader/exchange/websocket"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"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 (
hitbtcWebsocketAddress = "wss://api.hitbtc.com/api/2/ws"
rpcVersion = "2.0"
errAuthFailed = 1002
)
var subscriptionNames = map[string]string{
subscription.TickerChannel: "Ticker",
subscription.OrderbookChannel: "Orderbook",
subscription.CandlesChannel: "Candles",
subscription.AllTradesChannel: "Trades",
subscription.MyAccountChannel: "Reports",
}
var defaultSubscriptions = subscription.List{
{Enabled: true, Asset: asset.Spot, Channel: subscription.TickerChannel},
{Enabled: true, Asset: asset.Spot, Channel: subscription.OrderbookChannel},
{Enabled: true, Asset: asset.Spot, Channel: subscription.CandlesChannel, Interval: kline.ThirtyMin, Levels: 100},
{Enabled: true, Asset: asset.Spot, Channel: subscription.AllTradesChannel, Levels: 100},
{Enabled: true, Asset: asset.Spot, Channel: subscription.MyAccountChannel, Authenticated: true},
}
// WsConnect starts a new connection with the websocket API
func (e *Exchange) WsConnect() error {
ctx := context.TODO()
if !e.Websocket.IsEnabled() || !e.IsEnabled() {
return websocket.ErrWebsocketNotEnabled
}
var dialer gws.Dialer
err := e.Websocket.Conn.Dial(ctx, &dialer, http.Header{})
if err != nil {
return err
}
e.Websocket.Wg.Add(1)
go e.wsReadData()
if e.Websocket.CanUseAuthenticatedEndpoints() {
err = e.wsLogin(ctx)
if err != nil {
log.Errorf(log.ExchangeSys, "%v - authentication failed: %v\n", e.Name, err)
}
}
return nil
}
// wsReadData receives and passes on websocket messages for processing
func (e *Exchange) wsReadData() {
defer e.Websocket.Wg.Done()
for {
resp := e.Websocket.Conn.ReadMessage()
if resp.Raw == nil {
return
}
err := e.wsHandleData(resp.Raw)
if err != nil {
e.Websocket.DataHandler <- err
}
}
}
func (e *Exchange) wsGetTableName(respRaw []byte) (string, error) {
var init capture
err := json.Unmarshal(respRaw, &init)
if err != nil {
return "", err
}
if init.Error.Code == errAuthFailed {
e.Websocket.SetCanUseAuthenticatedEndpoints(false)
}
if init.ID != "" {
if e.Websocket.Match.IncomingWithData(init.ID, respRaw) {
return "", nil
}
}
if init.Error.Message != "" || init.Error.Code != 0 {
return "", fmt.Errorf("code: %d, Message: %s",
init.Error.Code,
init.Error.Message)
}
if _, ok := init.Result.(bool); ok {
return "", nil
}
if init.Method != "" {
return init.Method, nil
}
switch resultType := init.Result.(type) {
case map[string]any:
if reportType, ok := resultType["reportType"].(string); ok {
return reportType, nil
}
// check for ids - means it was a specific request
// and can't go through normal processing
if responseID, ok := resultType["id"].(string); ok {
if responseID != "" {
return "", nil
}
}
case []any:
if len(resultType) == 0 {
e.Websocket.DataHandler <- fmt.Sprintf("No data returned. ID: %v", init.ID)
return "", nil
}
data, ok := resultType[0].(map[string]any)
if !ok {
return "", errors.New("unable to type assert data")
}
if _, ok := data["clientOrderId"]; ok {
return "order", nil
} else if _, ok := data["available"]; ok {
return "trading", nil
}
}
e.Websocket.DataHandler <- websocket.UnhandledMessageWarning{Message: e.Name + websocket.UnhandledMessage + string(respRaw)}
return "", nil
}
func (e *Exchange) wsHandleData(respRaw []byte) error {
name, err := e.wsGetTableName(respRaw)
if err != nil {
return err
}
switch name {
case "":
return nil
case "ticker":
var wsTicker WsTicker
err := json.Unmarshal(respRaw, &wsTicker)
if err != nil {
return err
}
pairs, err := e.GetEnabledPairs(asset.Spot)
if err != nil {
return err
}
format, err := e.GetPairFormat(asset.Spot, true)
if err != nil {
return err
}
p, err := currency.NewPairFromFormattedPairs(wsTicker.Params.Symbol,
pairs,
format)
if err != nil {
return err
}
e.Websocket.DataHandler <- &ticker.Price{
ExchangeName: e.Name,
Open: wsTicker.Params.Open,
Volume: wsTicker.Params.Volume,
QuoteVolume: wsTicker.Params.VolumeQuote,
High: wsTicker.Params.High,
Low: wsTicker.Params.Low,
Bid: wsTicker.Params.Bid,
Ask: wsTicker.Params.Ask,
Last: wsTicker.Params.Last,
LastUpdated: wsTicker.Params.Timestamp,
AssetType: asset.Spot,
Pair: p,
}
case "snapshotOrderbook":
var obSnapshot WsOrderbook
err := json.Unmarshal(respRaw, &obSnapshot)
if err != nil {
return err
}
err = e.WsProcessOrderbookSnapshot(&obSnapshot)
if err != nil {
return err
}
case "updateOrderbook":
var obUpdate WsOrderbook
err := json.Unmarshal(respRaw, &obUpdate)
if err != nil {
return err
}
err = e.WsProcessOrderbookUpdate(&obUpdate)
if err != nil {
return err
}
case "snapshotTrades", "updateTrades":
if !e.IsSaveTradeDataEnabled() {
return nil
}
var tradeSnapshot WsTrade
err := json.Unmarshal(respRaw, &tradeSnapshot)
if err != nil {
return err
}
var trades []trade.Data
p, err := currency.NewPairFromString(tradeSnapshot.Params.Symbol)
if err != nil {
return &order.ClassificationError{
Exchange: e.Name,
Err: err,
}
}
for i := range tradeSnapshot.Params.Data {
side, err := order.StringToOrderSide(tradeSnapshot.Params.Data[i].Side)
if err != nil {
return &order.ClassificationError{
Exchange: e.Name,
Err: err,
}
}
trades = append(trades, trade.Data{
Timestamp: tradeSnapshot.Params.Data[i].Timestamp,
Exchange: e.Name,
CurrencyPair: p,
AssetType: asset.Spot,
Price: tradeSnapshot.Params.Data[i].Price,
Amount: tradeSnapshot.Params.Data[i].Quantity,
Side: side,
TID: strconv.FormatInt(tradeSnapshot.Params.Data[i].ID, 10),
})
}
return trade.AddTradesToBuffer(trades...)
case "activeOrders":
var o wsActiveOrdersResponse
err := json.Unmarshal(respRaw, &o)
if err != nil {
return err
}
for i := range o.Params {
err = e.wsHandleOrderData(&o.Params[i])
if err != nil {
return err
}
}
case "trading":
var trades WsGetTradingBalanceResponse
err := json.Unmarshal(respRaw, &trades)
if err != nil {
return err
}
e.Websocket.DataHandler <- trades
case "report":
var o wsReportResponse
err := json.Unmarshal(respRaw, &o)
if err != nil {
return err
}
err = e.wsHandleOrderData(&o.OrderData)
if err != nil {
return err
}
case "order":
var o wsActiveOrderRequestResponse
err := json.Unmarshal(respRaw, &o)
if err != nil {
return err
}
for i := range o.OrderData {
err = e.wsHandleOrderData(&o.OrderData[i])
if err != nil {
return err
}
}
case "replaced", "canceled", "new":
var o wsOrderResponse
err := json.Unmarshal(respRaw, &o)
if err != nil {
return err
}
err = e.wsHandleOrderData(&o.OrderData)
if err != nil {
return err
}
default:
e.Websocket.DataHandler <- websocket.UnhandledMessageWarning{Message: e.Name + websocket.UnhandledMessage + string(respRaw)}
return nil
}
return nil
}
// WsProcessOrderbookSnapshot processes a full orderbook snapshot to a local cache
func (e *Exchange) WsProcessOrderbookSnapshot(ob *WsOrderbook) error {
if len(ob.Params.Bid) == 0 || len(ob.Params.Ask) == 0 {
return errors.New("no orderbooks to process")
}
newOrderBook := orderbook.Book{
Bids: make(orderbook.Levels, len(ob.Params.Bid)),
Asks: make(orderbook.Levels, len(ob.Params.Ask)),
}
for i := range ob.Params.Bid {
newOrderBook.Bids[i] = orderbook.Level{
Amount: ob.Params.Bid[i].Size,
Price: ob.Params.Bid[i].Price,
}
}
for i := range ob.Params.Ask {
newOrderBook.Asks[i] = orderbook.Level{
Amount: ob.Params.Ask[i].Size,
Price: ob.Params.Ask[i].Price,
}
}
pairs, err := e.GetEnabledPairs(asset.Spot)
if err != nil {
return err
}
format, err := e.GetPairFormat(asset.Spot, true)
if err != nil {
return err
}
p, err := currency.NewPairFromFormattedPairs(ob.Params.Symbol,
pairs,
format)
if err != nil {
e.Websocket.DataHandler <- err
return err
}
newOrderBook.Asset = asset.Spot
newOrderBook.Pair = p
newOrderBook.Exchange = e.Name
newOrderBook.ValidateOrderbook = e.ValidateOrderbook
newOrderBook.LastUpdated = ob.Params.Timestamp
return e.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
}
func (e *Exchange) wsHandleOrderData(o *wsOrderData) error {
var trades []order.TradeHistory
if o.TradeID > 0 {
trades = append(trades, order.TradeHistory{
Price: o.TradePrice,
Amount: o.TradeQuantity,
Fee: o.TradeFee,
Exchange: e.Name,
TID: strconv.FormatFloat(o.TradeID, 'f', -1, 64),
Timestamp: o.UpdatedAt,
})
}
oType, err := order.StringToOrderType(o.Type)
if err != nil {
e.Websocket.DataHandler <- order.ClassificationError{
Exchange: e.Name,
OrderID: o.ID,
Err: err,
}
}
o.Status = strings.Replace(o.Status, "canceled", "cancelled", 1)
oStatus, err := order.StringToOrderStatus(o.Status)
if err != nil {
e.Websocket.DataHandler <- order.ClassificationError{
Exchange: e.Name,
OrderID: o.ID,
Err: err,
}
}
oSide, err := order.StringToOrderSide(o.Side)
if err != nil {
e.Websocket.DataHandler <- order.ClassificationError{
Exchange: e.Name,
OrderID: o.ID,
Err: err,
}
}
p, err := currency.NewPairFromString(o.Symbol)
if err != nil {
e.Websocket.DataHandler <- order.ClassificationError{
Exchange: e.Name,
OrderID: o.ID,
Err: err,
}
}
var a asset.Item
a, err = e.GetPairAssetType(p)
if err != nil {
return err
}
e.Websocket.DataHandler <- &order.Detail{
Price: o.Price,
Amount: o.Quantity,
ExecutedAmount: o.CumQuantity,
RemainingAmount: o.Quantity - o.CumQuantity,
Exchange: e.Name,
OrderID: o.ID,
Type: oType,
Side: oSide,
Status: oStatus,
AssetType: a,
Date: o.CreatedAt,
LastUpdated: o.UpdatedAt,
Pair: p,
Trades: trades,
}
return nil
}
// WsProcessOrderbookUpdate updates a local cache
func (e *Exchange) WsProcessOrderbookUpdate(update *WsOrderbook) error {
if len(update.Params.Bid) == 0 && len(update.Params.Ask) == 0 {
// Periodically HitBTC sends empty updates which includes a sequence
// can return this as nil.
return nil
}
bids := make(orderbook.Levels, len(update.Params.Bid))
for i := range update.Params.Bid {
bids[i] = orderbook.Level{
Price: update.Params.Bid[i].Price,
Amount: update.Params.Bid[i].Size,
}
}
asks := make(orderbook.Levels, len(update.Params.Ask))
for i := range update.Params.Ask {
asks[i] = orderbook.Level{
Price: update.Params.Ask[i].Price,
Amount: update.Params.Ask[i].Size,
}
}
pairs, err := e.GetEnabledPairs(asset.Spot)
if err != nil {
return err
}
format, err := e.GetPairFormat(asset.Spot, true)
if err != nil {
return err
}
p, err := currency.NewPairFromFormattedPairs(update.Params.Symbol,
pairs,
format)
if err != nil {
return err
}
return e.Websocket.Orderbook.Update(&orderbook.Update{
Asks: asks,
Bids: bids,
Pair: p,
UpdateID: update.Params.Sequence,
Asset: asset.Spot,
UpdateTime: update.Params.Timestamp,
})
}
// generateSubscriptions returns a list of subscriptions from the configured subscriptions feature
func (e *Exchange) generateSubscriptions() (subscription.List, error) {
return e.Features.Subscriptions.ExpandTemplates(e)
}
// GetSubscriptionTemplate returns a subscription channel template
func (e *Exchange) GetSubscriptionTemplate(_ *subscription.Subscription) (*template.Template, error) {
return template.New("master.tmpl").Funcs(sprig.FuncMap()).Funcs(template.FuncMap{
"subToReq": subToReq,
"isSymbolChannel": isSymbolChannel,
}).Parse(subTplText)
}
const (
subscribeOp = "subscribe"
unsubscribeOp = "unsubscribe"
)
// Subscribe sends a websocket message to receive data from the channel
func (e *Exchange) Subscribe(subs subscription.List) error {
ctx := context.TODO()
return e.ParallelChanOp(ctx, subs, func(ctx context.Context, subs subscription.List) error { return e.manageSubs(ctx, subscribeOp, subs) }, 1)
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (e *Exchange) Unsubscribe(subs subscription.List) error {
ctx := context.TODO()
return e.ParallelChanOp(ctx, subs, func(ctx context.Context, subs subscription.List) error { return e.manageSubs(ctx, unsubscribeOp, subs) }, 1)
}
func (e *Exchange) manageSubs(ctx context.Context, op string, subs subscription.List) error {
var errs error
subs, errs = subs.ExpandTemplates(e)
for _, s := range subs {
r := WsRequest{
JSONRPCVersion: rpcVersion,
ID: e.MessageID(),
}
if err := json.Unmarshal([]byte(s.QualifiedChannel), &r); err != nil {
errs = common.AppendError(errs, err)
continue
}
r.Method = op + r.Method
err := e.Websocket.Conn.SendJSONMessage(ctx, request.Unset, r) // v2 api does not return an ID with errors, so we don't use ReturnResponse
if err == nil {
if op == subscribeOp {
err = e.Websocket.AddSuccessfulSubscriptions(e.Websocket.Conn, s)
} else {
err = e.Websocket.RemoveSubscriptions(e.Websocket.Conn, s)
}
}
if err != nil {
errs = common.AppendError(errs, err)
}
}
return errs
}
// Unsubscribe sends a websocket message to stop receiving data from the channel
func (e *Exchange) wsLogin(ctx context.Context) error {
if !e.IsWebsocketAuthenticationSupported() {
return fmt.Errorf("%v AuthenticatedWebsocketAPISupport not enabled", e.Name)
}
creds, err := e.GetCredentials(ctx)
if err != nil {
return err
}
e.Websocket.SetCanUseAuthenticatedEndpoints(true)
n := strconv.FormatInt(time.Now().Unix(), 10)
hmac, err := crypto.GetHMAC(crypto.HashSHA256, []byte(n), []byte(creds.Secret))
if err != nil {
return err
}
req := WsLoginRequest{
Method: "login",
Params: WsLoginData{
Algo: "HS256",
PKey: creds.Key,
Nonce: n,
Signature: hex.EncodeToString(hmac),
},
ID: e.MessageID(),
}
err = e.Websocket.Conn.SendJSONMessage(ctx, request.Unset, req)
if err != nil {
e.Websocket.SetCanUseAuthenticatedEndpoints(false)
return err
}
return nil
}
// wsPlaceOrder sends a websocket message to submit an order
func (e *Exchange) wsPlaceOrder(ctx context.Context, pair currency.Pair, side string, price, quantity float64) (*WsSubmitOrderSuccessResponse, error) {
if !e.Websocket.CanUseAuthenticatedEndpoints() {
return nil, fmt.Errorf("%v not authenticated, cannot place order", e.Name)
}
id := e.MessageID()
fPair, err := e.FormatExchangeCurrency(pair, asset.Spot)
if err != nil {
return nil, err
}
req := WsSubmitOrderRequest{
Method: "newOrder",
Params: WsSubmitOrderRequestData{
ClientOrderID: id,
Symbol: fPair.String(),
Side: strings.ToLower(side),
Price: price,
Quantity: quantity,
},
ID: id,
}
resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, id, req)
if err != nil {
return nil, fmt.Errorf("%v %v", e.Name, err)
}
var response WsSubmitOrderSuccessResponse
err = json.Unmarshal(resp, &response)
if err != nil {
return nil, fmt.Errorf("%v %v", e.Name, err)
}
if response.Error.Code > 0 || response.Error.Message != "" {
return &response, fmt.Errorf("%v Error:%v Message:%v", e.Name, response.Error.Code, response.Error.Message)
}
return &response, nil
}
// wsCancelOrder sends a websocket message to cancel an order
func (e *Exchange) wsCancelOrder(ctx context.Context, clientOrderID string) (*WsCancelOrderResponse, error) {
if !e.Websocket.CanUseAuthenticatedEndpoints() {
return nil, fmt.Errorf("%v not authenticated, cannot place order", e.Name)
}
req := WsCancelOrderRequest{
Method: "cancelOrder",
Params: WsCancelOrderRequestData{
ClientOrderID: clientOrderID,
},
ID: e.MessageID(),
}
resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, req.ID, req)
if err != nil {
return nil, fmt.Errorf("%v %v", e.Name, err)
}
var response WsCancelOrderResponse
err = json.Unmarshal(resp, &response)
if err != nil {
return nil, fmt.Errorf("%v %v", e.Name, err)
}
if response.Error.Code > 0 || response.Error.Message != "" {
return &response, fmt.Errorf("%v Error:%v Message:%v", e.Name, response.Error.Code, response.Error.Message)
}
return &response, nil
}
// wsReplaceOrder sends a websocket message to replace an order
func (e *Exchange) wsReplaceOrder(ctx context.Context, clientOrderID string, quantity, price float64) (*WsReplaceOrderResponse, error) {
if !e.Websocket.CanUseAuthenticatedEndpoints() {
return nil, fmt.Errorf("%v not authenticated, cannot place order", e.Name)
}
req := WsReplaceOrderRequest{
Method: "cancelReplaceOrder",
Params: WsReplaceOrderRequestData{
ClientOrderID: clientOrderID,
RequestClientID: strconv.FormatInt(time.Now().Unix(), 10),
Quantity: quantity,
Price: price,
},
ID: e.MessageID(),
}
resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, req.ID, req)
if err != nil {
return nil, fmt.Errorf("%v %v", e.Name, err)
}
var response WsReplaceOrderResponse
err = json.Unmarshal(resp, &response)
if err != nil {
return nil, fmt.Errorf("%v %v", e.Name, err)
}
if response.Error.Code > 0 || response.Error.Message != "" {
return &response, fmt.Errorf("%v Error:%v Message:%v", e.Name, response.Error.Code, response.Error.Message)
}
return &response, nil
}
// wsGetActiveOrders sends a websocket message to get all active orders
func (e *Exchange) wsGetActiveOrders(ctx context.Context) (*wsActiveOrdersResponse, error) {
if !e.Websocket.CanUseAuthenticatedEndpoints() {
return nil, fmt.Errorf("%v not authenticated, cannot get active orders", e.Name)
}
req := WsReplaceOrderRequest{
Method: "getOrders",
Params: WsReplaceOrderRequestData{},
ID: e.MessageID(),
}
resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, req.ID, req)
if err != nil {
return nil, fmt.Errorf("%v %v", e.Name, err)
}
var response wsActiveOrdersResponse
err = json.Unmarshal(resp, &response)
if err != nil {
return nil, fmt.Errorf("%v %v", e.Name, err)
}
if response.Error.Code > 0 || response.Error.Message != "" {
return &response, fmt.Errorf("%v Error:%v Message:%v", e.Name, response.Error.Code, response.Error.Message)
}
return &response, nil
}
// wsGetTradingBalance sends a websocket message to get trading balance
func (e *Exchange) wsGetTradingBalance(ctx context.Context) (*WsGetTradingBalanceResponse, error) {
if !e.Websocket.CanUseAuthenticatedEndpoints() {
return nil, fmt.Errorf("%v not authenticated, cannot place order", e.Name)
}
req := WsReplaceOrderRequest{
Method: "getTradingBalance",
Params: WsReplaceOrderRequestData{},
ID: e.MessageID(),
}
resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, req.ID, req)
if err != nil {
return nil, fmt.Errorf("%v %v", e.Name, err)
}
var response WsGetTradingBalanceResponse
err = json.Unmarshal(resp, &response)
if err != nil {
return nil, fmt.Errorf("%v %v", e.Name, err)
}
if response.Error.Code > 0 || response.Error.Message != "" {
return &response, fmt.Errorf("%v Error:%v Message:%v", e.Name, response.Error.Code, response.Error.Message)
}
return &response, nil
}
// wsGetCurrencies sends a websocket message to get trading balance
func (e *Exchange) wsGetCurrencies(ctx context.Context, currencyItem currency.Code) (*WsGetCurrenciesResponse, error) {
req := WsGetCurrenciesRequest{
Method: "getCurrency",
Params: WsGetCurrenciesRequestParameters{
Currency: currencyItem,
},
ID: e.MessageID(),
}
resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, req.ID, req)
if err != nil {
return nil, fmt.Errorf("%v %v", e.Name, err)
}
var response WsGetCurrenciesResponse
err = json.Unmarshal(resp, &response)
if err != nil {
return nil, fmt.Errorf("%v %v", e.Name, err)
}
if response.Error.Code > 0 || response.Error.Message != "" {
return &response, fmt.Errorf("%v Error:%v Message:%v", e.Name, response.Error.Code, response.Error.Message)
}
return &response, nil
}
// wsGetSymbols sends a websocket message to get trading balance
func (e *Exchange) wsGetSymbols(ctx context.Context, c currency.Pair) (*WsGetSymbolsResponse, error) {
fPair, err := e.FormatExchangeCurrency(c, asset.Spot)
if err != nil {
return nil, err
}
req := WsGetSymbolsRequest{
Method: "getSymbol",
Params: WsGetSymbolsRequestParameters{
Symbol: fPair.String(),
},
ID: e.MessageID(),
}
resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, req.ID, req)
if err != nil {
return nil, fmt.Errorf("%v %v", e.Name, err)
}
var response WsGetSymbolsResponse
err = json.Unmarshal(resp, &response)
if err != nil {
return nil, fmt.Errorf("%v %v", e.Name, err)
}
if response.Error.Code > 0 || response.Error.Message != "" {
return &response, fmt.Errorf("%v Error:%v Message:%v", e.Name, response.Error.Code, response.Error.Message)
}
return &response, nil
}
// wsGetSymbols sends a websocket message to get trading balance
func (e *Exchange) wsGetTrades(ctx context.Context, c currency.Pair, limit int64, sort, by string) (*WsGetTradesResponse, error) {
fPair, err := e.FormatExchangeCurrency(c, asset.Spot)
if err != nil {
return nil, err
}
req := WsGetTradesRequest{
Method: "getTrades",
Params: WsGetTradesRequestParameters{
Symbol: fPair.String(),
Limit: limit,
Sort: sort,
By: by,
},
ID: e.MessageID(),
}
resp, err := e.Websocket.Conn.SendMessageReturnResponse(ctx, request.Unset, req.ID, req)
if err != nil {
return nil, fmt.Errorf("%v %v", e.Name, err)
}
var response WsGetTradesResponse
err = json.Unmarshal(resp, &response)
if err != nil {
return nil, fmt.Errorf("%v %v", e.Name, err)
}
if response.Error.Code > 0 || response.Error.Message != "" {
return &response, fmt.Errorf("%v Error:%v Message:%v", e.Name, response.Error.Code, response.Error.Message)
}
return &response, nil
}
// subToReq returns the subscription as a map to populate WsRequest
func subToReq(s *subscription.Subscription, maybePair ...currency.Pair) *WsRequest {
name, ok := subscriptionNames[s.Channel]
if !ok {
panic(fmt.Errorf("%w: %s", subscription.ErrNotSupported, s.Channel))
}
r := &WsRequest{
Method: name,
}
if len(maybePair) != 0 {
r.Params = &WsParams{
Symbol: maybePair[0].String(),
Limit: s.Levels,
}
if s.Interval != 0 {
var err error
if r.Params.Period, err = formatExchangeKlineInterval(s.Interval); err != nil {
panic(err)
}
}
} else if s.Levels != 0 {
r.Params = &WsParams{
Limit: s.Levels,
}
}
return r
}
// isSymbolChannel returns if the channel expects receive a symbol
func isSymbolChannel(s *subscription.Subscription) bool {
return s.Channel != subscription.MyAccountChannel
}
const subTplText = `
{{- if isSymbolChannel $.S }}
{{ range $asset, $pairs := $.AssetPairs }}
{{- range $p := $pairs -}}
{{- subToReq $.S $p | mustToJson }}
{{ $.PairSeparator }}
{{- end }}
{{ $.AssetSeparator }}
{{- end }}
{{- else }}
{{- subToReq $.S | mustToJson }}
{{- end }}
`