From 65ab0cd6e2b9cc6e2205d7b6b50af3b9c211f01f Mon Sep 17 00:00:00 2001 From: wuhewuhe Date: Fri, 3 Nov 2023 01:19:07 +0100 Subject: [PATCH] websocket public & private examples and impl --- bybit_api_client.go | 9 -- bybit_websocket.go | 150 +++++++++++++++++++++++++ consts.go | 32 +++++- examples/websocket/order_private_ws.go | 16 +++ examples/websocket/orderbook_ws.go | 17 +++ go.mod | 3 +- go.sum | 4 + trade.go | 1 + 8 files changed, 220 insertions(+), 12 deletions(-) create mode 100644 bybit_websocket.go create mode 100644 examples/websocket/order_private_ws.go create mode 100644 examples/websocket/orderbook_ws.go create mode 100644 trade.go diff --git a/bybit_api_client.go b/bybit_api_client.go index 15e0d8d..fae4c17 100644 --- a/bybit_api_client.go +++ b/bybit_api_client.go @@ -31,15 +31,6 @@ type Client struct { type doFunc func(req *http.Request) (*http.Response, error) -// Globals -const ( - timestampKey = "X-BAPI-TIMESTAMP" - signatureKey = "X-BAPI-SIGN" - apiRequestKey = "X-BAPI-API-KEY" - recvWindowKey = "X-BAPI-RECV-WINDOW" - signTypeKey = "2" -) - func currentTimestamp() int64 { return FormatTimestamp(time.Now()) } diff --git a/bybit_websocket.go b/bybit_websocket.go new file mode 100644 index 0000000..4d63923 --- /dev/null +++ b/bybit_websocket.go @@ -0,0 +1,150 @@ +package bybit + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "github.com/google/uuid" + "github.com/gorilla/websocket" + "time" +) + +type MessageHandler func(message string) error + +func (b *WebSocket) handleIncomingMessages() { + for { + _, message, err := b.conn.ReadMessage() + if err != nil { + fmt.Println("Error reading:", err) + return + } + + if b.onMessage != nil { + err := b.onMessage(string(message)) + if err != nil { + fmt.Println("Error handling message:", err) + return + } + } + } +} + +func (b *WebSocket) SetMessageHandler(handler MessageHandler) { + b.onMessage = handler +} + +type WebSocket struct { + conn *websocket.Conn + url string + apiKey string + apiSecret string + onMessage MessageHandler +} + +func NewBybitPrivateWebSocket(url, apiKey, apiSecret string, handler MessageHandler) *WebSocket { + return &WebSocket{ + url: url, + apiKey: apiKey, + apiSecret: apiSecret, + onMessage: handler, + } +} + +func NewBybitPublicWebSocket(url string, handler MessageHandler) *WebSocket { + return &WebSocket{ + url: url, + onMessage: handler, + } +} + +func (b *WebSocket) Connect(args []string) error { + var err error + b.conn, _, err = websocket.DefaultDialer.Dial(b.url, nil) + if err != nil { + return err + } + + if b.requiresAuthentication() { + if err = b.sendAuth(); err != nil { + return err + } + } + + go b.handleIncomingMessages() + + Ping(b) + + return b.sendSubscription(args) +} + +func Ping(b *WebSocket) { + ticker := time.NewTicker(15 * time.Second) + go func() { + defer ticker.Stop() // Ensure the ticker is stopped when this goroutine ends + for { + select { + case <-ticker.C: // Wait until the ticker sends a signal + if err := b.conn.WriteMessage(websocket.PingMessage, nil); err != nil { + fmt.Println("Failed to send ping:", err) + } + } + } + }() +} + +func (b *WebSocket) Disconnect() error { + return b.conn.Close() +} + +func (b *WebSocket) Send(message string) error { + return b.conn.WriteMessage(websocket.TextMessage, []byte(message)) +} + +func (b *WebSocket) requiresAuthentication() bool { + return b.url == WEBSOCKET_PRIVATE_MAINNET || + b.url == WEBSOCKET_PRIVATE_TESTNET || + b.url == V3_CONTRACT_PRIVATE || + b.url == V3_UNIFIED_PRIVATE || + b.url == V3_SPOT_PRIVATE +} + +func (b *WebSocket) sendAuth() error { + // Get current Unix time in milliseconds + expires := time.Now().UnixNano()/1e6 + 10000 + val := fmt.Sprintf("GET/realtime%d", expires) + + h := hmac.New(sha256.New, []byte(b.apiSecret)) + h.Write([]byte(val)) + + // Convert to hexadecimal instead of base64 + signature := hex.EncodeToString(h.Sum(nil)) + fmt.Println("signature generated : " + signature) + + authMessage := map[string]interface{}{ + "req_id": uuid.New(), // You would need to implement or find a package for generating GUID + "op": "auth", + "args": []interface{}{b.apiKey, expires, signature}, + } + fmt.Println("auth args:", fmt.Sprintf("%v", authMessage["args"])) + return b.SendAsJson(authMessage) +} + +func (b *WebSocket) sendSubscription(args []string) error { + subMessage := map[string]interface{}{ + "req_id": uuid.New(), + "op": "subscribe", + "args": args, + } + fmt.Println("subscribe msg:", fmt.Sprintf("%v", subMessage["args"])) + return b.SendAsJson(subMessage) +} + +func (b *WebSocket) SendAsJson(v interface{}) error { + data, err := json.Marshal(v) + if err != nil { + return err + } + return b.Send(string(data)) +} diff --git a/consts.go b/consts.go index cdbd73c..8a630e5 100644 --- a/consts.go +++ b/consts.go @@ -1,5 +1,33 @@ package bybit -const Name = "bybit.api.go" +const ( + Name = "bybit.api.go" + Version = "1.0.0" + // WebSocket public channel - Mainnet + SPOT_MAINNET = "wss://stream.bybit.com/v5/public/spot" + LINEAR_MAINNET = "wss://stream.bybit.com/v5/public/linear" + INVERSE_MAINNET = "wss://stream.bybit.com/v5/public/inverse" + OPTION_MAINNET = "wss://stream.bybit.com/v5/public/option" -const Version = "1.0.0" + // WebSocket public channel - Testnet + SPOT_TESTNET = "wss://stream-testnet.bybit.com/v5/public/spot" + LINEAR_TESTNET = "wss://stream-testnet.bybit.com/v5/public/linear" + INVERSE_TESTNET = "wss://stream-testnet.bybit.com/v5/public/inverse" + OPTION_TESTNET = "wss://stream-testnet.bybit.com/v5/public/option" + + // WebSocket private channel + WEBSOCKET_PRIVATE_MAINNET = "wss://stream.bybit.com/v5/private" + WEBSOCKET_PRIVATE_TESTNET = "wss://stream-testnet.bybit.com/v5/private" + + // V3 + V3_CONTRACT_PRIVATE = "wss://stream.bybit.com/contract/private/v3" + V3_UNIFIED_PRIVATE = "wss://stream.bybit.com/unified/private/v3" + V3_SPOT_PRIVATE = "wss://stream.bybit.com/spot/private/v3" + + // Globals + timestampKey = "X-BAPI-TIMESTAMP" + signatureKey = "X-BAPI-SIGN" + apiRequestKey = "X-BAPI-API-KEY" + recvWindowKey = "X-BAPI-RECV-WINDOW" + signTypeKey = "2" +) diff --git a/examples/websocket/order_private_ws.go b/examples/websocket/order_private_ws.go new file mode 100644 index 0000000..b0bf487 --- /dev/null +++ b/examples/websocket/order_private_ws.go @@ -0,0 +1,16 @@ +package main + +import ( + bybit "bybit.go.api" + "fmt" +) + +func main() { + ws := bybit.NewBybitPrivateWebSocket("wss://stream-testnet.bybit.com/v5/private", "8wYkmpLsMg10eNQyPm", "Ouxc34myDnXvei54XsBZgoQzfGxO4bkr2Zsj", func(message string) error { + fmt.Println("Received:", message) + return nil + }) + // Connect and subscribe to the desired topic + _ = ws.Connect([]string{"order"}) + select {} +} diff --git a/examples/websocket/orderbook_ws.go b/examples/websocket/orderbook_ws.go new file mode 100644 index 0000000..30e23ef --- /dev/null +++ b/examples/websocket/orderbook_ws.go @@ -0,0 +1,17 @@ +package main + +import ( + bybit "bybit.go.api" + "fmt" +) + +func main() { + // Create a public WebSocket client + ws := bybit.NewBybitPublicWebSocket("wss://stream.bybit.com/v5/public/spot", func(message string) error { + fmt.Println("Received:", message) + return nil + }) + // Connect and subscribe to the desired topic + _ = ws.Connect([]string{"orderbook.1.BTCUSDT"}) + select {} +} diff --git a/go.mod b/go.mod index 914a29e..22a06bc 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,6 @@ go 1.21 require ( github.com/bitly/go-simplejson v0.5.1 + github.com/google/uuid v1.4.0 github.com/gorilla/websocket v1.5.0 - ) \ No newline at end of file +) diff --git a/go.sum b/go.sum index a746f7a..b2f3d68 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= diff --git a/trade.go b/trade.go new file mode 100644 index 0000000..2f7bbfd --- /dev/null +++ b/trade.go @@ -0,0 +1 @@ +package bybit