Skip to content

Commit d9bf299

Browse files
committed
Add Binance announcement WebSocket support
1 parent b95e9fe commit d9bf299

File tree

4 files changed

+185
-9
lines changed

4 files changed

+185
-9
lines changed

v2/annouancement_service.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package binance
2+
3+
import (
4+
"crypto/rand"
5+
"encoding/hex"
6+
"errors"
7+
"fmt"
8+
"time"
9+
10+
"github.com/adshao/go-binance/v2/common"
11+
)
12+
13+
// CreateAnnouncementParam creates a new WsAnnouncementParam for use with WsAnnouncementServe.
14+
//
15+
// Currently supports only WithRecvWindow option, which defaults to 6000 milliseconds
16+
// if not specified.
17+
func (c *Client) CreateAnnouncementParam(opts ...RequestOption) (WsAnnouncementParam, error) {
18+
if c.APIKey == "" || c.SecretKey == "" {
19+
return WsAnnouncementParam{}, errors.New("miss apikey or secret key")
20+
}
21+
kt := c.KeyType
22+
if kt == "" {
23+
kt = common.KeyTypeHmac
24+
}
25+
req := new(request)
26+
for _, opt := range opts {
27+
opt(req)
28+
}
29+
if req.recvWindow == 0 {
30+
req.recvWindow = 6000
31+
}
32+
33+
sf, err := common.SignFunc(kt)
34+
if err != nil {
35+
return WsAnnouncementParam{}, err
36+
}
37+
r := make([]byte, 16)
38+
rand.Read(r)
39+
random := hex.EncodeToString(r)
40+
timestamp := time.Now().UnixMilli()
41+
recvWindow := req.recvWindow
42+
43+
param := WsAnnouncementParam{
44+
Random: random,
45+
Topic: "com_announcement_en",
46+
RecvWindow: recvWindow,
47+
Timestamp: timestamp,
48+
ApiKey: c.APIKey,
49+
}
50+
signature, err := sf(c.SecretKey, fmt.Sprintf("random=%s&topic=%s&recvWindow=%d&timestamp=%d", param.Random, param.Topic, param.RecvWindow, param.Timestamp))
51+
if err != nil {
52+
return WsAnnouncementParam{}, err
53+
}
54+
param.Signature = *signature
55+
return param, nil
56+
}

v2/websocket.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type ErrHandler func(err error)
1717
// WsConfig webservice configuration
1818
type WsConfig struct {
1919
Endpoint string
20+
Header *http.Header
2021
Proxy *string
2122
}
2223

@@ -42,7 +43,7 @@ var wsServe = func(cfg *WsConfig, handler WsHandler, errHandler ErrHandler) (don
4243
EnableCompression: true,
4344
}
4445

45-
c, _, err := Dialer.Dial(cfg.Endpoint, nil)
46+
c, _, err := Dialer.Dial(cfg.Endpoint, *cfg.Header)
4647
if err != nil {
4748
return nil, nil, err
4849
}

v2/websocket_service.go

Lines changed: 85 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package binance
22

