From 2bb5525cd2b82f0cb4b360a774a47069fa0be42e Mon Sep 17 00:00:00 2001 From: linstohu Date: Thu, 21 Dec 2023 00:30:35 +0800 Subject: [PATCH] feat: htx spot account ws --- binance/utils/websocket_response.go | 6 +- htx/spot/accountws/client.go | 399 +++++++++++++++++++++++++++ htx/spot/accountws/client_test.go | 69 +++++ htx/spot/accountws/events.go | 34 +++ htx/spot/accountws/request.go | 33 +++ htx/spot/accountws/response.go | 59 ++++ htx/spot/accountws/subscriptions.go | 54 ++++ htx/spot/accountws/topics.go | 37 +++ htx/spot/accountws/types/messages.go | 29 ++ htx/spot/accountws/vars.go | 27 ++ htx/spot/marketws/response.go | 7 +- htx/spot/rest/client.go | 1 - htx/spot/rest/client_test.go | 9 +- mexc/utils/websocket_response.go | 7 +- 14 files changed, 759 insertions(+), 12 deletions(-) create mode 100644 htx/spot/accountws/client.go create mode 100644 htx/spot/accountws/client_test.go create mode 100644 htx/spot/accountws/events.go create mode 100644 htx/spot/accountws/request.go create mode 100644 htx/spot/accountws/response.go create mode 100644 htx/spot/accountws/subscriptions.go create mode 100644 htx/spot/accountws/topics.go create mode 100644 htx/spot/accountws/types/messages.go create mode 100644 htx/spot/accountws/vars.go diff --git a/binance/utils/websocket_response.go b/binance/utils/websocket_response.go index 1c7b266..a8e4038 100644 --- a/binance/utils/websocket_response.go +++ b/binance/utils/websocket_response.go @@ -83,9 +83,11 @@ func (m *AnyMessage) UnmarshalJSON(data []byte) error { } if v.Exists("stream") { - var msg = &SubscribedMessage{ + msg := &SubscribedMessage{ Stream: string(v.GetStringBytes("stream")), - Data: v.Get("data").MarshalTo(nil), + } + if v.Get("data") != nil { + msg.Data = v.Get("data").MarshalTo(nil) } m.SubscribedMessage = msg diff --git a/htx/spot/accountws/client.go b/htx/spot/accountws/client.go new file mode 100644 index 0000000..e52ea2f --- /dev/null +++ b/htx/spot/accountws/client.go @@ -0,0 +1,399 @@ +/* + * Copyright (c) 2023, LinstoHu + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package accountws + +import ( + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "errors" + "fmt" + "log/slog" + "net/http" + "net/url" + "strings" + "sync" + "time" + + "github.com/chuckpreslar/emission" + "github.com/go-playground/validator" + "github.com/gorilla/websocket" + cmap "github.com/orcaman/concurrent-map/v2" +) + +type AccountWsClient struct { + host string + baseURL string + // debug mode + debug bool + // logger + logger *slog.Logger + + key, secret string + + ctx context.Context + conn *websocket.Conn + mu sync.RWMutex + isConnected bool + + autoReconnect bool + disconnect chan struct{} + + sending sync.Mutex + subscriptions cmap.ConcurrentMap[string, struct{}] + + emitter *emission.Emitter +} + +type AccountWsClientCfg struct { + Debug bool + // Logger + Logger *slog.Logger + BaseURL string `validate:"required"` + Key string `validate:"required"` + Secret string `validate:"required"` +} + +func NewAccountWsClient(ctx context.Context, cfg *AccountWsClientCfg) (*AccountWsClient, error) { + if err := validator.New().Struct(cfg); err != nil { + return nil, err + } + + baseURL, err := url.Parse(cfg.BaseURL) + if err != nil { + return nil, err + } + + cli := &AccountWsClient{ + debug: cfg.Debug, + logger: cfg.Logger, + baseURL: cfg.BaseURL, + host: baseURL.Host, + + key: cfg.Key, + secret: cfg.Secret, + + ctx: ctx, + autoReconnect: true, + + subscriptions: cmap.New[struct{}](), + emitter: emission.NewEmitter(), + } + + if cli.logger == nil { + cli.logger = slog.Default() + } + + err = cli.start() + if err != nil { + return nil, err + } + + time.Sleep(100 * time.Millisecond) + + return cli, nil +} + +func (m *AccountWsClient) start() error { + m.conn = nil + m.setIsConnected(false) + m.disconnect = make(chan struct{}) + + for i := 0; i < MaxTryTimes; i++ { + conn, _, err := m.connect() + if err != nil { + m.logger.Info(fmt.Sprintf("connect error, times(%v), error: %s", i, err.Error())) + tm := (i + 1) * 5 + time.Sleep(time.Duration(tm) * time.Second) + continue + } + m.conn = conn + break + } + if m.conn == nil { + return errors.New("connect failed") + } + + m.setIsConnected(true) + + m.resubscribe() + + if m.autoReconnect { + go m.reconnect() + } + + m.auth() + + go m.readMessages() + + return nil +} + +func (m *AccountWsClient) connect() (*websocket.Conn, *http.Response, error) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + conn, resp, err := websocket.DefaultDialer.DialContext(ctx, m.baseURL, nil) + if err == nil { + conn.SetReadLimit(32768 * 64) + } + + return conn, resp, err +} + +func (m *AccountWsClient) reconnect() { + <-m.disconnect + + m.setIsConnected(false) + + m.logger.Info("disconnect, then reconnect...") + + time.Sleep(1 * time.Second) + + select { + case <-m.ctx.Done(): + m.logger.Info(fmt.Sprintf("never reconnect, %s", m.ctx.Err())) + return + default: + m.start() + } +} + +// close closes the websocket connection +func (m *AccountWsClient) close() error { + close(m.disconnect) + + err := m.conn.Close() + if err != nil { + return err + } + + return nil +} + +// setIsConnected sets state for isConnected +func (m *AccountWsClient) setIsConnected(state bool) { + m.mu.Lock() + defer m.mu.Unlock() + + m.isConnected = state +} + +// IsConnected returns the WebSocket connection state +func (m *AccountWsClient) IsConnected() bool { + m.mu.RLock() + defer m.mu.RUnlock() + + return m.isConnected +} + +func (m *AccountWsClient) auth() error { + parameters := url.Values{} + + parameters.Add("accessKey", m.key) + parameters.Add("signatureMethod", "HmacSHA256") + parameters.Add("signatureVersion", "2.1") + + timestamp := time.Now().UTC().Format("2006-01-02T15:04:05") + parameters.Add("timestamp", timestamp) + + var sb strings.Builder + sb.WriteString(http.MethodGet) + sb.WriteString("\n") + sb.WriteString(m.host) + sb.WriteString("\n") + sb.WriteString("/ws/v2") + sb.WriteString("\n") + sb.WriteString(parameters.Encode()) + + hm := hmac.New(sha256.New, []byte(m.secret)) + hm.Write([]byte(sb.String())) + sign := base64.StdEncoding.EncodeToString(hm.Sum(nil)) + + msg := AuthRequest{ + Action: REQ, + Channel: "auth", + Params: AuthParams{ + AuthType: "api", + AccessKey: m.key, + SignatureMethod: "HmacSHA256", + SignatureVersion: "2.1", + Timestamp: timestamp, + Signature: sign, + }, + } + + m.sending.Lock() + defer m.sending.Unlock() + + if !m.IsConnected() { + return errors.New("connection is closed") + } + + return m.conn.WriteJSON(msg) +} + +func (m *AccountWsClient) readMessages() { + for { + select { + case <-m.ctx.Done(): + m.logger.Info(fmt.Sprintf("context done, error: %s", m.ctx.Err().Error())) + + if err := m.close(); err != nil { + m.logger.Info(fmt.Sprintf("websocket connection closed error, %s", err.Error())) + } + + return + default: + var msg Message + + err := m.conn.ReadJSON(&msg) + if err != nil { + m.logger.Error(fmt.Sprintf("read object error, %s", err)) + + if err := m.close(); err != nil { + m.logger.Error(fmt.Sprintf("websocket connection closed error, %s", err.Error())) + } + + return + } + + switch { + case msg.Action == PING: + err := m.pong(&Message{ + Action: PONG, + Data: msg.Data, + }) + if err != nil { + m.logger.Error(fmt.Sprintf("handle ping error: %s", err.Error())) + } + case msg.Action == SUB: + case msg.Action == REQ: + if msg.Channel == "auth" { + if msg.Code != 200 { + m.logger.Error(fmt.Sprintf("auth websocket error, action: %s, ch: %s, code: %v", msg.Action, msg.Channel, msg.Code)) + + if err := m.close(); err != nil { + m.logger.Error(fmt.Sprintf("websocket connection closed error, %s", err.Error())) + } + + return + } else { + m.logger.Info(fmt.Sprintf("auth websocket success, action: %s, ch: %s, code: %v", msg.Action, msg.Channel, msg.Code)) + } + } + case msg.Action == PUSH: + err := m.handle(&msg) + if err != nil { + m.logger.Error(fmt.Sprintf("handle message error: %s", err.Error())) + } + } + } + } +} + +func (m *AccountWsClient) resubscribe() error { + topics := m.subscriptions.Keys() + + if len(topics) == 0 { + return nil + } + + redo := make([]string, 0) + + for _, v := range topics { + // do subscription + err := m.send(&Message{ + Action: SUB, + Channel: v, + }) + + if err != nil { + redo = append(redo, v) + continue + } + } + + if len(redo) != 0 { + return fmt.Errorf("resubscribe error: %s", strings.Join(redo, ",")) + } + + return nil +} + +func (m *AccountWsClient) subscribe(topic string) error { + if m.subscriptions.Has(topic) { + return nil + } + + // do subscription + + err := m.send(&Message{ + Action: SUB, + Channel: topic, + }) + + if err != nil { + return err + } + + m.subscriptions.Set(topic, struct{}{}) + + return nil +} + +func (m *AccountWsClient) unsubscribe(topic string) error { + err := m.send(&Message{ + Action: UNSUB, + Channel: topic, + }) + + if err != nil { + return err + } + + m.subscriptions.Remove(topic) + + return nil +} + +func (m *AccountWsClient) send(req *Message) error { + m.sending.Lock() + defer m.sending.Unlock() + + if !m.IsConnected() { + return errors.New("connection is closed") + } + + return m.conn.WriteJSON(req) +} + +func (m *AccountWsClient) pong(ping *Message) error { + m.sending.Lock() + + // Rate Limit: https://www.htx.com/en-us/opend/newApiPages/?id=662 + defer time.Sleep(100 * time.Millisecond) + defer m.sending.Unlock() + + if !m.IsConnected() { + return errors.New("connection is closed") + } + + return m.conn.WriteJSON(ping) +} diff --git a/htx/spot/accountws/client_test.go b/htx/spot/accountws/client_test.go new file mode 100644 index 0000000..61dc0d1 --- /dev/null +++ b/htx/spot/accountws/client_test.go @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2023, LinstoHu + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package accountws + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/linstohu/nexapi/htx/spot/accountws/types" + "github.com/stretchr/testify/assert" +) + +func testNewAccountWsClient(ctx context.Context, t *testing.T, url string) *AccountWsClient { + cli, err := NewAccountWsClient(ctx, &AccountWsClientCfg{ + BaseURL: url, + Debug: true, + Key: os.Getenv("HTX_KEY"), + Secret: os.Getenv("HTX_SECRET"), + }) + + if err != nil { + t.Fatalf("Could not create websocket client, %s", err) + } + + return cli +} + +func TestSubscribeAccountUpdate(t *testing.T) { + ctx, cancel := context.WithCancel(context.TODO()) + defer cancel() + + cli := testNewAccountWsClient(ctx, t, GlobalWsBaseURL) + + topic, err := cli.GetAccountUpdateTopic(&AccountUpdateTopicParam{ + Mode: 2, + }) + assert.Nil(t, err) + + cli.AddListener(topic, func(e any) { + acc, ok := e.(*types.Account) + if !ok { + return + } + + fmt.Printf("Topic: %s, AccountId: %v, Currency: %v, Balance: %v, Available: %v, ChangeType: %v, AccountType: %v, ChangeTime: %v, SeqNum: %v\n", + topic, acc.AccountID, acc.Currency, acc.Balance, acc.Available, acc.ChangeType, acc.AccountType, acc.ChangeTime, acc.SeqNum) + }) + + cli.Subscribe(topic) + + select {} +} diff --git a/htx/spot/accountws/events.go b/htx/spot/accountws/events.go new file mode 100644 index 0000000..bf30e78 --- /dev/null +++ b/htx/spot/accountws/events.go @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023, LinstoHu + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package accountws + +import "github.com/chuckpreslar/emission" + +type Listener func(any) + +func (m *AccountWsClient) AddListener(event string, listener Listener) *emission.Emitter { + return m.emitter.On(event, listener) +} + +func (m *AccountWsClient) RemoveListener(event string, listener Listener) *emission.Emitter { + return m.emitter.Off(m, listener) +} + +func (m *AccountWsClient) GetListeners(event string, argument any) *emission.Emitter { + return m.emitter.Emit(event, argument) +} diff --git a/htx/spot/accountws/request.go b/htx/spot/accountws/request.go new file mode 100644 index 0000000..b715ebe --- /dev/null +++ b/htx/spot/accountws/request.go @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023, LinstoHu + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package accountws + +type AuthRequest struct { + Action ActionType `json:"action,omitempty"` + Channel string `json:"ch,omitempty"` + Params AuthParams `json:"params,omitempty"` +} + +type AuthParams struct { + AuthType string `json:"authType,omitempty"` + AccessKey string `json:"accessKey,omitempty"` + SignatureMethod string `json:"signatureMethod,omitempty"` + SignatureVersion string `json:"signatureVersion,omitempty"` + Timestamp string `json:"timestamp,omitempty"` + Signature string `json:"signature,omitempty"` +} diff --git a/htx/spot/accountws/response.go b/htx/spot/accountws/response.go new file mode 100644 index 0000000..7105e2d --- /dev/null +++ b/htx/spot/accountws/response.go @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023, LinstoHu + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package accountws + +import ( + "encoding/json" + + "github.com/valyala/fastjson" +) + +type ActionType = string + +const ( + SUB ActionType = "sub" + UNSUB ActionType = "unsub" + REQ ActionType = "req" + PING ActionType = "ping" + PONG ActionType = "pong" + PUSH ActionType = "push" +) + +type Message struct { + Action ActionType `json:"action,omitempty"` + Channel string `json:"ch,omitempty"` + Code int `json:"code,omitempty"` + Data json.RawMessage `json:"data,omitempty"` +} + +func (m *Message) UnmarshalJSON(data []byte) error { + var p fastjson.Parser + v, err := p.ParseBytes(data) + if err != nil { + return err + } + + m.Action = string(v.GetStringBytes("action")) + m.Channel = string(v.GetStringBytes("ch")) + m.Code = v.GetInt("code") + if v.Get("data") != nil { + m.Data = v.Get("data").MarshalTo(nil) + } + + return nil +} diff --git a/htx/spot/accountws/subscriptions.go b/htx/spot/accountws/subscriptions.go new file mode 100644 index 0000000..6c44afd --- /dev/null +++ b/htx/spot/accountws/subscriptions.go @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023, LinstoHu + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package accountws + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/linstohu/nexapi/htx/spot/accountws/types" +) + +func (m *AccountWsClient) Subscribe(topic string) error { + return m.subscribe(topic) +} + +func (m *AccountWsClient) UnSubscribe(topic string) error { + return m.unsubscribe(topic) +} + +func (m *AccountWsClient) handle(msg *Message) error { + if m.debug { + m.logger.Info(fmt.Sprintf("subscribed message, channel: %s", msg.Channel)) + } + + switch { + case strings.HasPrefix(msg.Channel, "accounts.update"): + var data types.Account + err := json.Unmarshal(msg.Data, &data) + if err != nil { + return err + } + m.GetListeners(msg.Channel, &data) + default: + return fmt.Errorf("unknown message, topic: %s", msg.Channel) + } + + return nil +} diff --git a/htx/spot/accountws/topics.go b/htx/spot/accountws/topics.go new file mode 100644 index 0000000..e7fae51 --- /dev/null +++ b/htx/spot/accountws/topics.go @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023, LinstoHu + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package accountws + +import ( + "fmt" + + "github.com/go-playground/validator" +) + +type AccountUpdateTopicParam struct { + Mode int `validate:"required,oneof=0 1 2"` +} + +func (m *AccountWsClient) GetAccountUpdateTopic(params *AccountUpdateTopicParam) (string, error) { + err := validator.New().Struct(params) + if err != nil { + return "", err + } + + return fmt.Sprintf("accounts.update#%d", params.Mode), nil +} diff --git a/htx/spot/accountws/types/messages.go b/htx/spot/accountws/types/messages.go new file mode 100644 index 0000000..5c3eee5 --- /dev/null +++ b/htx/spot/accountws/types/messages.go @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023, LinstoHu + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package types + +type Account struct { + Currency string `json:"currency,omitempty"` + AccountID int64 `json:"accountId,omitempty"` + Balance string `json:"balance,omitempty"` + Available string `json:"available,omitempty"` + ChangeType string `json:"changeType,omitempty"` + AccountType string `json:"accountType,omitempty"` + ChangeTime int64 `json:"changeTime,omitempty"` + SeqNum int64 `json:"seqNum,omitempty"` +} diff --git a/htx/spot/accountws/vars.go b/htx/spot/accountws/vars.go new file mode 100644 index 0000000..4e4709e --- /dev/null +++ b/htx/spot/accountws/vars.go @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023, LinstoHu + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package accountws + +const ( + GlobalWsBaseURL = "wss://api.huobi.pro/ws/v2" + GlobalWsBaseURLForAWS = "wss://api-aws.huobi.pro/ws/v2" +) + +const ( + MaxTryTimes = 5 +) diff --git a/htx/spot/marketws/response.go b/htx/spot/marketws/response.go index 00b9b83..36053e2 100644 --- a/htx/spot/marketws/response.go +++ b/htx/spot/marketws/response.go @@ -103,10 +103,13 @@ func (m *AnyMessage) UnmarshalJSON(data []byte) error { } if v.Exists("ch") { - var msg = &SubscribedMessage{ + msg := &SubscribedMessage{ Channel: string(v.GetStringBytes("ch")), Ts: v.GetInt64("ts"), - Data: v.Get("tick").MarshalTo(nil), + } + + if v.Get("tick") != nil { + msg.Data = v.Get("tick").MarshalTo(nil) } m.SubscribedMessage = msg diff --git a/htx/spot/rest/client.go b/htx/spot/rest/client.go index 6706a48..bc729fb 100644 --- a/htx/spot/rest/client.go +++ b/htx/spot/rest/client.go @@ -43,7 +43,6 @@ type SpotClientCfg struct { BaseURL string `validate:"required"` Key string Secret string - SignVersion string } func NewSpotClient(cfg *SpotClientCfg) (*SpotClient, error) { diff --git a/htx/spot/rest/client_test.go b/htx/spot/rest/client_test.go index 9b7d90c..7fa64cd 100644 --- a/htx/spot/rest/client_test.go +++ b/htx/spot/rest/client_test.go @@ -29,11 +29,10 @@ import ( func testNewSpotClient(t *testing.T) *SpotClient { cli, err := NewSpotClient(&SpotClientCfg{ - BaseURL: utils.ProdAWSBaseURL, - Key: os.Getenv("HTX_KEY"), - Secret: os.Getenv("HTX_SECRET"), - SignVersion: utils.ApiKeyVersionV2, - Debug: true, + BaseURL: utils.ProdAWSBaseURL, + Key: os.Getenv("HTX_KEY"), + Secret: os.Getenv("HTX_SECRET"), + Debug: true, }) if err != nil { diff --git a/mexc/utils/websocket_response.go b/mexc/utils/websocket_response.go index 1c7b266..1cb5a11 100644 --- a/mexc/utils/websocket_response.go +++ b/mexc/utils/websocket_response.go @@ -83,9 +83,12 @@ func (m *AnyMessage) UnmarshalJSON(data []byte) error { } if v.Exists("stream") { - var msg = &SubscribedMessage{ + msg := &SubscribedMessage{ Stream: string(v.GetStringBytes("stream")), - Data: v.Get("data").MarshalTo(nil), + } + + if v.Get("data") != nil { + msg.Data = v.Get("data").MarshalTo(nil) } m.SubscribedMessage = msg