decred.org/dcrdex@v1.0.5/server/market/bookrouter.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package market
     5  
     6  import (
     7  	"context"
     8  	"encoding/json"
     9  	"fmt"
    10  	"sync"
    11  
    12  	"decred.org/dcrdex/dex"
    13  	"decred.org/dcrdex/dex/msgjson"
    14  	"decred.org/dcrdex/dex/order"
    15  	"decred.org/dcrdex/server/comms"
    16  	"decred.org/dcrdex/server/matcher"
    17  )
    18  
    19  // A updateAction classifies updates into how they affect the book or epoch
    20  // queue.
    21  type updateAction uint8
    22  
    23  const (
    24  	// invalidAction is the zero value action and should be considered programmer
    25  	// error if received.
    26  	invalidAction updateAction = iota
    27  	// epochAction means an order is being added to the epoch queue and will
    28  	// result in a msgjson.EpochOrderNote being sent to subscribers.
    29  	epochAction
    30  	// bookAction means an order is being added to the order book, and will result
    31  	// in a msgjson.BookOrderNote being sent to subscribers.
    32  	bookAction
    33  	// unbookAction means an order is being removed from the order book and will
    34  	// result in a msgjson.UnbookOrderNote being sent to subscribers.
    35  	unbookAction
    36  	// updateRemainingAction means a standing limit order has partially filled
    37  	// and will result in a msgjson.UpdateRemainingNote being sent to
    38  	// subscribers.
    39  	updateRemainingAction
    40  	// newEpochAction is an internal signal to the routers main loop that
    41  	// indicates when a new epoch has opened.
    42  	newEpochAction
    43  	// epochReportAction is sent when all bookAction, unbookAction, and
    44  	// updateRemainingAction signals are sent for a completed epoch.
    45  	// This signal performs a couple of important roles. First, it informs the
    46  	// client that the book updates are done, and the book will be static until
    47  	// the end of the epoch. Second, it sends the candlestick data, so a
    48  	// subscriber can maintain a up-to-date candles.Cache without repeatedly
    49  	// querying the HTTP API for the data.
    50  	epochReportAction
    51  	// matchProofAction means the matching has been performed and will result in
    52  	// a msgjson.MatchProofNote being sent to subscribers.
    53  	matchProofAction
    54  	// suspendAction means the market has suspended.
    55  	suspendAction
    56  	// resumeAction means the market has resumed.
    57  	resumeAction
    58  )
    59  
    60  // String provides a string representation of a updateAction. This is primarily
    61  // for logging and debugging purposes.
    62  func (bua updateAction) String() string {
    63  	switch bua {
    64  	case invalidAction:
    65  		return "invalid"
    66  	case epochAction:
    67  		return "epoch"
    68  	case bookAction:
    69  		return "book"
    70  	case unbookAction:
    71  		return "unbook"
    72  	case updateRemainingAction:
    73  		return "update_remaining"
    74  	case newEpochAction:
    75  		return "newEpoch"
    76  	case matchProofAction:
    77  		return "matchProof"
    78  	case suspendAction:
    79  		return "suspend"
    80  	default:
    81  		return ""
    82  	}
    83  }
    84  
    85  // updateSignal combines an updateAction with data for which the action
    86  // applies.
    87  type updateSignal struct {
    88  	action updateAction
    89  	data   any // sigData* type
    90  }
    91  
    92  func (us updateSignal) String() string {
    93  	return us.action.String()
    94  }
    95  
    96  // nolint:structcheck,unused
    97  type sigDataOrder struct {
    98  	order    order.Order
    99  	epochIdx int64
   100  }
   101  
   102  type sigDataBookedOrder sigDataOrder
   103  type sigDataUnbookedOrder sigDataOrder
   104  type sigDataEpochOrder sigDataOrder
   105  type sigDataUpdateRemaining sigDataOrder
   106  
   107  type sigDataEpochReport struct {
   108  	epochIdx     int64
   109  	epochDur     int64
   110  	stats        *matcher.MatchCycleStats
   111  	spot         *msgjson.Spot
   112  	baseFeeRate  uint64
   113  	quoteFeeRate uint64
   114  	matches      [][2]int64
   115  }
   116  
   117  type sigDataNewEpoch struct {
   118  	idx int64
   119  }
   120  
   121  type sigDataSuspend struct {
   122  	finalEpoch  int64
   123  	persistBook bool
   124  }
   125  
   126  type sigDataResume struct {
   127  	epochIdx int64
   128  	// TODO: indicate config change if applicable
   129  }
   130  
   131  type sigDataMatchProof struct {
   132  	matchProof *order.MatchProof
   133  }
   134  
   135  // BookSource is a source of a market's order book and a feed of updates to the
   136  // order book and epoch queue.
   137  type BookSource interface {
   138  	Book() (epoch int64, buys []*order.LimitOrder, sells []*order.LimitOrder)
   139  	OrderFeed() <-chan *updateSignal
   140  	Base() uint32
   141  	Quote() uint32
   142  }
   143  
   144  // subscribers is a manager for a map of subscribers and a sequence counter. The
   145  // sequence counter should be incremented whenever the DEX accepts, books,
   146  // removes, or modifies an order. The client is responsible for tracking the
   147  // sequence ID to ensure all order updates are received. If an update appears to
   148  // be missing, the client should re-subscribe to the market to synchronize the
   149  // order book from scratch.
   150  type subscribers struct {
   151  	mtx   sync.RWMutex
   152  	conns map[uint64]comms.Link
   153  	seq   uint64
   154  }
   155  
   156  // add adds a new subscriber.
   157  func (s *subscribers) add(conn comms.Link) {
   158  	s.mtx.Lock()
   159  	defer s.mtx.Unlock()
   160  	s.conns[conn.ID()] = conn
   161  }
   162  
   163  func (s *subscribers) remove(id uint64) bool {
   164  	s.mtx.Lock()
   165  	defer s.mtx.Unlock()
   166  	_, found := s.conns[id]
   167  	if !found {
   168  		return false
   169  	}
   170  	delete(s.conns, id)
   171  	return true
   172  }
   173  
   174  // nextSeq gets the next sequence number by incrementing the counter. This
   175  // should be used when the book and orders are modified. Currently this applies
   176  // to the routes: book_order, unbook_order, update_remaining, and epoch_order,
   177  // plus suspend if the book is also being purged (persist=false).
   178  func (s *subscribers) nextSeq() uint64 {
   179  	s.mtx.Lock()
   180  	defer s.mtx.Unlock()
   181  	s.seq++
   182  	return s.seq
   183  }
   184  
   185  // lastSeq gets the last retrieved sequence number.
   186  func (s *subscribers) lastSeq() uint64 {
   187  	s.mtx.RLock()
   188  	defer s.mtx.RUnlock()
   189  	return s.seq
   190  }
   191  
   192  // msgBook is a local copy of the order book information. The orders are saved
   193  // as msgjson.BookOrderNote structures.
   194  type msgBook struct {
   195  	name string
   196  	// mtx guards orders and epochIdx
   197  	mtx           sync.RWMutex
   198  	running       bool
   199  	orders        map[order.OrderID]*msgjson.BookOrderNote
   200  	recentMatches [][3]int64
   201  	epochIdx      int64
   202  	subs          *subscribers
   203  	source        BookSource
   204  	baseID        uint32
   205  	quoteID       uint32
   206  }
   207  
   208  func (book *msgBook) setEpoch(idx int64) {
   209  	book.mtx.Lock()
   210  	book.epochIdx = idx
   211  	book.mtx.Unlock()
   212  }
   213  
   214  func (book *msgBook) addRecentMatches(matches [][3]int64) {
   215  	book.mtx.Lock()
   216  	defer book.mtx.Unlock()
   217  
   218  	book.recentMatches = append(matches, book.recentMatches...)
   219  	if len(book.recentMatches) > 100 {
   220  		book.recentMatches = book.recentMatches[:100]
   221  	}
   222  }
   223  
   224  func (book *msgBook) epoch() int64 {
   225  	book.mtx.RLock()
   226  	defer book.mtx.RUnlock()
   227  	return book.epochIdx
   228  }
   229  
   230  // insert adds the information for a new order into the order book. If the order
   231  // is already found, it is inserted, but an error is logged since update should
   232  // be used in that case.
   233  func (book *msgBook) insert(lo *order.LimitOrder) *msgjson.BookOrderNote {
   234  	msgOrder := limitOrderToMsgOrder(lo, book.name)
   235  	book.mtx.Lock()
   236  	defer book.mtx.Unlock()
   237  	if _, found := book.orders[lo.ID()]; found {
   238  		log.Errorf("Found existing order %v in book router when inserting a new one. "+
   239  			"Overwriting, but this should not happen.", lo.ID())
   240  		//panic("bad insert")
   241  	}
   242  	book.orders[lo.ID()] = msgOrder
   243  	return msgOrder
   244  }
   245  
   246  // update updates the order book with the new order information, such as when an
   247  // order's filled amount changes. If the order is not found, it is inserted, but
   248  // an error is logged since insert should be used in that case.
   249  func (book *msgBook) update(lo *order.LimitOrder) *msgjson.BookOrderNote {
   250  	msgOrder := limitOrderToMsgOrder(lo, book.name)
   251  	book.mtx.Lock()
   252  	defer book.mtx.Unlock()
   253  	if _, found := book.orders[lo.ID()]; !found {
   254  		log.Errorf("Did NOT find existing order %v in book router while attempting to update it. "+
   255  			"Adding a new entry, but this should not happen", lo.ID())
   256  		//panic("bad update")
   257  	}
   258  	book.orders[lo.ID()] = msgOrder
   259  	return msgOrder
   260  }
   261  
   262  // Remove the order from the order book.
   263  func (book *msgBook) remove(lo *order.LimitOrder) {
   264  	book.mtx.Lock()
   265  	defer book.mtx.Unlock()
   266  	delete(book.orders, lo.ID())
   267  }
   268  
   269  // addBulkOrders adds the lists of orders to the order book, and records the
   270  // currently active epoch. Use this for the initial sync of the orderbook.
   271  func (book *msgBook) addBulkOrders(epoch int64, orderSets ...[]*order.LimitOrder) {
   272  	book.mtx.Lock()
   273  	defer book.mtx.Unlock()
   274  	book.epochIdx = epoch
   275  	for _, set := range orderSets {
   276  		for _, lo := range set {
   277  			book.orders[lo.ID()] = limitOrderToMsgOrder(lo, book.name)
   278  		}
   279  	}
   280  }
   281  
   282  // BookRouter handles order book subscriptions, syncing the market with a group
   283  // of subscribers, and maintaining an intermediate copy of the orderbook in
   284  // message payload format for quick, full-book syncing.
   285  type BookRouter struct {
   286  	books     map[string]*msgBook
   287  	feeSource FeeSource
   288  
   289  	priceFeeders *subscribers
   290  	spotsMtx     sync.RWMutex
   291  	spots        map[string]*msgjson.Spot
   292  }
   293  
   294  // NewBookRouter is a constructor for a BookRouter. Routes are registered with
   295  // comms and a monitoring goroutine is started for each BookSource specified.
   296  // The input sources is a mapping of market names to sources for order and epoch
   297  // queue information.
   298  func NewBookRouter(sources map[string]BookSource, feeSource FeeSource, route func(route string, handler comms.MsgHandler)) *BookRouter {
   299  	router := &BookRouter{
   300  		books:     make(map[string]*msgBook),
   301  		feeSource: feeSource,
   302  		priceFeeders: &subscribers{
   303  			conns: make(map[uint64]comms.Link),
   304  		},
   305  		spots: make(map[string]*msgjson.Spot),
   306  	}
   307  	for mkt, src := range sources {
   308  		subs := &subscribers{
   309  			conns: make(map[uint64]comms.Link),
   310  		}
   311  		book := &msgBook{
   312  			name:    mkt,
   313  			orders:  make(map[order.OrderID]*msgjson.BookOrderNote),
   314  			subs:    subs,
   315  			source:  src,
   316  			baseID:  src.Base(),
   317  			quoteID: src.Quote(),
   318  		}
   319  		router.books[mkt] = book
   320  	}
   321  	route(msgjson.OrderBookRoute, router.handleOrderBook)
   322  	route(msgjson.UnsubOrderBookRoute, router.handleUnsubOrderBook)
   323  	route(msgjson.FeeRateRoute, router.handleFeeRate)
   324  	route(msgjson.PriceFeedRoute, router.handlePriceFeeder)
   325  
   326  	return router
   327  }
   328  
   329  // Run implements dex.Runner, and is blocking.
   330  func (r *BookRouter) Run(ctx context.Context) {
   331  	var wg sync.WaitGroup
   332  	for _, b := range r.books {
   333  		wg.Add(1)
   334  		go func(b *msgBook) {
   335  			r.runBook(ctx, b)
   336  			wg.Done()
   337  		}(b)
   338  	}
   339  	wg.Wait()
   340  }
   341  
   342  // runBook is a monitoring loop for an order book.
   343  func (r *BookRouter) runBook(ctx context.Context, book *msgBook) {
   344  	// Get the initial book.
   345  	feed := book.source.OrderFeed()
   346  	book.addBulkOrders(book.source.Book())
   347  	subs := book.subs
   348  
   349  	defer func() {
   350  		book.mtx.Lock()
   351  		book.running = false
   352  		book.orders = make(map[order.OrderID]*msgjson.BookOrderNote)
   353  		book.mtx.Unlock()
   354  		log.Infof("Book router terminating for market %q", book.name)
   355  	}()
   356  
   357  	book.mtx.Lock()
   358  	book.running = true
   359  	book.mtx.Unlock()
   360  
   361  out:
   362  	for {
   363  		select {
   364  		case u, ok := <-feed:
   365  			if !ok {
   366  				log.Errorf("Book order feed closed for market %q at epoch %d",
   367  					book.name, book.epoch())
   368  				break out
   369  			}
   370  
   371  			// Prepare the book/unbook/epoch note.
   372  			var note any
   373  			var route string
   374  			var spot *msgjson.Spot
   375  			switch sigData := u.data.(type) {
   376  			case sigDataNewEpoch:
   377  				// New epoch index should be sent here by the market following
   378  				// order matching and booking, but before new orders are added
   379  				// to this new epoch. This is needed for msgjson.OrderBook in
   380  				// sendBook, which must include the current epoch index.
   381  				book.setEpoch(sigData.idx)
   382  				continue // no notification to send
   383  
   384  			case sigDataBookedOrder:
   385  				route = msgjson.BookOrderRoute
   386  				lo, ok := sigData.order.(*order.LimitOrder)
   387  				if !ok {
   388  					panic("non-limit order received with bookAction")
   389  				}
   390  				n := book.insert(lo)
   391  				n.Seq = subs.nextSeq()
   392  				note = n
   393  
   394  			case sigDataUnbookedOrder:
   395  				route = msgjson.UnbookOrderRoute
   396  				lo, ok := sigData.order.(*order.LimitOrder)
   397  				if !ok {
   398  					panic("non-limit order received with unbookAction")
   399  				}
   400  				book.remove(lo)
   401  				oid := sigData.order.ID()
   402  				note = &msgjson.UnbookOrderNote{
   403  					Seq:      subs.nextSeq(),
   404  					MarketID: book.name,
   405  					OrderID:  oid[:],
   406  				}
   407  
   408  			case sigDataUpdateRemaining:
   409  				route = msgjson.UpdateRemainingRoute
   410  				lo, ok := sigData.order.(*order.LimitOrder)
   411  				if !ok {
   412  					panic("non-limit order received with updateRemainingAction")
   413  				}
   414  				bookNote := book.update(lo)
   415  				n := &msgjson.UpdateRemainingNote{
   416  					OrderNote: bookNote.OrderNote,
   417  					Remaining: lo.Remaining(),
   418  				}
   419  				n.Seq = subs.nextSeq()
   420  				note = n
   421  
   422  			case sigDataEpochReport:
   423  				route = msgjson.EpochReportRoute
   424  				startStamp := sigData.epochIdx * sigData.epochDur
   425  				endStamp := startStamp + sigData.epochDur
   426  				stats := sigData.stats
   427  				spot = sigData.spot
   428  
   429  				matchesWithTimestamp := make([][3]int64, 0, len(sigData.matches))
   430  				for _, match := range sigData.matches {
   431  					matchesWithTimestamp = append(matchesWithTimestamp, [3]int64{
   432  						match[0],
   433  						match[1],
   434  						endStamp})
   435  				}
   436  				book.addRecentMatches(matchesWithTimestamp)
   437  
   438  				note = &msgjson.EpochReportNote{
   439  					MarketID:     book.name,
   440  					Epoch:        uint64(sigData.epochIdx),
   441  					BaseFeeRate:  sigData.baseFeeRate,
   442  					QuoteFeeRate: sigData.quoteFeeRate,
   443  					Candle: msgjson.Candle{
   444  						StartStamp:  uint64(startStamp),
   445  						EndStamp:    uint64(endStamp),
   446  						MatchVolume: stats.MatchVolume,
   447  						QuoteVolume: stats.QuoteVolume,
   448  						HighRate:    stats.HighRate,
   449  						LowRate:     stats.LowRate,
   450  						StartRate:   stats.StartRate,
   451  						EndRate:     stats.EndRate,
   452  					},
   453  					MatchSummary: sigData.matches,
   454  				}
   455  
   456  			case sigDataEpochOrder:
   457  				route = msgjson.EpochOrderRoute
   458  				epochNote := new(msgjson.EpochOrderNote)
   459  				switch o := sigData.order.(type) {
   460  				case *order.LimitOrder:
   461  					epochNote.BookOrderNote = *limitOrderToMsgOrder(o, book.name)
   462  					epochNote.OrderType = msgjson.LimitOrderNum
   463  				case *order.MarketOrder:
   464  					epochNote.BookOrderNote = *marketOrderToMsgOrder(o, book.name)
   465  					epochNote.OrderType = msgjson.MarketOrderNum
   466  				case *order.CancelOrder:
   467  					epochNote.BookOrderNote = *cancelOrderToMsgOrder(o, book.name)
   468  					epochNote.OrderType = msgjson.CancelOrderNum
   469  					epochNote.TargetID = o.TargetOrderID[:]
   470  				}
   471  
   472  				epochNote.Seq = subs.nextSeq()
   473  				epochNote.MarketID = book.name
   474  				epochNote.Epoch = uint64(sigData.epochIdx)
   475  				c := sigData.order.Commitment()
   476  				epochNote.Commit = c[:]
   477  
   478  				note = epochNote
   479  
   480  			case sigDataMatchProof:
   481  				route = msgjson.MatchProofRoute
   482  				mp := sigData.matchProof
   483  				misses := make([]msgjson.Bytes, 0, len(mp.Misses))
   484  				for _, o := range mp.Misses {
   485  					oid := o.ID()
   486  					misses = append(misses, oid[:])
   487  				}
   488  				preimages := make([]msgjson.Bytes, 0, len(mp.Preimages))
   489  				for i := range mp.Preimages {
   490  					preimages = append(preimages, mp.Preimages[i][:])
   491  				}
   492  				note = &msgjson.MatchProofNote{
   493  					MarketID:  book.name,
   494  					Epoch:     mp.Epoch.Idx, // not u.epochIdx
   495  					Preimages: preimages,
   496  					Misses:    misses,
   497  					CSum:      mp.CSum,
   498  					Seed:      mp.Seed,
   499  				}
   500  
   501  			case sigDataSuspend:
   502  				// When sent with seq set, it indicates immediate stop, and may
   503  				// also indicate to purge the book.
   504  				route = msgjson.SuspensionRoute
   505  				susp := &msgjson.TradeSuspension{
   506  					MarketID: book.name,
   507  					// SuspendTime of 0 means now.
   508  					FinalEpoch: uint64(sigData.finalEpoch),
   509  					Persist:    sigData.persistBook,
   510  				}
   511  				// Only set Seq if there is a book update.
   512  				if !sigData.persistBook {
   513  					susp.Seq = subs.nextSeq() // book purge
   514  					book.mtx.Lock()
   515  					book.orders = make(map[order.OrderID]*msgjson.BookOrderNote)
   516  					book.mtx.Unlock()
   517  					// The router is "running" although the market is suspended.
   518  				}
   519  				note = susp
   520  
   521  				log.Infof("Market %q suspended after epoch %d, persist book = %v.",
   522  					book.name, sigData.finalEpoch, sigData.persistBook)
   523  
   524  			case sigDataResume:
   525  				route = msgjson.ResumptionRoute
   526  				note = &msgjson.TradeResumption{
   527  					MarketID: book.name,
   528  					// ResumeTime of 0 means now.
   529  					StartEpoch: uint64(sigData.epochIdx),
   530  				} // no Seq for the resume since it doesn't modify the book
   531  
   532  				log.Infof("Market %q resumed at epoch %d", book.name, sigData.epochIdx)
   533  
   534  			default:
   535  				log.Errorf("Unknown orderbook update action %d", u.action)
   536  				continue
   537  			}
   538  
   539  			r.sendNote(route, subs, note)
   540  
   541  			if spot != nil {
   542  				r.sendNote(msgjson.PriceUpdateRoute, r.priceFeeders, spot)
   543  			}
   544  		case <-ctx.Done():
   545  			break out
   546  		}
   547  	}
   548  }
   549  
   550  // Book creates a copy of the book as a *msgjson.OrderBook.
   551  func (r *BookRouter) Book(mktName string) (*msgjson.OrderBook, error) {
   552  	book := r.books[mktName]
   553  	if book == nil {
   554  		return nil, fmt.Errorf("market %s unknown", mktName)
   555  	}
   556  	msgOB := r.msgOrderBook(book)
   557  	if msgOB == nil {
   558  		return nil, fmt.Errorf("market %s not running", mktName)
   559  	}
   560  	return msgOB, nil
   561  }
   562  
   563  // sendBook encodes and sends the the entire order book to the specified client.
   564  func (r *BookRouter) sendBook(conn comms.Link, book *msgBook, msgID uint64) {
   565  	msgOB := r.msgOrderBook(book)
   566  	if msgOB == nil {
   567  		conn.SendError(msgID, msgjson.NewError(msgjson.MarketNotRunningError, "market not running"))
   568  		return
   569  	}
   570  	msg, err := msgjson.NewResponse(msgID, msgOB, nil)
   571  	if err != nil {
   572  		log.Errorf("error encoding 'orderbook' response: %v", err)
   573  		return
   574  	}
   575  
   576  	err = conn.Send(msg) // consider a synchronous send here
   577  	if err != nil {
   578  		log.Debugf("error sending 'orderbook' response: %v", err)
   579  	}
   580  }
   581  
   582  func (r *BookRouter) msgOrderBook(book *msgBook) *msgjson.OrderBook {
   583  	book.mtx.RLock() // book.orders and book.running
   584  	if !book.running {
   585  		book.mtx.RUnlock()
   586  		return nil
   587  	}
   588  	ords := make([]*msgjson.BookOrderNote, 0, len(book.orders))
   589  	for _, o := range book.orders {
   590  		ords = append(ords, o)
   591  	}
   592  	epochIdx := book.epochIdx // instead of book.epoch() while already locked
   593  
   594  	recentMatches := make([][3]int64, len(book.recentMatches))
   595  	copy(recentMatches, book.recentMatches)
   596  
   597  	book.mtx.RUnlock()
   598  
   599  	return &msgjson.OrderBook{
   600  		Seq:           book.subs.lastSeq(),
   601  		MarketID:      book.name,
   602  		Epoch:         uint64(epochIdx),
   603  		Orders:        ords,
   604  		BaseFeeRate:   r.feeSource.LastRate(book.baseID), // MaxFeeRate applied inside feeSource
   605  		QuoteFeeRate:  r.feeSource.LastRate(book.quoteID),
   606  		RecentMatches: recentMatches,
   607  	}
   608  }
   609  
   610  // handleOrderBook is the handler for the non-authenticated 'orderbook' route.
   611  // A client sends a request to this route to start an order book subscription,
   612  // downloading the existing order book and receiving updates as a feed of
   613  // notifications.
   614  func (r *BookRouter) handleOrderBook(conn comms.Link, msg *msgjson.Message) *msgjson.Error {
   615  	sub := new(msgjson.OrderBookSubscription)
   616  	err := msg.Unmarshal(&sub)
   617  	if err != nil || sub == nil {
   618  		return &msgjson.Error{
   619  			Code:    msgjson.RPCParseError,
   620  			Message: "error parsing orderbook request",
   621  		}
   622  	}
   623  	mkt, err := dex.MarketName(sub.Base, sub.Quote)
   624  	if err != nil {
   625  		return &msgjson.Error{
   626  			Code:    msgjson.UnknownMarket,
   627  			Message: "market name error: " + err.Error(),
   628  		}
   629  	}
   630  	book, found := r.books[mkt]
   631  	if !found {
   632  		return &msgjson.Error{
   633  			Code:    msgjson.UnknownMarket,
   634  			Message: "unknown market",
   635  		}
   636  	}
   637  	book.subs.add(conn)
   638  	r.sendBook(conn, book, msg.ID)
   639  	return nil
   640  }
   641  
   642  // handleUnsubOrderBook is the handler for the non-authenticated
   643  // 'unsub_orderbook' route. Clients use this route to unsubscribe from an
   644  // order book.
   645  func (r *BookRouter) handleUnsubOrderBook(conn comms.Link, msg *msgjson.Message) *msgjson.Error {
   646  	unsub := new(msgjson.UnsubOrderBook)
   647  	err := msg.Unmarshal(&unsub)
   648  	if err != nil || unsub == nil {
   649  		return &msgjson.Error{
   650  			Code:    msgjson.RPCParseError,
   651  			Message: "error parsing unsub_orderbook request",
   652  		}
   653  	}
   654  	book := r.books[unsub.MarketID]
   655  	if book == nil {
   656  		return &msgjson.Error{
   657  			Code:    msgjson.UnknownMarket,
   658  			Message: "unknown market: " + unsub.MarketID,
   659  		}
   660  	}
   661  
   662  	if !book.subs.remove(conn.ID()) {
   663  		return &msgjson.Error{
   664  			Code:    msgjson.NotSubscribedError,
   665  			Message: "not subscribed to " + unsub.MarketID,
   666  		}
   667  	}
   668  
   669  	ack, err := msgjson.NewResponse(msg.ID, true, nil)
   670  	if err != nil {
   671  		log.Errorf("failed to encode response payload = true?")
   672  	}
   673  
   674  	err = conn.Send(ack)
   675  	if err != nil {
   676  		log.Debugf("error sending unsub_orderbook response: %v", err)
   677  	}
   678  
   679  	return nil
   680  }
   681  
   682  // handleFeeRate handles a fee_rate request.
   683  func (r *BookRouter) handleFeeRate(conn comms.Link, msg *msgjson.Message) *msgjson.Error {
   684  	var assetID uint32
   685  	err := msg.Unmarshal(&assetID)
   686  	if err != nil {
   687  		return &msgjson.Error{
   688  			Code:    msgjson.RPCParseError,
   689  			Message: "error parsing fee_rate request",
   690  		}
   691  	}
   692  
   693  	// Note that MaxFeeRate is applied inside feeSource.
   694  	resp, err := msgjson.NewResponse(msg.ID, r.feeSource.LastRate(assetID), nil)
   695  	if err != nil {
   696  		log.Errorf("failed to encode fee_rate response: %v", err)
   697  	}
   698  	err = conn.Send(resp)
   699  	if err != nil {
   700  		log.Debugf("error sending fee_rate response: %v", err)
   701  	}
   702  	return nil
   703  }
   704  
   705  func (r *BookRouter) handlePriceFeeder(conn comms.Link, msg *msgjson.Message) *msgjson.Error {
   706  	r.spotsMtx.RLock()
   707  	msg, err := msgjson.NewResponse(msg.ID, r.spots, nil)
   708  	r.spotsMtx.RUnlock()
   709  	if err != nil {
   710  		return &msgjson.Error{
   711  			Code:    msgjson.RPCInternal,
   712  			Message: "encoding error",
   713  		}
   714  	}
   715  
   716  	if err := conn.Send(msg); err == nil {
   717  		r.priceFeeders.add(conn)
   718  	} else {
   719  		log.Debugf("error sending price_feed response: %v", err)
   720  	}
   721  
   722  	return nil
   723  }
   724  
   725  // sendNote sends a notification to the specified subscribers.
   726  func (r *BookRouter) sendNote(route string, subs *subscribers, note any) {
   727  	msg, err := msgjson.NewNotification(route, note)
   728  	if err != nil {
   729  		log.Errorf("error creating notification-type Message: %v", err)
   730  		// Do I need to do some kind of resync here?
   731  		return
   732  	}
   733  
   734  	// Marshal and send the bytes to avoid multiple marshals when sending.
   735  	b, err := json.Marshal(msg)
   736  	if err != nil {
   737  		log.Errorf("unable to marshal notification-type Message: %v", err)
   738  		return
   739  	}
   740  
   741  	var deletes []uint64
   742  	subs.mtx.RLock()
   743  	for _, conn := range subs.conns {
   744  		err := conn.SendRaw(b)
   745  		if err != nil {
   746  			deletes = append(deletes, conn.ID())
   747  		}
   748  	}
   749  	subs.mtx.RUnlock()
   750  	if len(deletes) > 0 {
   751  		subs.mtx.Lock()
   752  		for _, id := range deletes {
   753  			delete(subs.conns, id)
   754  		}
   755  		subs.mtx.Unlock()
   756  	}
   757  }
   758  
   759  // cancelOrderToMsgOrder converts an *order.CancelOrder to a
   760  // *msgjson.BookOrderNote.
   761  func cancelOrderToMsgOrder(o *order.CancelOrder, mkt string) *msgjson.BookOrderNote {
   762  	oid := o.ID()
   763  	return &msgjson.BookOrderNote{
   764  		OrderNote: msgjson.OrderNote{
   765  			// Seq is set by book router.
   766  			MarketID: mkt,
   767  			OrderID:  oid[:],
   768  		},
   769  		TradeNote: msgjson.TradeNote{
   770  			// Side is 0 (neither buy or sell), so omitted.
   771  			Time: uint64(o.ServerTime.UnixMilli()),
   772  		},
   773  	}
   774  }
   775  
   776  // limitOrderToMsgOrder converts an *order.LimitOrder to a
   777  // *msgjson.BookOrderNote.
   778  func limitOrderToMsgOrder(o *order.LimitOrder, mkt string) *msgjson.BookOrderNote {
   779  	oid := o.ID()
   780  	oSide := uint8(msgjson.BuyOrderNum)
   781  	if o.Sell {
   782  		oSide = msgjson.SellOrderNum
   783  	}
   784  	tif := uint8(msgjson.StandingOrderNum)
   785  	if o.Force == order.ImmediateTiF {
   786  		tif = msgjson.ImmediateOrderNum
   787  	}
   788  	return &msgjson.BookOrderNote{
   789  		OrderNote: msgjson.OrderNote{
   790  			// Seq is set by book router.
   791  			MarketID: mkt,
   792  			OrderID:  oid[:],
   793  		},
   794  		TradeNote: msgjson.TradeNote{
   795  			Side:     oSide,
   796  			Quantity: o.Remaining(),
   797  			Rate:     o.Rate,
   798  			TiF:      tif,
   799  			Time:     uint64(o.ServerTime.UnixMilli()),
   800  		},
   801  	}
   802  }
   803  
   804  // marketOrderToMsgOrder converts an *order.MarketOrder to a
   805  // *msgjson.BookOrderNote.
   806  func marketOrderToMsgOrder(o *order.MarketOrder, mkt string) *msgjson.BookOrderNote {
   807  	oid := o.ID()
   808  	oSide := uint8(msgjson.BuyOrderNum)
   809  	if o.Sell {
   810  		oSide = uint8(msgjson.SellOrderNum)
   811  	}
   812  	return &msgjson.BookOrderNote{
   813  		OrderNote: msgjson.OrderNote{
   814  			// Seq is set by book router.
   815  			MarketID: mkt,
   816  			OrderID:  oid[:],
   817  		},
   818  		TradeNote: msgjson.TradeNote{
   819  			Side:     oSide,
   820  			Quantity: o.Remaining(),
   821  			Time:     uint64(o.ServerTime.UnixMilli()),
   822  			// Rate and TiF not set for market orders.
   823  		},
   824  	}
   825  }
   826  
   827  // OrderToMsgOrder converts an order.Order into a *msgjson.BookOrderNote.
   828  func OrderToMsgOrder(ord order.Order, mkt string) (*msgjson.BookOrderNote, error) {
   829  	switch o := ord.(type) {
   830  	case *order.LimitOrder:
   831  		return limitOrderToMsgOrder(o, mkt), nil
   832  	case *order.MarketOrder:
   833  		return marketOrderToMsgOrder(o, mkt), nil
   834  	case *order.CancelOrder:
   835  		return cancelOrderToMsgOrder(o, mkt), nil
   836  	}
   837  	return nil, fmt.Errorf("unknown order type for %v: %T", ord.ID(), ord)
   838  }