decred.org/dcrdex@v1.0.5/client/core/bookie.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 core
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"sync"
    10  	"sync/atomic"
    11  	"time"
    12  
    13  	"decred.org/dcrdex/client/asset"
    14  	"decred.org/dcrdex/client/db"
    15  	"decred.org/dcrdex/client/orderbook"
    16  	"decred.org/dcrdex/dex"
    17  	"decred.org/dcrdex/dex/calc"
    18  	"decred.org/dcrdex/dex/candles"
    19  	"decred.org/dcrdex/dex/msgjson"
    20  	"decred.org/dcrdex/dex/order"
    21  	"decred.org/dcrdex/dex/utils"
    22  	"github.com/decred/dcrd/dcrec/secp256k1/v4"
    23  )
    24  
    25  var (
    26  	feederID        uint32
    27  	bookFeedTimeout = time.Minute
    28  
    29  	outdatedClientErr = errors.New("outdated client")
    30  )
    31  
    32  // BookFeed manages a channel for receiving order book updates. It is imperative
    33  // that the feeder (BookFeed).Close() when no longer using the feed.
    34  type BookFeed interface {
    35  	Next() <-chan *BookUpdate
    36  	Close()
    37  	Candles(dur string) error
    38  }
    39  
    40  // bookFeed implements BookFeed.
    41  type bookFeed struct {
    42  	// c is the update channel. Access to c is synchronized by the bookie's
    43  	// feedMtx.
    44  	c      chan *BookUpdate
    45  	bookie *bookie
    46  	id     uint32
    47  }
    48  
    49  // Next returns the channel for receiving updates.
    50  func (f *bookFeed) Next() <-chan *BookUpdate {
    51  	return f.c
    52  }
    53  
    54  // Close the BookFeed.
    55  func (f *bookFeed) Close() {
    56  	f.bookie.closeFeed(f.id)
    57  }
    58  
    59  // Candles subscribes to the candlestick duration and sends the initial set
    60  // of sticks over the update channel.
    61  func (f *bookFeed) Candles(durStr string) error {
    62  	return f.bookie.candles(durStr, f.id)
    63  }
    64  
    65  // candleCache adds synchronization and an on/off switch to *candles.Cache.
    66  type candleCache struct {
    67  	*candles.Cache
    68  	// candleMtx protects the integrity of candles.Cache (e.g. we can't update
    69  	// it while making copy at the same time), so it represents a consistent
    70  	// data snapshot.
    71  	candleMtx sync.RWMutex
    72  	on        uint32
    73  }
    74  
    75  // init resets the candles with the supplied set.
    76  func (c *candleCache) init(in []*msgjson.Candle) {
    77  	c.candleMtx.Lock()
    78  	defer c.candleMtx.Unlock()
    79  	c.Reset()
    80  	for _, candle := range in {
    81  		c.Add(candle)
    82  	}
    83  }
    84  
    85  // addCandle adds the candle using candles.Cache.Add. It returns most recent candle
    86  // in cache.
    87  func (c *candleCache) addCandle(msgCandle *msgjson.Candle) (recent msgjson.Candle, ok bool) {
    88  	if atomic.LoadUint32(&c.on) == 0 {
    89  		return msgjson.Candle{}, false
    90  	}
    91  	c.candleMtx.Lock()
    92  	defer c.candleMtx.Unlock()
    93  	c.Add(msgCandle)
    94  	return *c.Last(), true
    95  }
    96  
    97  // bookie is a BookFeed manager. bookie will maintain any number of order book
    98  // subscribers. When the number of subscribers goes to zero, book feed does
    99  // not immediately close(). Instead, a timer is set and if no more feeders
   100  // subscribe before the timer expires, then the bookie will invoke it's caller
   101  // supplied close() callback.
   102  type bookie struct {
   103  	*orderbook.OrderBook
   104  	dc           *dexConnection
   105  	candleCaches map[string]*candleCache
   106  	log          dex.Logger
   107  
   108  	feedsMtx sync.RWMutex
   109  	feeds    map[uint32]*bookFeed
   110  
   111  	timerMtx   sync.Mutex
   112  	closeTimer *time.Timer
   113  
   114  	base, quote           uint32
   115  	baseUnits, quoteUnits dex.UnitInfo
   116  }
   117  
   118  func defaultUnitInfo(symbol string) dex.UnitInfo {
   119  	return dex.UnitInfo{
   120  		AtomicUnit: "atoms",
   121  		Conventional: dex.Denomination{
   122  			ConversionFactor: 1e8,
   123  			Unit:             symbol,
   124  		},
   125  	}
   126  }
   127  
   128  // newBookie is a constructor for a bookie. The caller should provide a callback
   129  // function to be called when there are no subscribers and the close timer has
   130  // expired.
   131  func newBookie(dc *dexConnection, base, quote uint32, binSizes []string, logger dex.Logger) *bookie {
   132  	candleCaches := make(map[string]*candleCache, len(binSizes))
   133  	for _, durStr := range binSizes {
   134  		dur, err := time.ParseDuration(durStr)
   135  		if err != nil {
   136  			logger.Errorf("failed to ParseDuration(%q)", durStr)
   137  			continue
   138  		}
   139  		candleCaches[durStr] = &candleCache{
   140  			Cache: candles.NewCache(candles.CacheSize, uint64(dur.Milliseconds())),
   141  		}
   142  	}
   143  
   144  	parseUnitInfo := func(assetID uint32) dex.UnitInfo {
   145  		unitInfo, err := asset.UnitInfo(assetID)
   146  		if err == nil {
   147  			return unitInfo
   148  		} else {
   149  			dexAsset := dc.assets[assetID]
   150  			if dexAsset == nil {
   151  				dc.log.Errorf("DEX market has no %d asset. Is this even possible?", base)
   152  				return defaultUnitInfo("XYZ")
   153  			} else {
   154  				unitInfo := dexAsset.UnitInfo
   155  				if unitInfo.Conventional.ConversionFactor == 0 {
   156  					return defaultUnitInfo(dexAsset.Symbol)
   157  				}
   158  				return unitInfo
   159  			}
   160  		}
   161  	}
   162  
   163  	return &bookie{
   164  		OrderBook:    orderbook.NewOrderBook(logger.SubLogger("book")),
   165  		dc:           dc,
   166  		candleCaches: candleCaches,
   167  		log:          logger,
   168  		feeds:        make(map[uint32]*bookFeed, 1),
   169  		base:         base,
   170  		quote:        quote,
   171  		baseUnits:    parseUnitInfo(base),
   172  		quoteUnits:   parseUnitInfo(quote),
   173  	}
   174  }
   175  
   176  // logEpochReport handles the epoch candle in the epoch_report message.
   177  func (b *bookie) logEpochReport(note *msgjson.EpochReportNote) error {
   178  	err := b.LogEpochReport(note)
   179  	if err != nil {
   180  		return err
   181  	}
   182  	if note.Candle.EndStamp == 0 {
   183  		return fmt.Errorf("epoch report has zero-valued candle end stamp")
   184  	}
   185  
   186  	marketID := marketName(b.base, b.quote)
   187  	matchSummaries := b.AddRecentMatches(note.MatchSummary, note.EndStamp)
   188  
   189  	b.send(&BookUpdate{
   190  		Action:   EpochMatchSummary,
   191  		MarketID: marketID,
   192  		Payload: &EpochMatchSummaryPayload{
   193  			MatchSummaries: matchSummaries,
   194  			Epoch:          note.Epoch,
   195  		},
   196  	})
   197  
   198  	for durStr, cache := range b.candleCaches {
   199  		c, ok := cache.addCandle(&note.Candle)
   200  		if !ok {
   201  			continue
   202  		}
   203  		dur, _ := time.ParseDuration(durStr)
   204  		b.send(&BookUpdate{
   205  			Action:   CandleUpdateAction,
   206  			Host:     b.dc.acct.host,
   207  			MarketID: marketID,
   208  			Payload: CandleUpdate{
   209  				Dur:          durStr,
   210  				DurMilliSecs: uint64(dur.Milliseconds()),
   211  				// Providing a copy of msgjson.Candle data here since it will be used concurrently.
   212  				Candle: &c,
   213  			},
   214  		})
   215  	}
   216  
   217  	return nil
   218  }
   219  
   220  // newFeed gets a new *bookFeed and cancels the close timer. feed must be called
   221  // with the bookie.mtx locked. The feed is primed with the provided *BookUpdate.
   222  func (b *bookie) newFeed(u *BookUpdate) *bookFeed {
   223  	b.timerMtx.Lock()
   224  	if b.closeTimer != nil {
   225  		// If Stop returns true, the timer did not fire. If false, the timer
   226  		// already fired and the close func was called. The caller of feed()
   227  		// must be OK with that, or the close func must be able to detect when
   228  		// new feeds exist and abort. To solve the race, the caller of feed()
   229  		// must synchronize with the close func. e.g. Sync locks bookMtx before
   230  		// creating new feeds, and StopBook locks bookMtx to check for feeds
   231  		// before unsubscribing.
   232  		b.closeTimer.Stop()
   233  		b.closeTimer = nil
   234  	}
   235  	b.timerMtx.Unlock()
   236  	feed := &bookFeed{
   237  		c:      make(chan *BookUpdate, 256),
   238  		bookie: b,
   239  		id:     atomic.AddUint32(&feederID, 1),
   240  	}
   241  	feed.c <- u
   242  	b.feedsMtx.Lock()
   243  	b.feeds[feed.id] = feed
   244  	b.feedsMtx.Unlock()
   245  	return feed
   246  }
   247  
   248  // closeFeeds closes the bookie's book feeds and resets the feeds map.
   249  func (b *bookie) closeFeeds() {
   250  	b.feedsMtx.Lock()
   251  	defer b.feedsMtx.Unlock()
   252  	for _, f := range b.feeds {
   253  		close(f.c)
   254  	}
   255  	b.feeds = make(map[uint32]*bookFeed, 1)
   256  
   257  }
   258  
   259  // candles fetches the candle set from the server and activates the candle
   260  // cache.
   261  func (b *bookie) candles(durStr string, feedID uint32) error {
   262  	cache := b.candleCaches[durStr]
   263  	if cache == nil {
   264  		return fmt.Errorf("no candles for %s-%s %q", unbip(b.base), unbip(b.quote), durStr)
   265  	}
   266  	var err error
   267  	defer func() {
   268  		if err != nil {
   269  			return
   270  		}
   271  		b.feedsMtx.RLock()
   272  		defer b.feedsMtx.RUnlock()
   273  		f, ok := b.feeds[feedID]
   274  		if !ok {
   275  			// Feed must have been closed in another thread.
   276  			return
   277  		}
   278  		dur, _ := time.ParseDuration(durStr)
   279  		cache.candleMtx.RLock()
   280  		cdls := cache.CandlesCopy()
   281  		cache.candleMtx.RUnlock()
   282  		f.c <- &BookUpdate{
   283  			Action:   FreshCandlesAction,
   284  			Host:     b.dc.acct.host,
   285  			MarketID: marketName(b.base, b.quote),
   286  			Payload: &CandlesPayload{
   287  				Dur:          durStr,
   288  				DurMilliSecs: uint64(dur.Milliseconds()),
   289  				Candles:      cdls,
   290  			},
   291  		}
   292  	}()
   293  	if atomic.LoadUint32(&cache.on) == 1 {
   294  		return nil
   295  	}
   296  	// Subscribe to the feed.
   297  	payload := &msgjson.CandlesRequest{
   298  		BaseID:     b.base,
   299  		QuoteID:    b.quote,
   300  		BinSize:    durStr,
   301  		NumCandles: candles.CacheSize,
   302  	}
   303  	wireCandles := new(msgjson.WireCandles)
   304  	err = sendRequest(b.dc.WsConn, msgjson.CandlesRoute, payload, wireCandles, DefaultResponseTimeout)
   305  	if err != nil {
   306  		return err
   307  	}
   308  	cache.init(wireCandles.Candles())
   309  	atomic.StoreUint32(&cache.on, 1)
   310  	return nil
   311  }
   312  
   313  // closeFeed closes the specified feed, and if no more feeds are open, sets a
   314  // close timer to disconnect from the market feed.
   315  func (b *bookie) closeFeed(feedID uint32) {
   316  	b.feedsMtx.Lock()
   317  	delete(b.feeds, feedID)
   318  	numFeeds := len(b.feeds)
   319  	b.feedsMtx.Unlock()
   320  
   321  	// If that was the last BookFeed, set a timer to unsubscribe w/ server.
   322  	if numFeeds == 0 {
   323  		b.timerMtx.Lock()
   324  		if b.closeTimer != nil {
   325  			b.closeTimer.Stop()
   326  		}
   327  		b.closeTimer = time.AfterFunc(bookFeedTimeout, func() {
   328  			b.feedsMtx.RLock()
   329  			numFeeds := len(b.feeds)
   330  			b.feedsMtx.RUnlock() // cannot be locked for b.close
   331  			// Note that it is possible that the timer fired as b.feed() was
   332  			// about to stop it before inserting a new BookFeed. If feed() got
   333  			// the mutex first, there will be a feed to prevent b.close below.
   334  			// If closeFeed() got the mutex first, feed() will fail to stop the
   335  			// timer but still register a new BookFeed. The caller of feed()
   336  			// must synchronize with the close func to prevent this.
   337  
   338  			// Call the close func if there are no more feeds.
   339  			if numFeeds == 0 {
   340  				b.dc.stopBook(b.base, b.quote)
   341  			}
   342  		})
   343  		b.timerMtx.Unlock()
   344  	}
   345  }
   346  
   347  // send sends a *BookUpdate to all subscribers.
   348  func (b *bookie) send(u *BookUpdate) {
   349  	b.feedsMtx.Lock()
   350  	defer b.feedsMtx.Unlock()
   351  	for fid, feed := range b.feeds {
   352  		select {
   353  		case feed.c <- u:
   354  		default:
   355  			b.log.Errorf("bookie %p: feed %d is blocking and book update was thrown away.", b, fid)
   356  		}
   357  	}
   358  }
   359  
   360  // book returns the bookie's current order book.
   361  func (b *bookie) book() *OrderBook {
   362  	buys, sells, epoch := b.Orders()
   363  	return &OrderBook{
   364  		Buys:          b.translateBookSide(buys),
   365  		Sells:         b.translateBookSide(sells),
   366  		Epoch:         b.translateBookSide(epoch),
   367  		RecentMatches: b.RecentMatches(),
   368  	}
   369  }
   370  
   371  // minifyOrder creates a MiniOrder from a TradeNote. The epoch and order ID must
   372  // be supplied.
   373  func (b *bookie) minifyOrder(oid dex.Bytes, trade *msgjson.TradeNote, epoch uint64) *MiniOrder {
   374  	return &MiniOrder{
   375  		Qty:       float64(trade.Quantity) / float64(b.baseUnits.Conventional.ConversionFactor),
   376  		QtyAtomic: trade.Quantity,
   377  		Rate:      calc.ConventionalRate(trade.Rate, b.baseUnits, b.quoteUnits),
   378  		MsgRate:   trade.Rate,
   379  		Sell:      trade.Side == msgjson.SellOrderNum,
   380  		Token:     token(oid),
   381  		Epoch:     epoch,
   382  	}
   383  }
   384  
   385  // bookie gets the bookie for the market, if it exists, else nil.
   386  func (dc *dexConnection) bookie(marketID string) *bookie {
   387  	dc.booksMtx.RLock()
   388  	defer dc.booksMtx.RUnlock()
   389  	return dc.books[marketID]
   390  }
   391  
   392  func (dc *dexConnection) midGap(base, quote uint32) (midGap uint64, err error) {
   393  	marketID := marketName(base, quote)
   394  	booky := dc.bookie(marketID)
   395  	if booky == nil {
   396  		return 0, fmt.Errorf("no bookie found for market %s", marketID)
   397  	}
   398  
   399  	return booky.MidGap()
   400  }
   401  
   402  // syncBook subscribes to the order book and returns the book and a BookFeed to
   403  // receive order book updates. The BookFeed must be Close()d when it is no
   404  // longer in use. Use stopBook to unsubscribed and clean up the feed.
   405  func (dc *dexConnection) syncBook(base, quote uint32) (*orderbook.OrderBook, BookFeed, error) {
   406  	dc.cfgMtx.RLock()
   407  	cfg := dc.cfg
   408  	dc.cfgMtx.RUnlock()
   409  
   410  	dc.booksMtx.Lock()
   411  	defer dc.booksMtx.Unlock()
   412  
   413  	mktID := marketName(base, quote)
   414  	booky, found := dc.books[mktID]
   415  	if !found {
   416  		// Make sure the market exists.
   417  		if dc.marketConfig(mktID) == nil {
   418  			return nil, nil, fmt.Errorf("unknown market %s", mktID)
   419  		}
   420  
   421  		obRes, err := dc.subscribe(base, quote)
   422  		if err != nil {
   423  			return nil, nil, err
   424  		}
   425  
   426  		booky = newBookie(dc, base, quote, cfg.BinSizes, dc.log.SubLogger(mktID))
   427  		err = booky.Sync(obRes)
   428  		if err != nil {
   429  			return nil, nil, err
   430  		}
   431  		dc.books[mktID] = booky
   432  	}
   433  
   434  	// Get the feed and the book under a single lock to make sure the first
   435  	// message is the book.
   436  	feed := booky.newFeed(&BookUpdate{
   437  		Action:   FreshBookAction,
   438  		Host:     dc.acct.host,
   439  		MarketID: mktID,
   440  		Payload: &MarketOrderBook{
   441  			Base:  base,
   442  			Quote: quote,
   443  			Book:  booky.book(),
   444  		},
   445  	})
   446  
   447  	return booky.OrderBook, feed, nil
   448  }
   449  
   450  // subscribe subscribes to the given market's order book via the 'orderbook'
   451  // request. The response, which includes book's snapshot, is returned. Proper
   452  // synchronization is required by the caller to ensure that order feed messages
   453  // aren't processed before they are prepared to handle this subscription.
   454  func (dc *dexConnection) subscribe(baseID, quoteID uint32) (*msgjson.OrderBook, error) {
   455  	mkt := marketName(baseID, quoteID)
   456  	// Subscribe via the 'orderbook' request.
   457  	dc.log.Debugf("Subscribing to the %v order book for %v", mkt, dc.acct.host)
   458  	req, err := msgjson.NewRequest(dc.NextID(), msgjson.OrderBookRoute, &msgjson.OrderBookSubscription{
   459  		Base:  baseID,
   460  		Quote: quoteID,
   461  	})
   462  	if err != nil {
   463  		return nil, fmt.Errorf("error encoding 'orderbook' request: %w", err)
   464  	}
   465  	errChan := make(chan error, 1)
   466  	result := new(msgjson.OrderBook)
   467  	err = dc.RequestWithTimeout(req, func(msg *msgjson.Message) {
   468  		errChan <- msg.UnmarshalResult(result)
   469  	}, DefaultResponseTimeout, func() {
   470  		errChan <- fmt.Errorf("timed out waiting for '%s' response", msgjson.OrderBookRoute)
   471  	})
   472  	if err != nil {
   473  		return nil, fmt.Errorf("error subscribing to %s orderbook: %w", mkt, err)
   474  	}
   475  	err = <-errChan
   476  	if err != nil {
   477  		return nil, err
   478  	}
   479  	return result, nil
   480  }
   481  
   482  // stopBook is the close callback passed to the bookie, and will be called when
   483  // there are no more subscribers and the close delay period has expired.
   484  func (dc *dexConnection) stopBook(base, quote uint32) {
   485  	mkt := marketName(base, quote)
   486  	dc.booksMtx.Lock()
   487  	defer dc.booksMtx.Unlock() // hold it locked until unsubscribe request is completed
   488  
   489  	// Abort the unsubscribe if feeds exist for the bookie. This can happen if a
   490  	// bookie's close func is called while a new BookFeed is generated elsewhere.
   491  	if booky, found := dc.books[mkt]; found {
   492  		booky.feedsMtx.Lock()
   493  		numFeeds := len(booky.feeds)
   494  		booky.feedsMtx.Unlock()
   495  		if numFeeds > 0 {
   496  			dc.log.Warnf("Aborting booky %p unsubscribe for market %s with active feeds", booky, mkt)
   497  			return
   498  		}
   499  		// No BookFeeds, delete the bookie.
   500  		delete(dc.books, mkt)
   501  	}
   502  
   503  	if err := dc.unsubscribe(base, quote); err != nil {
   504  		dc.log.Error(err)
   505  	}
   506  }
   507  
   508  // unsubscribe unsubscribes from to the given market's order book.
   509  func (dc *dexConnection) unsubscribe(base, quote uint32) error {
   510  	mkt := marketName(base, quote)
   511  	dc.log.Debugf("Unsubscribing from the %v order book for %v", mkt, dc.acct.host)
   512  	req, err := msgjson.NewRequest(dc.NextID(), msgjson.UnsubOrderBookRoute, &msgjson.UnsubOrderBook{
   513  		MarketID: mkt,
   514  	})
   515  	if err != nil {
   516  		return fmt.Errorf("unsub_orderbook message encoding error: %w", err)
   517  	}
   518  	// Request to unsubscribe. NOTE: does not wait for response.
   519  	err = dc.Request(req, func(msg *msgjson.Message) {
   520  		var res bool
   521  		_ = msg.UnmarshalResult(&res) // res==false if unmarshal fails
   522  		if !res {
   523  			dc.log.Errorf("error unsubscribing from %s", mkt)
   524  		}
   525  	})
   526  	if err != nil {
   527  		return fmt.Errorf("request error unsubscribing from %s orderbook: %w", mkt, err)
   528  	}
   529  	return nil
   530  }
   531  
   532  // SyncBook subscribes to the order book and returns the book and a BookFeed to
   533  // receive order book updates. The BookFeed must be Close()d when it is no
   534  // longer in use.
   535  func (c *Core) SyncBook(host string, base, quote uint32) (*orderbook.OrderBook, BookFeed, error) {
   536  	c.connMtx.RLock()
   537  	dc, found := c.conns[host]
   538  	c.connMtx.RUnlock()
   539  	if !found {
   540  		return nil, nil, fmt.Errorf("unknown DEX '%s'", host)
   541  	}
   542  
   543  	return dc.syncBook(base, quote)
   544  }
   545  
   546  // Book fetches the order book. If a subscription doesn't exist, one will be
   547  // attempted and immediately closed.
   548  func (c *Core) Book(dex string, base, quote uint32) (*OrderBook, error) {
   549  	dex, err := addrHost(dex)
   550  	if err != nil {
   551  		return nil, newError(addressParseErr, "error parsing address: %w", err)
   552  	}
   553  	c.connMtx.RLock()
   554  	dc, found := c.conns[dex]
   555  	c.connMtx.RUnlock()
   556  	if !found {
   557  		return nil, fmt.Errorf("no DEX %s", dex)
   558  	}
   559  
   560  	mkt := marketName(base, quote)
   561  	dc.booksMtx.RLock()
   562  	defer dc.booksMtx.RUnlock() // hold it locked until any transient sub/unsub is completed
   563  	book, found := dc.books[mkt]
   564  	// If not found, attempt to make a temporary subscription and return the
   565  	// initial book.
   566  	if !found {
   567  		snap, err := dc.subscribe(base, quote)
   568  		if err != nil {
   569  			return nil, fmt.Errorf("unable to subscribe to book: %w", err)
   570  		}
   571  		err = dc.unsubscribe(base, quote)
   572  		if err != nil {
   573  			c.log.Errorf("Failed to unsubscribe to %q book: %v", mkt, err)
   574  		}
   575  
   576  		dc.cfgMtx.RLock()
   577  		cfg := dc.cfg
   578  		dc.cfgMtx.RUnlock()
   579  
   580  		book = newBookie(dc, base, quote, cfg.BinSizes, dc.log.SubLogger(mkt))
   581  		if err = book.Sync(snap); err != nil {
   582  			return nil, fmt.Errorf("unable to sync book: %w", err)
   583  		}
   584  	}
   585  
   586  	buys, sells, epoch := book.OrderBook.Orders()
   587  	return &OrderBook{
   588  		Buys:  book.translateBookSide(buys),
   589  		Sells: book.translateBookSide(sells),
   590  		Epoch: book.translateBookSide(epoch),
   591  	}, nil
   592  }
   593  
   594  // translateBookSide translates from []*orderbook.Order to []*MiniOrder.
   595  func (b *bookie) translateBookSide(ins []*orderbook.Order) (outs []*MiniOrder) {
   596  	for _, o := range ins {
   597  		outs = append(outs, &MiniOrder{
   598  			Qty:       float64(o.Quantity) / float64(b.baseUnits.Conventional.ConversionFactor),
   599  			QtyAtomic: o.Quantity,
   600  			Rate:      calc.ConventionalRate(o.Rate, b.baseUnits, b.quoteUnits),
   601  			MsgRate:   o.Rate,
   602  			Sell:      o.Side == msgjson.SellOrderNum,
   603  			Token:     token(o.OrderID[:]),
   604  			Epoch:     o.Epoch,
   605  		})
   606  	}
   607  	return
   608  }
   609  
   610  // handleBookOrderMsg is called when a book_order notification is received.
   611  func handleBookOrderMsg(_ *Core, dc *dexConnection, msg *msgjson.Message) error {
   612  	note := new(msgjson.BookOrderNote)
   613  	err := msg.Unmarshal(note)
   614  	if err != nil {
   615  		return fmt.Errorf("book order note unmarshal error: %w", err)
   616  	}
   617  
   618  	book := dc.bookie(note.MarketID)
   619  	if book == nil {
   620  		return fmt.Errorf("no order book found with market id '%v'",
   621  			note.MarketID)
   622  	}
   623  	err = book.Book(note)
   624  	if err != nil {
   625  		return err
   626  	}
   627  	book.send(&BookUpdate{
   628  		Action:   BookOrderAction,
   629  		Host:     dc.acct.host,
   630  		MarketID: note.MarketID,
   631  		Payload:  book.minifyOrder(note.OrderID, &note.TradeNote, 0),
   632  	})
   633  	return nil
   634  }
   635  
   636  // findMarketConfig searches the stored ConfigResponse for the named market.
   637  // This must be called with cfgMtx at least read locked.
   638  func (dc *dexConnection) findMarketConfig(name string) *msgjson.Market {
   639  	if dc.cfg == nil {
   640  		return nil
   641  	}
   642  	for _, mktConf := range dc.cfg.Markets {
   643  		if mktConf.Name == name {
   644  			return mktConf
   645  		}
   646  	}
   647  	return nil
   648  }
   649  
   650  // setMarketStartEpoch revises the StartEpoch field of the named market in the
   651  // stored ConfigResponse. It optionally zeros FinalEpoch and Persist, which
   652  // should only be done at start time.
   653  func (dc *dexConnection) setMarketStartEpoch(name string, startEpoch uint64, clearFinal bool) {
   654  	dc.cfgMtx.Lock()
   655  	defer dc.cfgMtx.Unlock()
   656  	mkt := dc.findMarketConfig(name)
   657  	if mkt == nil {
   658  		return
   659  	}
   660  	mkt.StartEpoch = startEpoch
   661  	// NOTE: should only clear these if starting now.
   662  	if clearFinal {
   663  		mkt.FinalEpoch = 0
   664  		mkt.Persist = nil
   665  	}
   666  }
   667  
   668  // setMarketFinalEpoch revises the FinalEpoch and Persist fields of the named
   669  // market in the stored ConfigResponse.
   670  func (dc *dexConnection) setMarketFinalEpoch(name string, finalEpoch uint64, persist bool) {
   671  	dc.cfgMtx.Lock()
   672  	defer dc.cfgMtx.Unlock()
   673  	mkt := dc.findMarketConfig(name)
   674  	if mkt == nil {
   675  		return
   676  	}
   677  	mkt.FinalEpoch = finalEpoch
   678  	mkt.Persist = &persist
   679  }
   680  
   681  // handleTradeSuspensionMsg is called when a trade suspension notification is
   682  // received. This message may come in advance of suspension, in which case it
   683  // has a SuspendTime set, or at the time of suspension if subscribed to the
   684  // order book, in which case it has a Seq value set.
   685  func handleTradeSuspensionMsg(c *Core, dc *dexConnection, msg *msgjson.Message) error {
   686  	var sp msgjson.TradeSuspension
   687  	err := msg.Unmarshal(&sp)
   688  	if err != nil {
   689  		return fmt.Errorf("trade suspension unmarshal error: %w", err)
   690  	}
   691  
   692  	// Ensure the provided market exists for the dex.
   693  	mkt := dc.marketConfig(sp.MarketID)
   694  	if mkt == nil {
   695  		return fmt.Errorf("no market found with ID %s", sp.MarketID)
   696  	}
   697  
   698  	// Update the data in the stored ConfigResponse.
   699  	dc.setMarketFinalEpoch(sp.MarketID, sp.FinalEpoch, sp.Persist)
   700  
   701  	// SuspendTime == 0 means suspending now.
   702  	if sp.SuspendTime != 0 {
   703  		// This is just a warning about a scheduled suspension.
   704  		suspendTime := time.UnixMilli(int64(sp.SuspendTime))
   705  		subject, detail := c.formatDetails(TopicMarketSuspendScheduled, sp.MarketID, dc.acct.host, suspendTime)
   706  		c.notify(newServerNotifyNote(TopicMarketSuspendScheduled, subject, detail, db.WarningLevel))
   707  		return nil
   708  	}
   709  
   710  	topic := TopicMarketSuspended
   711  	if !sp.Persist {
   712  		topic = TopicMarketSuspendedWithPurge
   713  	}
   714  	subject, detail := c.formatDetails(topic, sp.MarketID, dc.acct.host)
   715  	c.notify(newServerNotifyNote(topic, subject, detail, db.WarningLevel))
   716  
   717  	if sp.Persist {
   718  		// No book changes. Just wait for more order notes.
   719  		return nil
   720  	}
   721  
   722  	// Clear the book and unbook/revoke own orders.
   723  	book := dc.bookie(sp.MarketID)
   724  	if book == nil {
   725  		return fmt.Errorf("no order book found with market id '%s'", sp.MarketID)
   726  	}
   727  
   728  	err = book.Reset(&msgjson.OrderBook{
   729  		MarketID: sp.MarketID,
   730  		Seq:      sp.Seq,        // forces seq reset, but should be in seq with previous
   731  		Epoch:    sp.FinalEpoch, // unused?
   732  		// Orders is nil
   733  		// BaseFeeRate = QuoteFeeRate = 0 effectively disables the book's fee
   734  		// cache until an update is received, since bestBookFeeSuggestion
   735  		// ignores zeros.
   736  	})
   737  	// Return any non-nil error, but still revoke purged orders.
   738  
   739  	// Revoke all active orders of the suspended market for the dex.
   740  	c.log.Warnf("Revoking all active orders for market %s at %s.", sp.MarketID, dc.acct.host)
   741  	updatedAssets := make(assetMap)
   742  	dc.tradeMtx.RLock()
   743  	for _, tracker := range dc.trades {
   744  		if tracker.Order.Base() == mkt.Base && tracker.Order.Quote() == mkt.Quote &&
   745  			tracker.metaData.Host == dc.acct.host && tracker.status() == order.OrderStatusBooked {
   746  			// Locally revoke the purged book order.
   747  			tracker.revoke()
   748  			subject, details := c.formatDetails(TopicOrderAutoRevoked, tracker.token(), sp.MarketID, dc.acct.host)
   749  			c.notify(newOrderNote(TopicOrderAutoRevoked, subject, details, db.WarningLevel, tracker.coreOrder()))
   750  			updatedAssets.count(tracker.fromAssetID)
   751  		}
   752  	}
   753  	dc.tradeMtx.RUnlock()
   754  
   755  	// Clear the book.
   756  	book.send(&BookUpdate{
   757  		Action:   FreshBookAction,
   758  		Host:     dc.acct.host,
   759  		MarketID: sp.MarketID,
   760  		Payload: &MarketOrderBook{
   761  			Base:  mkt.Base,
   762  			Quote: mkt.Quote,
   763  			Book:  book.book(), // empty
   764  		},
   765  	})
   766  
   767  	if len(updatedAssets) > 0 {
   768  		c.updateBalances(updatedAssets)
   769  	}
   770  
   771  	return err
   772  }
   773  
   774  // handleTradeResumptionMsg is called when a trade resumption notification is
   775  // received. This may be an orderbook message at the time of resumption, or a
   776  // notification of a newly-schedule resumption while the market is suspended
   777  // (prior to the market resume).
   778  func handleTradeResumptionMsg(c *Core, dc *dexConnection, msg *msgjson.Message) error {
   779  	var rs msgjson.TradeResumption
   780  	err := msg.Unmarshal(&rs)
   781  	if err != nil {
   782  		return fmt.Errorf("trade resumption unmarshal error: %w", err)
   783  	}
   784  
   785  	// Ensure the provided market exists for the dex.
   786  	if dc.marketConfig(rs.MarketID) == nil {
   787  		return fmt.Errorf("no market at %v found with ID %s", dc.acct.host, rs.MarketID)
   788  	}
   789  
   790  	// rs.ResumeTime == 0 means resume now.
   791  	if rs.ResumeTime != 0 {
   792  		// This is just a notice about a scheduled resumption.
   793  		dc.setMarketStartEpoch(rs.MarketID, rs.StartEpoch, false) // set the start epoch, leaving any final/persist data
   794  		resTime := time.UnixMilli(int64(rs.ResumeTime))
   795  		subject, detail := c.formatDetails(TopicMarketResumeScheduled, rs.MarketID, dc.acct.host, resTime)
   796  		c.notify(newServerNotifyNote(TopicMarketResumeScheduled, subject, detail, db.WarningLevel))
   797  		return nil
   798  	}
   799  
   800  	// Update the market's status and mark the new epoch.
   801  	dc.setMarketStartEpoch(rs.MarketID, rs.StartEpoch, true) // and clear the final/persist data
   802  	dc.epochMtx.Lock()
   803  	dc.epoch[rs.MarketID] = rs.StartEpoch
   804  	dc.epochMtx.Unlock()
   805  
   806  	// TODO: Server config change without restart is not implemented on the
   807  	// server, but it would involve either getting the config response or adding
   808  	// the entire market config to the TradeResumption payload.
   809  	//
   810  	// Fetch the updated DEX configuration.
   811  	// dc.refreshServerConfig()
   812  
   813  	subject, detail := c.formatDetails(TopicMarketResumed, rs.MarketID, dc.acct.host, rs.StartEpoch)
   814  	c.notify(newServerNotifyNote(TopicMarketResumed, subject, detail, db.Success))
   815  
   816  	// Book notes may resume at any time. Seq not set since no book changes.
   817  
   818  	return nil
   819  }
   820  
   821  func (dc *dexConnection) apiVersion() int32 {
   822  	return atomic.LoadInt32(&dc.apiVer)
   823  }
   824  
   825  // refreshServerConfig fetches and replaces server configuration data. It also
   826  // initially checks that a server's API version is one of serverAPIVers.
   827  func (dc *dexConnection) refreshServerConfig() (*msgjson.ConfigResult, error) {
   828  	// Fetch the updated DEX configuration.
   829  	cfg := new(msgjson.ConfigResult)
   830  	err := sendRequest(dc.WsConn, msgjson.ConfigRoute, nil, cfg, DefaultResponseTimeout)
   831  	if err != nil {
   832  		return nil, fmt.Errorf("unable to fetch server config: %w", err)
   833  	}
   834  
   835  	apiVer := int32(cfg.APIVersion)
   836  	dc.log.Infof("Server %v supports API version %v.", dc.acct.host, cfg.APIVersion)
   837  	atomic.StoreInt32(&dc.apiVer, apiVer)
   838  
   839  	// Check that we are able to communicate with this DEX.
   840  	var supported bool
   841  	for _, ver := range supportedAPIVers {
   842  		if apiVer == ver {
   843  			supported = true
   844  		}
   845  	}
   846  	if !supported {
   847  		err := fmt.Errorf("unsupported server API version %v", apiVer)
   848  		if apiVer > supportedAPIVers[len(supportedAPIVers)-1] {
   849  			err = fmt.Errorf("%v: %w", err, outdatedClientErr)
   850  		}
   851  		return nil, err
   852  	}
   853  
   854  	bTimeout := time.Millisecond * time.Duration(cfg.BroadcastTimeout)
   855  	tickInterval := bTimeout / tickCheckDivisions
   856  	dc.log.Debugf("Server %v broadcast timeout %v. Tick interval %v", dc.acct.host, bTimeout, tickInterval)
   857  	if dc.ticker.Dur() != tickInterval {
   858  		dc.ticker.Reset(tickInterval)
   859  	}
   860  
   861  	// Update the dex connection with the new config details, including
   862  	// StartEpoch and FinalEpoch, and rebuild the market data maps.
   863  	dc.cfgMtx.Lock()
   864  	defer dc.cfgMtx.Unlock()
   865  	dc.cfg = cfg
   866  
   867  	assets, epochs, err := generateDEXMaps(dc.acct.host, cfg)
   868  	if err != nil {
   869  		return nil, fmt.Errorf("inconsistent 'config' response: %w", err)
   870  	}
   871  
   872  	// Update dc.{epoch,assets}
   873  	dc.assetsMtx.Lock()
   874  	dc.assets = assets
   875  	dc.assetsMtx.Unlock()
   876  
   877  	// If we're fetching config and the server sends the pubkey in config, set
   878  	// the dexPubKey now. We also know that we're fetching the config for the
   879  	// first time (via connectDEX), and the dexConnection has not been assigned
   880  	// to dc.conns yet, so we can still update the acct.dexPubKey field without
   881  	// a data race.
   882  	if dc.acct.dexPubKey == nil && len(cfg.DEXPubKey) > 0 {
   883  		dc.acct.dexPubKey, err = secp256k1.ParsePubKey(cfg.DEXPubKey)
   884  		if err != nil {
   885  			return nil, fmt.Errorf("error decoding secp256k1 PublicKey from bytes: %w", err)
   886  		}
   887  	}
   888  
   889  	dc.epochMtx.Lock()
   890  	dc.epoch = epochs
   891  	dc.resolvedEpoch = utils.CopyMap(epochs)
   892  	dc.epochMtx.Unlock()
   893  
   894  	return cfg, nil
   895  }
   896  
   897  // subPriceFeed subscribes to the price_feed notification feed and primes the
   898  // initial prices.
   899  func (dc *dexConnection) subPriceFeed() {
   900  	var spots map[string]*msgjson.Spot
   901  	err := sendRequest(dc.WsConn, msgjson.PriceFeedRoute, nil, &spots, DefaultResponseTimeout)
   902  	if err != nil {
   903  		var msgErr *msgjson.Error
   904  		// Ignore old servers' errors.
   905  		if !errors.As(err, &msgErr) || msgErr.Code != msgjson.UnknownMessageType {
   906  			dc.log.Errorf("subPriceFeed: unable to fetch market overview: %v", err)
   907  		}
   908  		return
   909  	}
   910  
   911  	// We expect there to be a map in handlePriceUpdateNote.
   912  	spotsCopy := make(map[string]*msgjson.Spot, len(spots))
   913  	for mkt, spot := range spots {
   914  		spotsCopy[mkt] = spot
   915  	}
   916  	dc.notify(newSpotPriceNote(dc.acct.host, spotsCopy)) // consumers read spotsCopy async with this call
   917  
   918  	dc.spotsMtx.Lock()
   919  	dc.spots = spots
   920  	dc.spotsMtx.Unlock()
   921  }
   922  
   923  // handlePriceUpdateNote handles the price_update note that is part of the
   924  // price feed.
   925  func handlePriceUpdateNote(c *Core, dc *dexConnection, msg *msgjson.Message) error {
   926  	spot := new(msgjson.Spot)
   927  	if err := msg.Unmarshal(spot); err != nil {
   928  		return fmt.Errorf("error unmarshaling price update: %v", err)
   929  	}
   930  	mktName, err := dex.MarketName(spot.BaseID, spot.QuoteID)
   931  	if err != nil {
   932  		return nil // it's just an asset we don't support, don't insert, but don't spam
   933  	}
   934  	dc.spotsMtx.Lock()
   935  	dc.spots[mktName] = spot
   936  	dc.spotsMtx.Unlock()
   937  
   938  	dc.notify(newSpotPriceNote(dc.acct.host, map[string]*msgjson.Spot{mktName: spot}))
   939  	return nil
   940  }
   941  
   942  // handleUnbookOrderMsg is called when an unbook_order notification is
   943  // received.
   944  func handleUnbookOrderMsg(_ *Core, dc *dexConnection, msg *msgjson.Message) error {
   945  	note := new(msgjson.UnbookOrderNote)
   946  	err := msg.Unmarshal(note)
   947  	if err != nil {
   948  		return fmt.Errorf("unbook order note unmarshal error: %w", err)
   949  	}
   950  
   951  	book := dc.bookie(note.MarketID)
   952  	if book == nil {
   953  		return fmt.Errorf("no order book found with market id %q",
   954  			note.MarketID)
   955  	}
   956  	err = book.Unbook(note)
   957  	if err != nil {
   958  		return err
   959  	}
   960  	book.send(&BookUpdate{
   961  		Action:   UnbookOrderAction,
   962  		Host:     dc.acct.host,
   963  		MarketID: note.MarketID,
   964  		Payload:  &MiniOrder{Token: token(note.OrderID)},
   965  	})
   966  
   967  	return nil
   968  }
   969  
   970  // handleUpdateRemainingMsg is called when an update_remaining notification is
   971  // received.
   972  func handleUpdateRemainingMsg(_ *Core, dc *dexConnection, msg *msgjson.Message) error {
   973  	note := new(msgjson.UpdateRemainingNote)
   974  	err := msg.Unmarshal(note)
   975  	if err != nil {
   976  		return fmt.Errorf("book order note unmarshal error: %w", err)
   977  	}
   978  
   979  	book := dc.bookie(note.MarketID)
   980  	if book == nil {
   981  		return fmt.Errorf("no order book found with market id '%v'",
   982  			note.MarketID)
   983  	}
   984  	err = book.UpdateRemaining(note)
   985  	if err != nil {
   986  		return err
   987  	}
   988  	book.send(&BookUpdate{
   989  		Action:   UpdateRemainingAction,
   990  		Host:     dc.acct.host,
   991  		MarketID: note.MarketID,
   992  		Payload: &RemainderUpdate{
   993  			Token:     token(note.OrderID),
   994  			Qty:       float64(note.Remaining) / float64(book.baseUnits.Conventional.ConversionFactor),
   995  			QtyAtomic: note.Remaining,
   996  		},
   997  	})
   998  	return nil
   999  }
  1000  
  1001  // handleEpochReportMsg is called when an epoch_report notification is received.
  1002  func handleEpochReportMsg(c *Core, dc *dexConnection, msg *msgjson.Message) error {
  1003  	note := new(msgjson.EpochReportNote)
  1004  	err := msg.Unmarshal(note)
  1005  	if err != nil {
  1006  		return fmt.Errorf("epoch report note unmarshal error: %w", err)
  1007  	}
  1008  	book := dc.bookie(note.MarketID)
  1009  	if book == nil {
  1010  		return fmt.Errorf("no order book found with market id '%v'",
  1011  			note.MarketID)
  1012  	}
  1013  	err = book.logEpochReport(note)
  1014  	if err != nil {
  1015  		return fmt.Errorf("error logging epoch report: %w", err)
  1016  	}
  1017  	c.checkEpochResolution(dc.acct.host, note.MarketID)
  1018  	return nil
  1019  }
  1020  
  1021  // handleEpochOrderMsg is called when an epoch_order notification is
  1022  // received.
  1023  func handleEpochOrderMsg(_ *Core, dc *dexConnection, msg *msgjson.Message) error {
  1024  	note := new(msgjson.EpochOrderNote)
  1025  	err := msg.Unmarshal(note)
  1026  	if err != nil {
  1027  		return fmt.Errorf("epoch order note unmarshal error: %w", err)
  1028  	}
  1029  
  1030  	book := dc.bookie(note.MarketID)
  1031  	if book == nil {
  1032  		return fmt.Errorf("no order book found with market id %q",
  1033  			note.MarketID)
  1034  	}
  1035  
  1036  	err = book.Enqueue(note)
  1037  	if err != nil {
  1038  		return fmt.Errorf("failed to Enqueue epoch order: %w", err)
  1039  	}
  1040  
  1041  	// Send a MiniOrder for book updates.
  1042  	book.send(&BookUpdate{
  1043  		Action:   EpochOrderAction,
  1044  		Host:     dc.acct.host,
  1045  		MarketID: note.MarketID,
  1046  		Payload:  book.minifyOrder(note.OrderID, &note.TradeNote, note.Epoch),
  1047  	})
  1048  
  1049  	return nil
  1050  }