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  }