33
import (
44
"encoding/json"
5+
"errors"
56
"fmt"
7+
"net/http"
68
"strings"
79
"time"
810

@@ -17,6 +19,7 @@ var (
1719
BaseCombinedTestnetURL = "wss://stream.testnet.binance.vision/stream?streams="
1820
BaseWsApiMainURL = "wss://ws-api.binance.com:443/ws-api/v3"
1921
BaseWsApiTestnetURL = "wss://ws-api.testnet.binance.vision/ws-api/v3"
22+
BaseWsAnnouncementURL = "wss://api.binance.com/sapi/wss"
2023

2124
// WebsocketTimeout is an interval for sending ping/pong messages if WebsocketKeepalive is enabled
2225
WebsocketTimeout = time.Second * 600
@@ -135,20 +138,20 @@ func WsCombinedPartialDepthServe(symbolLevels map[string]string, handler WsParti
135138
event.Symbol = strings.ToUpper(symbol)
136139
data := j.Get("data").MustMap()
137140
event.LastUpdateID, _ = data["lastUpdateId"].(json.Number).Int64()
138-
bidsLen := len(data["bids"].([]interface{}))
141+
bidsLen := len(data["bids"].([]any))
139142
event.Bids = make([]Bid, bidsLen)
140143
for i := 0; i < bidsLen; i++ {
141-
item := data["bids"].([]interface{})[i].([]interface{})
144+
item := data["bids"].([]any)[i].([]any)
142145
event.Bids[i] = Bid{
143146
Price: item[0].(string),
144147
Quantity: item[1].(string),
145148
}
146149
}
147-
asksLen := len(data["asks"].([]interface{}))
150+
asksLen := len(data["asks"].([]any))
148151
event.Asks = make([]Ask, asksLen)
149152
for i := 0; i < asksLen; i++ {
150153

151-
item := data["asks"].([]interface{})[i].([]interface{})
154+
item := data["asks"].([]any)[i].([]any)
152155
event.Asks[i] = Ask{
153156
Price: item[0].(string),
154157
Quantity: item[1].(string),
@@ -258,20 +261,20 @@ func wsCombinedDepthServe(endpoint string, handler WsDepthHandler, errHandler Er
258261
event.Time, _ = data["E"].(json.Number).Int64()
259262
event.LastUpdateID, _ = data["u"].(json.Number).Int64()
260263
event.FirstUpdateID, _ = data["U"].(json.Number).Int64()
261-
bidsLen := len(data["b"].([]interface{}))
264+
bidsLen := len(data["b"].([]any))
262265
event.Bids = make([]Bid, bidsLen)
263266
for i := 0; i < bidsLen; i++ {
264-
item := data["b"].([]interface{})[i].([]interface{})
267+
item := data["b"].([]any)[i].([]any)
265268
event.Bids[i] = Bid{
266269
Price: item[0].(string),
267270
Quantity: item[1].(string),
268271
}
269272
}
270-
asksLen := len(data["a"].([]interface{}))
273+
asksLen := len(data["a"].([]any))
271274
event.Asks = make([]Ask, asksLen)
272275
for i := 0; i < asksLen; i++ {
273276

274-
item := data["a"].([]interface{})[i].([]interface{})
277+
item := data["a"].([]any)[i].([]any)
275278
event.Asks[i] = Ask{
276279
Price: item[0].(string),
277280
Quantity: item[1].(string),
@@ -872,6 +875,80 @@ func WsApiInitReadWriteConn() (*websocket.Conn, error) {
872875
return conn, err
873876
}
874877

878+
type WsAnnouncementEvent struct {
879+
CatalogID int64 `json:"catalogId"`
880+
CatalogName string `json:"catalogName"`
881+
PublishDate int64 `json:"publishDate"`
882+
Title string `json:"title"`
883+
Body string `json:"body"`
884+
Disclaimer string `json:"disclaimer"`
885+
}
886+
887+
type WsAnnouncementParam struct {
888+
Random string
889+
Topic string
890+
RecvWindow int64
891+
Timestamp int64
892+
Signature string
893+
ApiKey string
894+
}
895+
type WsAnnouncementHandler func(event *WsAnnouncementEvent)
896+
897+
// WsAnnouncementServe establishes a WebSocket connection to listen for Binance announcements.
898+
// See API documentation: https://developers.binance.com/docs/cms/announcement
899+
//
900+
// Parameters:
901+
//
902+
// params - Should be created using client.CreateAnnouncementParam
903+
// handler - Callback function to handle incoming announcement messages
904+
// errHandler - Error callback function for connection errors
905+
//
906+
// Returns:
907+
//
908+
// doneC - Channel that closes when the connection terminates
909+
// stopC - Channel that can be closed to stop the connection
910+
// err - Any initial connection error
911+
func WsAnnouncementServe(params WsAnnouncementParam, handler WsAnnouncementHandler, errHandler ErrHandler) (doneC, stopC chan struct{}, err error) {
912+
if UseTestnet {
913+
return nil, nil, errors.New("not support testnet")
914+
}
915+
endpoint := fmt.Sprintf("%s?random=%s&topic=%s&recvWindow=%d&timestamp=%d&signature=%s",
916+
BaseWsAnnouncementURL, params.Random, params.Topic, params.RecvWindow, params.Timestamp, params.Signature,
917+
)
918+
919+
cfg := newWsConfig(endpoint)
920+
cfg.Header = &http.Header{}
921+
cfg.Header.Add("X-MBX-APIKEY", params.ApiKey)
922+
wsHandler := func(message []byte) {
923+
event := struct {
924+
Type string `json:"type"`
925+
Topic string `json:"topic"`
926+
Data string `json:"data"`
927+
}{}
928+
929+
err := json.Unmarshal(message, &event)
930+
if err != nil {
931+
errHandler(err)
932+
return
933+
}
934+
935+
if event.Type != "DATA" {
936+
errHandler(errors.New("type is not DATA: " + event.Type))
937+
return
938+
}
939+
940+
if event.Topic != "com_announcement_en" {
941+
errHandler(errors.New("topic is not com_announcement_en: " + event.Topic))
942+
return
943+
}
944+
945+
e := new(WsAnnouncementEvent)
946+
json.Unmarshal([]byte(event.Data), &e)
947+
handler(e)
948+
}
949+
return wsServe(cfg, wsHandler, errHandler)
950+
}
951+
875952
// getWsApiEndpoint return the base endpoint of the API WS according the UseTestnet flag
876953
func getWsApiEndpoint() string {
877954
if UseTestnet {

v2/websocket_service_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1678,3 +1678,45 @@ func (s *websocketServiceTestSuite) assertWsBookTickerEvent(e, a *WsBookTickerEv
16781678
r.Equal(e.BestAskPrice, a.BestAskPrice, "BestAskPrice")
16791679
r.Equal(e.BestAskQty, a.BestAskQty, "BestAskQty")
16801680
}
1681+
1682+
// https://binance-docs.github.io/apidocs/spot/en/#all-book-tickers-stream
1683+
func (s *websocketServiceTestSuite) TestWsAnnouncementServe() {
1684+
data := []byte(`{
1685+
"type": "DATA",
1686+
"topic": "com_announcement_en",
1687+
"data": "{\"catalogId\":161,\"catalogName\":\"Delisting\",\"publishDate\":1753257631403,\"title\":\"Notice of...\",\"body\":\"This is...\",\"disclaimer\":\"Trade on-the-go...\"}"
1688+
}`)
1689+
fakeErrMsg := "fake error"
1690+
s.mockWsServe(data, errors.New(fakeErrMsg))
1691+
defer s.assertWsServe()
1692+
1693+
doneC, stopC, err := WsAnnouncementServe(WsAnnouncementParam{}, func(event *WsAnnouncementEvent) {
1694+
e := &WsAnnouncementEvent{
1695+
CatalogID: 161,
1696+
CatalogName: "Delisting",
1697+
PublishDate: 1753257631403,
1698+
Title: "Notice of...",
1699+
Body: "This is...",
1700+
Disclaimer: "Trade on-the-go...",
1701+
}
1702+
_ = e
1703+
s.assertWsAnnouncementEvent(e, event)
1704+
},
1705+
func(err error) {
1706+
s.r().EqualError(err, fakeErrMsg)
1707+
})
1708+
1709+
s.r().NoError(err)
1710+
stopC <- struct{}{}
1711+
<-doneC
1712+
}
1713+
1714+
func (s *websocketServiceTestSuite) assertWsAnnouncementEvent(e, a *WsAnnouncementEvent) {
1715+
r := s.r()
1716+
r.Equal(e.CatalogID, a.CatalogID, "CatalogID")
1717+
r.Equal(e.CatalogName, a.CatalogName, "CatalogName")
1718+
r.Equal(e.PublishDate, a.PublishDate, "PublishDate")
1719+
r.Equal(e.Title, a.Title, "Title")
1720+
r.Equal(e.Body, a.Body, "Body")
1721+
r.Equal(e.Disclaimer, a.Disclaimer, "Disclaimer")
1722+
}

0 commit comments

Comments
 (0)