github.com/bitfinexcom/bitfinex-api-go@v0.0.0-20210608095005-9e0b26f200fb/v1/websocket.go (about) 1 package bitfinex 2 3 import ( 4 "bytes" 5 "crypto/tls" 6 "encoding/json" 7 "log" 8 "net/http" 9 "reflect" 10 11 "github.com/bitfinexcom/bitfinex-api-go/pkg/utils" 12 13 "github.com/gorilla/websocket" 14 ) 15 16 // Pairs available 17 const ( 18 // Pairs 19 BTCUSD = "BTCUSD" 20 LTCUSD = "LTCUSD" 21 LTCBTC = "LTCBTC" 22 ETHUSD = "ETHUSD" 23 ETHBTC = "ETHBTC" 24 ETCUSD = "ETCUSD" 25 ETCBTC = "ETCBTC" 26 BFXUSD = "BFXUSD" 27 BFXBTC = "BFXBTC" 28 ZECUSD = "ZECUSD" 29 ZECBTC = "ZECBTC" 30 XMRUSD = "XMRUSD" 31 XMRBTC = "XMRBTC" 32 RRTUSD = "RRTUSD" 33 RRTBTC = "RRTBTC" 34 XRPUSD = "XRPUSD" 35 XRPBTC = "XRPBTC" 36 EOSETH = "EOSETH" 37 EOSUSD = "EOSUSD" 38 EOSBTC = "EOSBTC" 39 IOTUSD = "IOTUSD" 40 IOTBTC = "IOTBTC" 41 IOTETH = "IOTETH" 42 BCCBTC = "BCCBTC" 43 BCUBTC = "BCUBTC" 44 BCCUSD = "BCCUSD" 45 BCUUSD = "BCUUSD" 46 47 // Channels 48 ChanBook = "book" 49 ChanTrade = "trades" 50 ChanTicker = "ticker" 51 ) 52 53 // WebSocketService allow to connect and receive stream data 54 // from bitfinex.com ws service. 55 // nolint:megacheck,structcheck 56 type WebSocketService struct { 57 // http client 58 client *Client 59 // websocket client 60 ws *websocket.Conn 61 // special web socket for private messages 62 privateWs *websocket.Conn 63 // map internal channels to websocket's 64 chanMap map[float64]chan []float64 65 subscribes []subscribeToChannel 66 } 67 68 type subscribeMsg struct { 69 Event string `json:"event"` 70 Channel string `json:"channel"` 71 Pair string `json:"pair"` 72 ChanID float64 `json:"chanId,omitempty"` 73 } 74 75 type subscribeToChannel struct { 76 Channel string 77 Pair string 78 Chan chan []float64 79 } 80 81 // NewWebSocketService returns a WebSocketService using the given client. 82 func NewWebSocketService(c *Client) *WebSocketService { 83 return &WebSocketService{ 84 client: c, 85 chanMap: make(map[float64]chan []float64), 86 subscribes: make([]subscribeToChannel, 0), 87 } 88 } 89 90 // Connect create new bitfinex websocket connection 91 func (w *WebSocketService) Connect() error { 92 var d = websocket.Dialer{ 93 Subprotocols: []string{"p1", "p2"}, 94 ReadBufferSize: 1024, 95 WriteBufferSize: 1024, 96 Proxy: http.ProxyFromEnvironment, 97 } 98 99 if w.client.WebSocketTLSSkipVerify { 100 d.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} 101 } 102 103 ws, _, err := d.Dial(w.client.WebSocketURL, nil) 104 if err != nil { 105 return err 106 } 107 w.ws = ws 108 return nil 109 } 110 111 // Close web socket connection 112 func (w *WebSocketService) Close() { 113 w.ws.Close() 114 } 115 116 func (w *WebSocketService) AddSubscribe(channel string, pair string, c chan []float64) { 117 s := subscribeToChannel{ 118 Channel: channel, 119 Pair: pair, 120 Chan: c, 121 } 122 w.subscribes = append(w.subscribes, s) 123 } 124 125 func (w *WebSocketService) ClearSubscriptions() { 126 w.subscribes = make([]subscribeToChannel, 0) 127 } 128 129 func (w *WebSocketService) sendSubscribeMessages() error { 130 for _, s := range w.subscribes { 131 msg, _ := json.Marshal(subscribeMsg{ 132 Event: "subscribe", 133 Channel: s.Channel, 134 Pair: s.Pair, 135 }) 136 137 err := w.ws.WriteMessage(websocket.TextMessage, msg) 138 if err != nil { 139 return err 140 } 141 } 142 return nil 143 } 144 145 // Subscribe allows to subsribe to channels and watch for new updates. 146 // This method supports next channels: book, trade, ticker. 147 func (w *WebSocketService) Subscribe() error { 148 // Subscribe to each channel 149 if err := w.sendSubscribeMessages(); err != nil { 150 return err 151 } 152 153 for { 154 _, p, err := w.ws.ReadMessage() 155 if err != nil { 156 return err 157 } 158 159 if bytes.Contains(p, []byte("event")) { 160 w.handleEventMessage(p) 161 } else { 162 w.handleDataMessage(p) 163 } 164 } 165 // nolint 166 return nil 167 } 168 169 func (w *WebSocketService) handleEventMessage(msg []byte) { 170 // Check for first message(event:subscribed) 171 event := &subscribeMsg{} 172 err := json.Unmarshal(msg, event) 173 174 // Received "subscribed" resposne. Link channels. 175 if err == nil { 176 for _, k := range w.subscribes { 177 if event.Event == "subscribed" && event.Pair == k.Pair && event.Channel == k.Channel { 178 w.chanMap[event.ChanID] = k.Chan 179 } 180 } 181 } 182 } 183 184 func (w *WebSocketService) handleDataMessage(msg []byte) { 185 // Received payload or data update 186 var dataUpdate []float64 187 err := json.Unmarshal(msg, &dataUpdate) 188 if err == nil { 189 chanID := dataUpdate[0] 190 // Remove chanID from data update 191 // and send message to internal chan 192 w.chanMap[chanID] <- dataUpdate[1:] 193 } 194 195 // Payload received 196 var fullPayload []interface{} 197 err = json.Unmarshal(msg, &fullPayload) 198 199 if err != nil { 200 log.Println("Error decoding fullPayload", err) 201 } else { 202 if len(fullPayload) > 3 { 203 itemsSlice := fullPayload[3:] 204 i, _ := json.Marshal(itemsSlice) 205 var item []float64 206 err = json.Unmarshal(i, &item) 207 if err == nil { 208 chanID := fullPayload[0].(float64) 209 w.chanMap[chanID] <- item 210 } 211 } else { 212 itemsSlice := fullPayload[1] 213 i, _ := json.Marshal(itemsSlice) 214 var items [][]float64 215 err = json.Unmarshal(i, &items) 216 if err == nil { 217 chanID := fullPayload[0].(float64) 218 for _, v := range items { 219 w.chanMap[chanID] <- v 220 } 221 } 222 } 223 } 224 } 225 226 ///////////////////////////// 227 // Private websocket messages 228 ///////////////////////////// 229 230 type privateConnect struct { 231 Event string `json:"event"` 232 APIKey string `json:"apiKey"` 233 AuthSig string `json:"authSig"` 234 AuthPayload string `json:"authPayload"` 235 } 236 237 // Private channel auth response 238 type privateResponse struct { 239 Event string `json:"event"` 240 Status string `json:"status"` 241 ChanID float64 `json:"chanId,omitempty"` 242 UserID float64 `json:"userId"` 243 } 244 245 type TermData struct { 246 // Data term. E.g: ps, ws, ou, etc... See official documentation for more details. 247 Term string 248 // Data will contain different number of elements for each term. 249 // Examples: 250 // Term: ws, Data: ["exchange","BTC",0.01410829,0] 251 // Term: oc, Data: [0,"BTCUSD",0,-0.01,"","CANCELED",270,0,"2015-10-15T11:26:13Z",0] 252 Data []interface{} 253 Error string 254 } 255 256 func (c *TermData) HasError() bool { 257 return len(c.Error) > 0 258 } 259 260 func (w *WebSocketService) ConnectPrivate(ch chan TermData) { 261 262 var d = websocket.Dialer{ 263 Subprotocols: []string{"p1", "p2"}, 264 ReadBufferSize: 1024, 265 WriteBufferSize: 1024, 266 Proxy: http.ProxyFromEnvironment, 267 } 268 269 if w.client.WebSocketTLSSkipVerify { 270 d.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} 271 } 272 273 ws, _, err := d.Dial(w.client.WebSocketURL, nil) 274 if err != nil { 275 ch <- TermData{ 276 Error: err.Error(), 277 } 278 return 279 } 280 281 nonce := utils.GetNonce() 282 payload := "AUTH" + nonce 283 sig, err_sig := w.client.signPayload(payload) 284 if err_sig != nil { 285 return 286 } 287 connectMsg, _ := json.Marshal(&privateConnect{ 288 Event: "auth", 289 APIKey: w.client.APIKey, 290 AuthSig: sig, 291 AuthPayload: payload, 292 }) 293 294 // Send auth message 295 err = ws.WriteMessage(websocket.TextMessage, connectMsg) 296 if err != nil { 297 ch <- TermData{ 298 Error: err.Error(), 299 } 300 ws.Close() 301 return 302 } 303 304 for { 305 _, p, err := ws.ReadMessage() 306 if err != nil { 307 ch <- TermData{ 308 Error: err.Error(), 309 } 310 ws.Close() 311 return 312 } 313 314 event := &privateResponse{} 315 err = json.Unmarshal(p, &event) 316 if err != nil { 317 // received data update 318 var data []interface{} 319 err = json.Unmarshal(p, &data) 320 if err == nil { 321 if len(data) == 2 { // Heartbeat 322 // XXX: Consider adding a switch to enable/disable passing these along. 323 ch <- TermData{Term: data[1].(string)} 324 return 325 } 326 327 dataTerm := data[1].(string) 328 dataList := data[2].([]interface{}) 329 330 // check for empty data 331 if len(dataList) > 0 { 332 if reflect.TypeOf(dataList[0]) == reflect.TypeOf([]interface{}{}) { 333 // received list of lists 334 for _, v := range dataList { 335 ch <- TermData{ 336 Term: dataTerm, 337 Data: v.([]interface{}), 338 } 339 } 340 } else { 341 // received flat list 342 ch <- TermData{ 343 Term: dataTerm, 344 Data: dataList, 345 } 346 } 347 } 348 } 349 } else { 350 // received auth response 351 if event.Event == "auth" && event.Status != "OK" { 352 ch <- TermData{ 353 Error: "Error connecting to private web socket channel.", 354 } 355 ws.Close() 356 } 357 } 358 } 359 }