github.com/bitfinexcom/bitfinex-api-go@v0.0.0-20210608095005-9e0b26f200fb/v2/websocket/client.go (about) 1 package websocket 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "strings" 8 "sync" 9 "time" 10 "unicode" 11 12 "github.com/gorilla/websocket" 13 "github.com/op/go-logging" 14 15 "github.com/bitfinexcom/bitfinex-api-go/pkg/models/common" 16 "github.com/bitfinexcom/bitfinex-api-go/pkg/utils" 17 18 "crypto/hmac" 19 "crypto/sha512" 20 "encoding/hex" 21 ) 22 23 var productionBaseURL = "wss://api-pub.bitfinex.com/ws/2" 24 25 // ws-specific errors 26 var ( 27 ErrWSNotConnected = fmt.Errorf("websocket connection not established") 28 ErrWSAlreadyConnected = fmt.Errorf("websocket connection already established") 29 ) 30 31 // Available channels 32 const ( 33 ChanBook = "book" 34 ChanTrades = "trades" 35 ChanTicker = "ticker" 36 ChanCandles = "candles" 37 ChanStatus = "status" 38 ) 39 40 // Events 41 const ( 42 EventSubscribe = "subscribe" 43 EventUnsubscribe = "unsubscribe" 44 EventPing = "ping" 45 ) 46 47 // Authentication states 48 const ( 49 NoAuthentication AuthState = 0 50 PendingAuthentication AuthState = 1 51 SuccessfulAuthentication AuthState = 2 52 RejectedAuthentication AuthState = 3 53 ) 54 55 // private type--cannot instantiate. 56 type authState byte 57 58 // AuthState provides a typed authentication state. 59 type AuthState authState // prevent user construction of authStates 60 61 // DMSCancelOnDisconnect cancels session orders on disconnect. 62 const DMSCancelOnDisconnect int = 4 63 64 // Asynchronous interface decouples the underlying transport from API logic. 65 type Asynchronous interface { 66 Connect() error 67 Send(ctx context.Context, msg interface{}) error 68 Listen() <-chan []byte 69 Close() 70 Done() <-chan error 71 } 72 73 type SocketId int 74 type Socket struct { 75 Id SocketId 76 Asynchronous 77 IsConnected bool 78 ResetSubscriptions []*subscription 79 IsAuthenticated bool 80 } 81 82 // AsynchronousFactory provides an interface to re-create asynchronous transports during reconnect events. 83 type AsynchronousFactory interface { 84 Create() Asynchronous 85 } 86 87 // WebsocketAsynchronousFactory creates a websocket-based asynchronous transport. 88 type WebsocketAsynchronousFactory struct { 89 parameters *Parameters 90 } 91 92 // NewWebsocketAsynchronousFactory creates a new websocket factory with a given URL. 93 func NewWebsocketAsynchronousFactory(parameters *Parameters) AsynchronousFactory { 94 return &WebsocketAsynchronousFactory{ 95 parameters: parameters, 96 } 97 } 98 99 // Create returns a new websocket transport. 100 func (w *WebsocketAsynchronousFactory) Create() Asynchronous { 101 return newWs(w.parameters.URL, w.parameters.LogTransport, w.parameters.Logger) 102 } 103 104 // Client provides a unified interface for users to interact with the Bitfinex V2 Websocket API. 105 // nolint:megacheck,structcheck 106 type Client struct { 107 asyncFactory AsynchronousFactory // for re-creating transport during reconnects 108 109 timeout int64 // read timeout 110 apiKey string 111 apiSecret string 112 cancelOnDisconnect bool 113 Authentication AuthState 114 sockets map[SocketId]*Socket 115 nonce utils.NonceGenerator 116 terminal bool 117 init bool 118 log *logging.Logger 119 120 // connection & operational behavior 121 parameters *Parameters 122 123 // subscription manager 124 subscriptions *subscriptions 125 factories map[string]messageFactory 126 orderbooks map[string]*Orderbook 127 128 // close signal sent to user on shutdown 129 shutdown chan bool 130 131 // downstream listener channel to deliver API objects 132 listener chan interface{} 133 134 // race management 135 mtx *sync.RWMutex 136 waitGroup sync.WaitGroup 137 } 138 139 // Credentials assigns authentication credentials to a connection request. 140 func (c *Client) Credentials(key string, secret string) *Client { 141 c.apiKey = key 142 c.apiSecret = secret 143 return c 144 } 145 146 // CancelOnDisconnect ensures all orders will be canceled if this API session is disconnected. 147 func (c *Client) CancelOnDisconnect(cxl bool) *Client { 148 c.cancelOnDisconnect = cxl 149 return c 150 } 151 152 func (c *Client) sign(msg string) (string, error) { 153 sig := hmac.New(sha512.New384, []byte(c.apiSecret)) 154 _, err := sig.Write([]byte(msg)) 155 if err != nil { 156 return "", err 157 } 158 return hex.EncodeToString(sig.Sum(nil)), nil 159 } 160 161 func (c *Client) registerFactory(channel string, factory messageFactory) { 162 c.factories[channel] = factory 163 } 164 165 // New creates a default client. 166 func New() *Client { 167 return NewWithParams(NewDefaultParameters()) 168 } 169 170 // NewWithAsyncFactory creates a new default client with a given asynchronous transport factory interface. 171 func NewWithAsyncFactory(async AsynchronousFactory) *Client { 172 return NewWithParamsAsyncFactory(NewDefaultParameters(), async) 173 } 174 175 // NewWithParams creates a new default client with a given set of parameters. 176 func NewWithParams(params *Parameters) *Client { 177 return NewWithParamsAsyncFactory(params, NewWebsocketAsynchronousFactory(params)) 178 } 179 180 // NewWithAsyncFactoryNonce creates a new default client with a given asynchronous transport factory and nonce generator. 181 func NewWithAsyncFactoryNonce(async AsynchronousFactory, nonce utils.NonceGenerator) *Client { 182 return NewWithParamsAsyncFactoryNonce(NewDefaultParameters(), async, nonce) 183 } 184 185 // NewWithParamsNonce creates a new default client with a given set of parameters and nonce generator. 186 func NewWithParamsNonce(params *Parameters, nonce utils.NonceGenerator) *Client { 187 return NewWithParamsAsyncFactoryNonce(params, NewWebsocketAsynchronousFactory(params), nonce) 188 } 189 190 // NewWithParamsAsyncFactory creates a new default client with a given set of parameters and asynchronous transport factory interface. 191 func NewWithParamsAsyncFactory(params *Parameters, async AsynchronousFactory) *Client { 192 return NewWithParamsAsyncFactoryNonce(params, async, utils.NewEpochNonceGenerator()) 193 } 194 195 // NewWithParamsAsyncFactoryNonce creates a new client with a given set of parameters, asynchronous transport factory, and nonce generator interfaces. 196 func NewWithParamsAsyncFactoryNonce(params *Parameters, async AsynchronousFactory, nonce utils.NonceGenerator) *Client { 197 c := &Client{ 198 asyncFactory: async, 199 Authentication: NoAuthentication, 200 factories: make(map[string]messageFactory), 201 subscriptions: newSubscriptions(params.HeartbeatTimeout, params.Logger), 202 orderbooks: make(map[string]*Orderbook), 203 nonce: nonce, 204 parameters: params, 205 listener: make(chan interface{}), 206 terminal: false, 207 shutdown: nil, 208 sockets: make(map[SocketId]*Socket), 209 mtx: &sync.RWMutex{}, 210 log: params.Logger, 211 } 212 c.registerPublicFactories() 213 return c 214 } 215 216 // Connect to the Bitfinex API, this should only be called once. 217 func (c *Client) Connect() error { 218 c.dumpParams() 219 c.terminal = false 220 go c.listenDisconnect() 221 return c.connectSocket(SocketId(len(c.sockets))) 222 } 223 224 // Returns true if the underlying asynchronous transport is connected to an endpoint. 225 func (c *Client) IsConnected() bool { 226 c.mtx.RLock() 227 defer c.mtx.RUnlock() 228 for _, socket := range c.sockets { 229 if socket.IsConnected { 230 return true 231 } 232 } 233 return false 234 } 235 236 // Listen for all incoming api websocket messages 237 // When a websocket connection is terminated, the publisher channel will close. 238 func (c *Client) Listen() <-chan interface{} { 239 return c.listener 240 } 241 242 // Close the websocket client which will cause for all 243 // active sockets to be exited and the Done() function 244 // to be called 245 func (c *Client) Close() { 246 c.terminal = true 247 var wg sync.WaitGroup 248 socketCount := len(c.sockets) 249 if socketCount > 0 { 250 for _, socket := range c.sockets { 251 if socket.IsConnected { 252 wg.Add(1) 253 socket.IsConnected = false 254 go func(s *Socket) { 255 c.closeAsyncAndWait(s, c.parameters.ShutdownTimeout) 256 wg.Done() 257 }(socket) 258 } 259 } 260 wg.Wait() 261 } 262 c.subscriptions.Close() 263 close(c.listener) 264 } 265 266 // Unsubscribe from the existing subscription with the given id 267 func (c *Client) Unsubscribe(ctx context.Context, id string) error { 268 sub, err := c.subscriptions.lookupBySubscriptionID(id) 269 if err != nil { 270 return err 271 } 272 // sub is removed from manager on ack from API 273 return c.sendUnsubscribeMessage(ctx, sub) 274 } 275 276 func (c *Client) listenDisconnect() { 277 for { 278 select { 279 case <-c.shutdown: 280 return 281 case hbErr := <-c.subscriptions.ListenDisconnect(): // subscription heartbeat timeout 282 c.log.Warningf("heartbeat disconnect: %s", hbErr.Error.Error()) 283 c.mtx.Lock() 284 if socket, ok := c.sockets[hbErr.Subscription.SocketId]; ok { 285 if socket.IsConnected { 286 c.log.Infof("restarting socket (id=%d) connection", socket.Id) 287 socket.IsConnected = false 288 // reconnect to the socket 289 go func() { 290 c.closeAsyncAndWait(socket, c.parameters.ShutdownTimeout) 291 err := c.reconnect(socket, hbErr.Error) 292 if err != nil { 293 c.log.Warningf("socket disconnect: %s", err.Error()) 294 return 295 } 296 }() 297 } 298 } 299 c.mtx.Unlock() 300 } 301 } 302 } 303 304 func extractSymbolResolutionFromKey(subscription string) (symbol string, resolution common.CandleResolution, err error) { 305 var res, sym string 306 str := strings.Split(subscription, ":") 307 if len(str) < 3 { 308 return "", resolution, fmt.Errorf("could not convert symbol resolution for %s: len %d", subscription, len(str)) 309 } 310 res = str[1] 311 sym = str[2] 312 resolution, err = common.CandleResolutionFromString(res) 313 if err != nil { 314 return "", resolution, err 315 } 316 return sym, resolution, nil 317 } 318 319 func (c *Client) registerPublicFactories() { 320 c.registerFactory(ChanTicker, newTickerFactory(c.subscriptions)) 321 c.registerFactory(ChanTrades, newTradeFactory(c.subscriptions)) 322 c.registerFactory(ChanBook, newBookFactory(c.subscriptions, c.orderbooks, c.parameters.ManageOrderbook)) 323 c.registerFactory(ChanCandles, newCandlesFactory(c.subscriptions)) 324 c.registerFactory(ChanStatus, newStatsFactory(c.subscriptions)) 325 } 326 327 func (c *Client) reconnect(socket *Socket, err error) error { 328 c.mtx.RLock() 329 if c.terminal { 330 // dont attempt to reconnect if terminal 331 c.mtx.RUnlock() 332 return err 333 } 334 if !c.parameters.AutoReconnect { 335 err := fmt.Errorf("AutoReconnect setting is disabled, do not reconnect: %s", err.Error()) 336 c.mtx.RUnlock() 337 return err 338 } 339 c.mtx.RUnlock() 340 reconnectTry := 0 341 for ; reconnectTry < c.parameters.ReconnectAttempts; reconnectTry++ { 342 c.log.Debugf("socket (id=%d) waiting %s until reconnect...", socket.Id, c.parameters.ReconnectInterval) 343 time.Sleep(c.parameters.ReconnectInterval) 344 c.log.Infof("socket (id=%d) reconnect attempt %d/%d", socket.Id, reconnectTry+1, c.parameters.ReconnectAttempts) 345 if err := c.reconnectSocket(socket); err == nil { 346 c.log.Debugf("reconnect OK") 347 return nil 348 } 349 c.log.Warningf("socket (id=%d) reconnect failed: %s", socket.Id, err.Error()) 350 } 351 if err != nil { 352 c.log.Errorf("socket (id=%d) could not reconnect: %s", socket.Id, err.Error()) 353 } 354 return err 355 } 356 357 func (c *Client) dumpParams() { 358 c.log.Debug("----Bitfinex Client Parameters----") 359 c.log.Debugf("AutoReconnect=%t", c.parameters.AutoReconnect) 360 c.log.Debugf("CapacityPerConnection=%t", c.parameters.CapacityPerConnection) 361 c.log.Debugf("ReconnectInterval=%s", c.parameters.ReconnectInterval) 362 c.log.Debugf("ReconnectAttempts=%d", c.parameters.ReconnectAttempts) 363 c.log.Debugf("ShutdownTimeout=%s", c.parameters.ShutdownTimeout) 364 c.log.Debugf("ResubscribeOnReconnect=%t", c.parameters.ResubscribeOnReconnect) 365 c.log.Debugf("HeartbeatTimeout=%s", c.parameters.HeartbeatTimeout) 366 c.log.Debugf("URL=%s", c.parameters.URL) 367 c.log.Debugf("ManageOrderbook=%t", c.parameters.ManageOrderbook) 368 } 369 370 func (c *Client) connectSocket(socketId SocketId) error { 371 async := c.asyncFactory.Create() 372 // create new socket instance 373 socket := &Socket{ 374 Id: socketId, 375 Asynchronous: async, 376 IsConnected: false, 377 ResetSubscriptions: nil, 378 IsAuthenticated: false, 379 } 380 oldSocket, _ := c.socketById(socketId) 381 if oldSocket != nil { 382 // socket exists so use its state 383 socket.IsAuthenticated = oldSocket.IsAuthenticated 384 socket.ResetSubscriptions = oldSocket.ResetSubscriptions 385 } 386 c.mtx.Lock() 387 // add socket to managed map 388 c.sockets[socket.Id] = socket 389 c.mtx.Unlock() 390 // connect socket 391 err := socket.Asynchronous.Connect() 392 if err != nil { 393 // unable to establish connection 394 return err 395 } 396 socket.IsConnected = true 397 go c.listenUpstream(socket) 398 return nil 399 } 400 401 func (c *Client) reconnectSocket(socket *Socket) error { 402 // remove subscriptions from manager but keep a copy so we can resubscribe once 403 // a new connection is established 404 if socket.ResetSubscriptions == nil && c.parameters.ResubscribeOnReconnect { 405 socket.ResetSubscriptions = c.subscriptions.ResetSocketSubscriptions(socket.Id) 406 } 407 // establish a new connection 408 err := c.connectSocket(socket.Id) 409 if err != nil { 410 return err 411 } 412 return nil 413 } 414 415 // start this goroutine before connecting, but this should die during a connection failure 416 func (c *Client) listenUpstream(socket *Socket) { 417 for { 418 select { 419 case err := <-socket.Asynchronous.Done(): 420 if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) { 421 err := c.reconnect(socket, err) 422 if err != nil { 423 c.log.Errorf("Unable to reconnect socket (id=%d) after err: %s", socket.Id, err.Error()) 424 return 425 } 426 } 427 return 428 case msg := <-socket.Asynchronous.Listen(): 429 if msg != nil { 430 err := c.handleMessage(socket.Id, msg) 431 if err != nil { 432 c.log.Warningf("upstream listen error: %s", err.Error()) 433 } 434 } 435 } 436 } 437 } 438 439 func (c *Client) closeAsyncAndWait(socket *Socket, t time.Duration) { 440 timeout := make(chan bool) 441 wg := sync.WaitGroup{} 442 wg.Add(1) 443 go func() { 444 select { 445 case <-socket.Asynchronous.Done(): 446 wg.Done() 447 case <-timeout: 448 c.log.Errorf("socket (id=%d) took too long to close.", socket.Id) 449 wg.Done() 450 } 451 }() 452 go func() { 453 time.Sleep(t) 454 close(timeout) 455 }() 456 socket.Asynchronous.Close() 457 wg.Wait() 458 } 459 460 func (c *Client) handleMessage(socketId SocketId, msg []byte) error { 461 t := bytes.TrimLeftFunc(msg, unicode.IsSpace) 462 var err error 463 // either a channel data array or an event object, raw json encoding 464 if bytes.HasPrefix(t, []byte("[")) { 465 err = c.handleChannel(socketId, msg) 466 } else if bytes.HasPrefix(t, []byte("{")) { 467 err = c.handleEvent(socketId, msg) 468 } else { 469 return fmt.Errorf("unexpected message in socket (id=%d): %s", socketId, msg) 470 } 471 return err 472 } 473 474 func (c *Client) sendUnsubscribeMessage(ctx context.Context, sub *subscription) error { 475 socket, err := c.socketById(sub.SocketId) 476 if err != nil { 477 return err 478 } 479 return socket.Asynchronous.Send(ctx, unsubscribeMsg{Event: "unsubscribe", ChanID: sub.ChanID}) 480 } 481 482 func (c *Client) checkResubscription(socketId SocketId) { 483 socket, err := c.socketById(socketId) 484 if err != nil { 485 panic(err) 486 } 487 if c.parameters.ManageOrderbook { 488 ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 489 defer cancel() 490 _, err_flag := c.EnableFlag(ctx, common.Checksum) 491 if err_flag != nil { 492 c.log.Errorf("could not enable checksum flag %s ", err_flag) 493 } 494 } 495 if c.parameters.ResubscribeOnReconnect && socket.ResetSubscriptions != nil { 496 for _, sub := range socket.ResetSubscriptions { 497 if sub.Request.Event == "auth" { 498 continue 499 } 500 ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 501 defer cancel() 502 sub.Request.SubID = c.nonce.GetNonce() // new nonce 503 c.log.Infof("socket (id=%d) resubscribing to %s with nonce %s", socket.Id, sub.Request.String(), sub.Request.SubID) 504 _, err := c.subscribeBySocket(ctx, socket, sub.Request) 505 if err != nil { 506 c.log.Errorf("could not resubscribe: %s", err.Error()) 507 } 508 } 509 socket.ResetSubscriptions = nil 510 } 511 } 512 513 // called when an info event is received 514 func (c *Client) handleOpen(socketId SocketId) error { 515 authSocket, _ := c.GetAuthenticatedSocket() 516 // if we have auth credentials and there is currently no authenticated 517 // sockets (we are only allowed one) 518 if c.hasCredentials() && authSocket == nil { 519 err_auth := c.authenticate(context.Background(), socketId) 520 if err_auth != nil { 521 return err_auth 522 } 523 return nil 524 } 525 // if the opening socket (triggered by reconnect) is authenticated then re-authenticate 526 if authSocket != nil && authSocket.Id == socketId { 527 err_auth := c.authenticate(context.Background(), socketId) 528 if err_auth != nil { 529 return err_auth 530 } 531 return nil 532 } 533 // resubscribe public channels (we will handle authenticated in authAck) 534 c.checkResubscription(socketId) 535 return nil 536 } 537 538 // called when an auth event is received 539 func (c *Client) handleAuthAck(socketId SocketId, auth *AuthEvent) { 540 if c.Authentication == SuccessfulAuthentication { 541 // set socket to authenticated 542 socket, err := c.socketById(socketId) 543 if err != nil { 544 panic(err) 545 } 546 socket.IsAuthenticated = true 547 err = c.subscriptions.activate(auth.SubID, auth.ChanID) 548 if err != nil { 549 c.log.Errorf("could not activate auth subscription: %s", err.Error()) 550 } 551 c.checkResubscription(socketId) 552 } else { 553 c.log.Error("authentication failed") 554 } 555 } 556 557 func (c *Client) hasCredentials() bool { 558 return c.apiKey != "" && c.apiSecret != "" 559 } 560 561 // Authenticate creates the payload for the authentication request and sends it 562 // to the API. The filters will be applied to the authenticated channel, i.e. 563 // only subscribe to the filtered messages. 564 func (c *Client) authenticate(ctx context.Context, socketId SocketId, filter ...string) error { 565 nonce := c.nonce.GetNonce() 566 payload := "AUTH" + nonce 567 sig, err := c.sign(payload) 568 if err != nil { 569 return err 570 } 571 s := &SubscriptionRequest{ 572 Event: "auth", 573 APIKey: c.apiKey, 574 AuthSig: sig, 575 AuthPayload: payload, 576 AuthNonce: nonce, 577 Filter: filter, 578 SubID: nonce, 579 } 580 if c.cancelOnDisconnect { 581 s.DMS = DMSCancelOnDisconnect 582 } 583 c.subscriptions.add(socketId, s) 584 socket, err := c.socketById(socketId) 585 if err != nil { 586 return err 587 } 588 if err = socket.Asynchronous.Send(ctx, s); err != nil { 589 return err 590 } 591 c.Authentication = PendingAuthentication 592 return nil 593 } 594 595 // get a random socket 596 func (c *Client) getSocket() (*Socket, error) { 597 if len(c.sockets) <= 0 { 598 return nil, fmt.Errorf("no socket found") 599 } 600 return c.sockets[0], nil 601 } 602 603 func (c *Client) getMostAvailableSocket() (*Socket, error) { 604 var retSocket *Socket 605 bestCapacity := 0 606 for _, socket := range c.sockets { 607 capac := c.getAvailableSocketCapacity(socket.Id) 608 if retSocket == nil { 609 retSocket = socket 610 bestCapacity = capac 611 continue 612 } 613 if capac > bestCapacity { 614 retSocket = socket 615 bestCapacity = capac 616 } 617 } 618 if retSocket == nil { 619 return nil, fmt.Errorf("no socket found") 620 } 621 return retSocket, nil 622 } 623 624 // lookup the socket with the given Id, throw error if not found 625 func (c *Client) socketById(socketId SocketId) (*Socket, error) { 626 c.mtx.RLock() 627 defer c.mtx.RUnlock() 628 if socket, ok := c.sockets[socketId]; ok { 629 return socket, nil 630 } 631 return nil, fmt.Errorf("could not find socket with ID %d", socketId) 632 } 633 634 // calculates how many free channels are available across all of the sockets 635 func (c *Client) getTotalAvailableSocketCapacity() int { 636 freeCapacity := 0 637 c.mtx.RLock() 638 ids := make([]SocketId, len(c.sockets)) 639 for i, socket := range c.sockets { 640 ids[i] = socket.Id 641 } 642 c.mtx.RUnlock() 643 for _, id := range ids { 644 freeCapacity += c.getAvailableSocketCapacity(id) 645 } 646 return freeCapacity 647 } 648 649 // calculates how many free channels are available on the given socket 650 func (c *Client) getAvailableSocketCapacity(socketId SocketId) int { 651 c.mtx.RLock() 652 defer c.mtx.RUnlock() 653 subs, err := c.subscriptions.lookupBySocketId(socketId) 654 if err == nil { 655 return c.parameters.CapacityPerConnection - subs.Len() 656 } 657 return c.parameters.CapacityPerConnection 658 } 659 660 // Get the authenticated socket. Due to rate limitations 661 // there can only be one authenticated socket active at a time 662 func (c *Client) GetAuthenticatedSocket() (*Socket, error) { 663 c.mtx.RLock() 664 defer c.mtx.RUnlock() 665 for _, socket := range c.sockets { 666 if socket.IsAuthenticated { 667 return socket, nil 668 } 669 } 670 return nil, fmt.Errorf("no authenticated socket found") 671 }