github.com/bitfinexcom/bitfinex-api-go@v0.0.0-20210608095005-9e0b26f200fb/v2/websocket/channels.go (about) 1 package websocket 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 8 "github.com/bitfinexcom/bitfinex-api-go/pkg/convert" 9 "github.com/bitfinexcom/bitfinex-api-go/pkg/models/balanceinfo" 10 "github.com/bitfinexcom/bitfinex-api-go/pkg/models/fundingcredit" 11 "github.com/bitfinexcom/bitfinex-api-go/pkg/models/fundinginfo" 12 "github.com/bitfinexcom/bitfinex-api-go/pkg/models/fundingloan" 13 "github.com/bitfinexcom/bitfinex-api-go/pkg/models/fundingoffer" 14 "github.com/bitfinexcom/bitfinex-api-go/pkg/models/fundingtrade" 15 "github.com/bitfinexcom/bitfinex-api-go/pkg/models/margin" 16 "github.com/bitfinexcom/bitfinex-api-go/pkg/models/notification" 17 "github.com/bitfinexcom/bitfinex-api-go/pkg/models/order" 18 "github.com/bitfinexcom/bitfinex-api-go/pkg/models/position" 19 "github.com/bitfinexcom/bitfinex-api-go/pkg/models/tradeexecution" 20 "github.com/bitfinexcom/bitfinex-api-go/pkg/models/tradeexecutionupdate" 21 "github.com/bitfinexcom/bitfinex-api-go/pkg/models/wallet" 22 ) 23 24 type Heartbeat struct { 25 //ChannelIDs []int64 26 } 27 28 func (c *Client) handleChannel(socketId SocketId, msg []byte) error { 29 if c.terminal { 30 return fmt.Errorf("received a message after close") 31 } 32 33 var raw []interface{} 34 err := json.Unmarshal(msg, &raw) 35 if err != nil { 36 return err 37 } else if len(raw) < 2 { 38 return nil 39 } 40 41 chID, ok := raw[0].(float64) 42 if !ok { 43 return fmt.Errorf("expected message to start with a channel id but got %#v instead", raw[0]) 44 } 45 46 chanID := int64(chID) 47 sub, err := c.subscriptions.lookupBySocketChannelID(chanID, socketId) 48 if err != nil { 49 // no subscribed channel for message 50 return err 51 } 52 c.subscriptions.heartbeat(chanID) 53 if sub.Public { 54 switch data := raw[1].(type) { 55 case string: 56 switch data { 57 case "hb": 58 // no-op, already updated heartbeat timeout from this event 59 return nil 60 case "cs": 61 if checksum, ok := raw[2].(float64); ok { 62 return c.handleChecksumChannel(sub, int(checksum)) 63 } else { 64 c.log.Error("Unable to parse checksum") 65 } 66 default: 67 body := raw[2].([]interface{}) 68 return c.handlePublicChannel(sub, sub.Request.Channel, data, body, msg) 69 } 70 case []interface{}: 71 return c.handlePublicChannel(sub, sub.Request.Channel, "", data, msg) 72 } 73 } else { 74 return c.handlePrivateChannel(raw) 75 } 76 return nil 77 } 78 79 func (c *Client) handleChecksumChannel(sub *subscription, checksum int) error { 80 symbol := sub.Request.Symbol 81 // force to signed integer 82 bChecksum := uint32(checksum) 83 var orderbook *Orderbook 84 c.mtx.Lock() 85 if ob, ok := c.orderbooks[symbol]; ok { 86 orderbook = ob 87 } 88 c.mtx.Unlock() 89 if orderbook != nil { 90 oChecksum := orderbook.Checksum() 91 // compare bitfinex checksum with local checksum 92 if bChecksum == oChecksum { 93 c.log.Debugf("Orderbook '%s' checksum verification successful.", symbol) 94 } else { 95 c.log.Warningf("Orderbook '%s' checksum is invalid got %d bot got %d. Data Out of sync, reconnecting.", 96 symbol, bChecksum, oChecksum) 97 err := c.sendUnsubscribeMessage(context.Background(), sub) 98 if err != nil { 99 return err 100 } 101 newSub := &SubscriptionRequest{ 102 SubID: c.nonce.GetNonce(), // generate new subID 103 Event: sub.Request.Event, 104 Channel: sub.Request.Channel, 105 Symbol: sub.Request.Symbol, 106 } 107 _, err_sub := c.Subscribe(context.Background(), newSub) 108 if err_sub != nil { 109 c.log.Warningf("could not resubscribe: %s", err_sub.Error()) 110 return err_sub 111 } 112 } 113 } 114 return nil 115 } 116 117 func (c *Client) handlePublicChannel(sub *subscription, channel, objType string, data []interface{}, raw_msg []byte) error { 118 // unauthenticated data slice 119 // public data is returned as raw interface arrays, use a factory to convert to raw type & publish 120 if factory, ok := c.factories[channel]; ok { 121 // convert to type array of interfaces 122 if len(data) > 0 { 123 if _, ok := data[0].([]interface{}); ok { 124 interfaceArray := convert.ToInterfaceArray(data) 125 // snapshot item 126 c.mtx.Lock() 127 // lock mutex since its mutates client struct 128 msg, err := factory.BuildSnapshot(sub, interfaceArray, raw_msg) 129 c.mtx.Unlock() 130 if err != nil { 131 return err 132 } 133 if msg != nil { 134 c.listener <- msg 135 } 136 } else { 137 // single item 138 msg, err := factory.Build(sub, objType, data, raw_msg) 139 if err != nil { 140 return err 141 } 142 if msg != nil { 143 c.listener <- msg 144 } 145 } 146 } 147 } else { 148 // factory lookup error 149 return fmt.Errorf("could not find public factory for %s channel", channel) 150 } 151 return nil 152 } 153 154 func (c *Client) handlePrivateChannel(raw []interface{}) error { 155 // authenticated data slice, or a heartbeat 156 if val, ok := raw[1].(string); ok && val == "hb" { 157 chanID, ok := raw[0].(float64) 158 if !ok { 159 c.log.Warningf("could not find chanID: %#v", raw) 160 return nil 161 } 162 c.handleHeartbeat(int64(chanID)) 163 } else { 164 // raw[2] is data slice 165 // authenticated snapshots? 166 if len(raw) > 2 { 167 if arr, ok := raw[2].([]interface{}); ok { 168 obj, err := c.handlePrivateDataMessage(raw[1].(string), arr) 169 if err != nil { 170 return err 171 } 172 // private data is returned as strongly typed data, publish directly 173 if obj != nil { 174 c.listener <- obj 175 } 176 } 177 } 178 } 179 return nil 180 } 181 182 func (c *Client) handleHeartbeat(chanID int64) { 183 c.subscriptions.heartbeat(chanID) 184 } 185 186 type unsubscribeMsg struct { 187 Event string `json:"event"` 188 ChanID int64 `json:"chanId"` 189 } 190 191 // public msg: [ChanID, [Data]] 192 // hb (both): [ChanID, "hb"] 193 // private update msg: [ChanID, "type", [Data]] 194 // private snapshot msg: [ChanID, "type", [[Data]]] 195 func (c *Client) handlePrivateDataMessage(term string, data []interface{}) (ms interface{}, err error) { 196 if len(data) == 0 { 197 // empty data msg 198 return nil, nil 199 } 200 201 if term == "hb" { // Heartbeat 202 // TODO: Consider adding a switch to enable/disable passing these along. 203 return &Heartbeat{}, nil 204 } 205 /* 206 list, ok := data[2].([]interface{}) 207 if !ok { 208 return ms, fmt.Errorf("expected data list in third position but got %#v in %#v", data[2], data) 209 } 210 */ 211 ms = c.convertRaw(term, data) 212 213 return 214 } 215 216 // convertRaw takes a term and the raw data attached to it to try and convert that 217 // untyped list into a proper type. 218 func (c *Client) convertRaw(term string, raw []interface{}) interface{} { 219 // The things you do to get proper types. 220 switch term { 221 case "bu": 222 o, err := balanceinfo.FromRaw(raw) 223 if err != nil { 224 return err 225 } 226 bu := balanceinfo.Update(*o) 227 return &bu 228 case "ps": 229 o, err := position.SnapshotFromRaw(raw) 230 if err != nil { 231 return err 232 } 233 return o 234 case "pn": 235 o, err := position.FromRaw(raw) 236 if err != nil { 237 return err 238 } 239 pn := position.New(*o) 240 return &pn 241 case "pu": 242 o, err := position.FromRaw(raw) 243 if err != nil { 244 return err 245 } 246 pu := position.Update(*o) 247 return &pu 248 case "pc": 249 o, err := position.FromRaw(raw) 250 if err != nil { 251 return err 252 } 253 pc := position.Cancel(*o) 254 return &pc 255 case "ws": 256 o, err := wallet.SnapshotFromRaw(raw) 257 if err != nil { 258 return err 259 } 260 return o 261 case "wu": 262 o, err := wallet.FromRaw(raw) 263 if err != nil { 264 return err 265 } 266 wu := wallet.Update(*o) 267 return &wu 268 case "os": 269 o, err := order.SnapshotFromRaw(raw) 270 if err != nil { 271 return err 272 } 273 return o 274 case "on": 275 o, err := order.FromRaw(raw) 276 if err != nil { 277 return err 278 } 279 on := order.New(*o) 280 return &on 281 case "ou": 282 o, err := order.FromRaw(raw) 283 if err != nil { 284 return err 285 } 286 ou := order.Update(*o) 287 return &ou 288 case "oc": 289 o, err := order.FromRaw(raw) 290 if err != nil { 291 return err 292 } 293 oc := order.Cancel(*o) 294 return &oc 295 case "hts": 296 tu, err := tradeexecutionupdate.SnapshotFromRaw(raw) 297 if err != nil { 298 return err 299 } 300 hts := tradeexecutionupdate.HistoricalTradeSnapshot(*tu) 301 return &hts 302 case "te": 303 o, err := tradeexecution.FromRaw(raw) 304 if err != nil { 305 return err 306 } 307 return o 308 case "tu": 309 tu, err := tradeexecutionupdate.FromRaw(raw) 310 if err != nil { 311 return err 312 } 313 return tu 314 case "fte": 315 o, err := fundingtrade.FromRaw(raw) 316 if err != nil { 317 return err 318 } 319 fte := fundingtrade.Execution(*o) 320 return &fte 321 case "ftu": 322 o, err := fundingtrade.FromRaw(raw) 323 if err != nil { 324 return err 325 } 326 ftu := fundingtrade.Update(*o) 327 return &ftu 328 case "hfts": 329 fts, err := fundingtrade.SnapshotFromRaw(raw) 330 if err != nil { 331 return err 332 } 333 nfts := fundingtrade.HistoricalSnapshot(*fts) 334 return &nfts 335 case "n": 336 o, err := notification.FromRaw(raw) 337 if err != nil { 338 return err 339 } 340 return o 341 case "fos": 342 o, err := fundingoffer.SnapshotFromRaw(raw) 343 if err != nil { 344 return err 345 } 346 return o 347 case "fon": 348 o, err := fundingoffer.FromRaw(raw) 349 if err != nil { 350 return err 351 } 352 fon := fundingoffer.New(*o) 353 return &fon 354 case "fou": 355 o, err := fundingoffer.FromRaw(raw) 356 if err != nil { 357 return err 358 } 359 fou := fundingoffer.Update(*o) 360 return &fou 361 case "foc": 362 o, err := fundingoffer.FromRaw(raw) 363 if err != nil { 364 return err 365 } 366 foc := fundingoffer.Cancel(*o) 367 return &foc 368 case "fiu": 369 o, err := fundinginfo.FromRaw(raw) 370 if err != nil { 371 return err 372 } 373 return o 374 case "fcs": 375 o, err := fundingcredit.SnapshotFromRaw(raw) 376 if err != nil { 377 return err 378 } 379 return o 380 case "fcn": 381 o, err := fundingcredit.FromRaw(raw) 382 if err != nil { 383 return err 384 } 385 fcn := fundingcredit.New(*o) 386 return &fcn 387 case "fcu": 388 o, err := fundingcredit.FromRaw(raw) 389 if err != nil { 390 return err 391 } 392 fcu := fundingcredit.Update(*o) 393 return &fcu 394 case "fcc": 395 o, err := fundingcredit.FromRaw(raw) 396 if err != nil { 397 return err 398 } 399 fcc := fundingcredit.Cancel(*o) 400 return &fcc 401 case "fls": 402 o, err := fundingloan.SnapshotFromRaw(raw) 403 if err != nil { 404 return err 405 } 406 return o 407 case "fln": 408 o, err := fundingloan.FromRaw(raw) 409 if err != nil { 410 return err 411 } 412 fln := fundingloan.New(*o) 413 return &fln 414 case "flu": 415 o, err := fundingloan.FromRaw(raw) 416 if err != nil { 417 return err 418 } 419 flu := fundingloan.Update(*o) 420 return &flu 421 case "flc": 422 o, err := fundingloan.FromRaw(raw) 423 if err != nil { 424 return err 425 } 426 flc := fundingloan.Cancel(*o) 427 return &flc 428 //case "uac": 429 case "hb": 430 return &Heartbeat{} 431 case "ats": 432 // TODO: Is not in documentation, so figure out what it is. 433 return nil 434 case "oc-req": 435 // TODO 436 return nil 437 case "on-req": 438 // TODO 439 return nil 440 case "mis": // Should not be sent anymore as of 2017-04-01 441 return nil 442 case "miu": 443 o, err := margin.FromRaw(raw) 444 if err != nil { 445 return err 446 } 447 // return a strongly typed reference, rather than dereference a generic interface 448 // too bad golang doesn't inherit an interface's underlying type when creating a reference to the interface 449 if base, ok := o.(*margin.InfoBase); ok { 450 return base 451 } 452 if update, ok := o.(*margin.InfoUpdate); ok { 453 return update 454 } 455 return o // better than nothing 456 default: 457 c.log.Warningf("unhandled channel data, term: %s", term) 458 } 459 460 return fmt.Errorf("term %q not recognized", term) 461 }