decred.org/dcrdex@v1.0.3/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.Warnf("bookie %p: Closing book update feed %d with no receiver. "+
   356  				"The receiver should have closed the feed before going away.", b, fid)
   357  			go b.closeFeed(feed.id) // delete it and maybe start a delayed bookie close
   358  		}
   359  	}
   360  }
   361  
   362  // book returns the bookie's current order book.
   363  func (b *bookie) book() *OrderBook {
   364  	buys, sells, epoch := b.Orders()
   365  	return &OrderBook{
   366  		Buys:          b.translateBookSide(buys),
   367  		Sells:         b.translateBookSide(sells),
   368  		Epoch:         b.translateBookSide(epoch),
   369  		RecentMatches: b.RecentMatches(),
   370  	}
   371  }
   372  
   373  // minifyOrder creates a MiniOrder from a TradeNote. The epoch and order ID must
   374  // be supplied.
   375  func (b *bookie) minifyOrder(oid dex.Bytes, trade *msgjson.TradeNote, epoch uint64) *MiniOrder {
   376  	return &MiniOrder{
   377  		Qty:       float64(trade.Quantity) / float64(b.baseUnits.Conventional.ConversionFactor),
   378  		QtyAtomic: trade.Quantity,
   379  		Rate:      calc.ConventionalRate(trade.Rate, b.baseUnits, b.quoteUnits),
   380  		MsgRate:   trade.Rate,
   381  		Sell:      trade.Side == msgjson.SellOrderNum,
   382  		Token:     token(oid),
   383  		Epoch:     epoch,
   384  	}
   385  }
   386  
   387  // bookie gets the bookie for the market, if it exists, else nil.
   388  func (dc *dexConnection) bookie(marketID string) *bookie {
   389  	dc.booksMtx.RLock()
   390  	defer dc.booksMtx.RUnlock()
   391  	return dc.books[marketID]
   392  }
   393  
   394  func (dc *dexConnection) midGap(base, quote uint32) (midGap uint64, err error) {
   395  	marketID := marketName(base, quote)
   396  	booky := dc.bookie(marketID)
   397  	if booky == nil {
   398  		return 0, fmt.Errorf("no bookie found for market %s", marketID)
   399  	}
   400  
   401  	return booky.MidGap()
   402  }
   403  
   404  // syncBook subscribes to the order book and returns the book and a BookFeed to
   405  // receive order book updates. The BookFeed must be Close()d when it is no
   406  // longer in use. Use stopBook to unsubscribed and clean up the feed.
   407  func (dc *dexConnection) syncBook(base, quote uint32) (*orderbook.OrderBook, BookFeed, error) {
   408  	dc.cfgMtx.RLock()
   409  	cfg := dc.cfg
   410  	dc.cfgMtx.RUnlock()
   411  
   412  	dc.booksMtx.Lock()
   413  	defer dc.booksMtx.Unlock()
   414  
   415  	mktID := marketName(base, quote)
   416  	booky, found := dc.books[mktID]
   417  	if !found {
   418  		// Make sure the market exists.
   419  		if dc.marketConfig(mktID) == nil {
   420  			return nil, nil, fmt.Errorf("unknown market %s", mktID)
   421  		}
   422  
   423  		obRes, err := dc.subscribe(base, quote)
   424  		if err != nil {
   425  			return nil, nil, err
   426  		}
   427  
   428  		booky = newBookie(dc, base, quote, cfg.BinSizes, dc.log.SubLogger(mktID))
   429  		err = booky.Sync(obRes)
   430  		if err != nil {
   431  			return nil, nil, err
   432  		}
   433  		dc.books[mktID] = booky
   434  	}
   435  
   436  	// Get the feed and the book under a single lock to make sure the first
   437  	// message is the book.
   438  	feed := booky.newFeed(&BookUpdate{
   439  		Action:   FreshBookAction,
   440  		Host:     dc.acct.host,
   441  		MarketID: mktID,
   442  		Payload: &MarketOrderBook{
   443  			Base:  base,
   444  			Quote: quote,
   445  			Book:  booky.book(),
   446  		},
   447  	})
   448  
   449  	return booky.OrderBook, feed, nil
   450  }
   451  
   452  // subscribe subscribes to the given market's order book via the 'orderbook'
   453  // request. The response, which includes book's snapshot, is returned. Proper
   454  // synchronization is required by the caller to ensure that order feed messages
   455  // aren't processed before they are prepared to handle this subscription.
   456  func (dc *dexConnection) subscribe(baseID, quoteID uint32) (*msgjson.OrderBook, error) {
   457  	mkt := marketName(baseID, quoteID)
   458  	// Subscribe via the 'orderbook' request.
   459  	dc.log.Debugf("Subscribing to the %v order book for %v", mkt, dc.acct.host)
   460  	req, err := msgjson.NewRequest(dc.NextID(), msgjson.OrderBookRoute, &msgjson.OrderBookSubscription{
   461  		Base:  baseID,
   462  		Quote: quoteID,
   463  	})
   464  	if err != nil {
   465  		return nil, fmt.Errorf("error encoding 'orderbook' request: %w", err)
   466  	}
   467  	errChan := make(chan error, 1)
   468  	result := new(msgjson.OrderBook)
   469  	err = dc.RequestWithTimeout(req, func(msg *msgjson.Message) {
   470  		errChan <- msg.UnmarshalResult(result)
   471  	}, DefaultResponseTimeout, func() {
   472  		errChan <- fmt.Errorf("timed out waiting for '%s' response", msgjson.OrderBookRoute)
   473  	})
   474  	if err != nil {
   475  		return nil, fmt.Errorf("error subscribing to %s orderbook: %w", mkt, err)
   476  	}
   477  	err = <-errChan
   478  	if err != nil {
   479  		return nil, err
   480  	}
   481  	return result, nil
   482  }
   483  
   484  // stopBook is the close callback passed to the bookie, and will be called when
   485  // there are no more subscribers and the close delay period has expired.
   486  func (dc *dexConnection) stopBook(base, quote uint32) {
   487  	mkt := marketName(base, quote)
   488  	dc.booksMtx.Lock()
   489  	defer dc.booksMtx.Unlock() // hold it locked until unsubscribe request is completed
   490  
   491  	// Abort the unsubscribe if feeds exist for the bookie. This can happen if a
   492  	// bookie's close func is called while a new BookFeed is generated elsewhere.
   493  	if booky, found := dc.books[mkt]; found {
   494  		booky.feedsMtx.Lock()
   495  		numFeeds := len(booky.feeds)
   496  		booky.feedsMtx.Unlock()
   497  		if numFeeds > 0 {
   498  			dc.log.Warnf("Aborting booky %p unsubscribe for market %s with active feeds", booky, mkt)
   499  			return
   500  		}
   501  		// No BookFeeds, delete the bookie.
   502  		delete(dc.books, mkt)
   503  	}
   504  
   505  	if err := dc.unsubscribe(base, quote); err != nil {
   506  		dc.log.Error(err)
   507  	}
   508  }
   509  
   510  // unsubscribe unsubscribes from to the given market's order book.
   511  func (dc *dexConnection) unsubscribe(base, quote uint32) error {
   512  	mkt := marketName(base, quote)
   513  	dc.log.Debugf("Unsubscribing from the %v order book for %v", mkt, dc.acct.host)
   514  	req, err := msgjson.NewRequest(dc.NextID(), msgjson.UnsubOrderBookRoute, &msgjson.UnsubOrderBook{
   515  		MarketID: mkt,
   516  	})
   517  	if err != nil {
   518  		return fmt.Errorf("unsub_orderbook message encoding error: %w", err)
   519  	}
   520  	// Request to unsubscribe. NOTE: does not wait for response.
   521  	err = dc.Request(req, func(msg *msgjson.Message) {
   522  		var res bool
   523  		_ = msg.UnmarshalResult(&res) // res==false if unmarshal fails
   524  		if !res {
   525  			dc.log.Errorf("error unsubscribing from %s", mkt)
   526  		}
   527  	})
   528  	if err != nil {
   529  		return fmt.Errorf("request error unsubscribing from %s orderbook: %w", mkt, err)
   530  	}
   531  	return nil
   532  }
   533  
   534  // SyncBook subscribes to the order book and returns the book and a BookFeed to
   535  // receive order book updates. The BookFeed must be Close()d when it is no
   536  // longer in use.
   537  func (c *Core) SyncBook(host string, base, quote uint32) (*orderbook.OrderBook, BookFeed, error) {
   538  	c.connMtx.RLock()
   539  	dc, found := c.conns[host]
   540  	c.connMtx.RUnlock()
   541  	if !found {
   542  		return nil, nil, fmt.Errorf("unknown DEX '%s'", host)
   543  	}
   544  
   545  	return dc.syncBook(base, quote)
   546  }
   547  
   548  // Book fetches the order book. If a subscription doesn't exist, one will be
   549  // attempted and immediately closed.
   550  func (c *Core) Book(dex string, base, quote uint32) (*OrderBook, error) {
   551  	dex, err := addrHost(dex)
   552  	if err != nil {
   553  		return nil, newError(addressParseErr, "error parsing address: %w", err)
   554  	}
   555  	c.connMtx.RLock()
   556  	dc, found := c.conns[dex]
   557  	c.connMtx.RUnlock()
   558  	if !found {
   559  		return nil, fmt.Errorf("no DEX %s", dex)
   560  	}
   561  
   562  	mkt := marketName(base, quote)
   563  	dc.booksMtx.RLock()
   564  	defer dc.booksMtx.RUnlock() // hold it locked until any transient sub/unsub is completed
   565  	book, found := dc.books[mkt]
   566  	// If not found, attempt to make a temporary subscription and return the
   567  	// initial book.
   568  	if !found {
   569  		snap, err := dc.subscribe(base, quote)
   570  		if err != nil {
   571  			return nil, fmt.Errorf("unable to subscribe to book: %w", err)
   572  		}
   573  		err = dc.unsubscribe(base, quote)
   574  		if err != nil {
   575  			c.log.Errorf("Failed to unsubscribe to %q book: %v", mkt, err)
   576  		}
   577  
   578  		dc.cfgMtx.RLock()
   579  		cfg := dc.cfg
   580  		dc.cfgMtx.RUnlock()
   581  
   582  		book = newBookie(dc, base, quote, cfg.BinSizes, dc.log.SubLogger(mkt))
   583  		if err = book.Sync(snap); err != nil {
   584  			return nil, fmt.Errorf("unable to sync book: %w", err)
   585  		}
   586  	}
   587  
   588  	buys, sells, epoch := book.OrderBook.Orders()
   589  	return &OrderBook{
   590  		Buys:  book.translateBookSide(buys),
   591  		Sells: book.translateBookSide(sells),
   592  		Epoch: book.translateBookSide(epoch),
   593  	}, nil
   594  }
   595  
   596  // translateBookSide translates from []*orderbook.Order to []*MiniOrder.
   597  func (b *bookie) translateBookSide(ins []*orderbook.Order) (outs []*MiniOrder) {
   598  	for _, o := range ins {
   599  		outs = append(outs, &MiniOrder{
   600  			Qty:       float64(o.Quantity) / float64(b.baseUnits.Conventional.ConversionFactor),
   601  			QtyAtomic: o.Quantity,
   602  			Rate:      calc.ConventionalRate(o.Rate, b.baseUnits, b.quoteUnits),
   603  			MsgRate:   o.Rate,
   604  			Sell:      o.Side == msgjson.SellOrderNum,
   605  			Token:     token(o.OrderID[:]),
   606  			Epoch:     o.Epoch,
   607  		})
   608  	}
   609  	return
   610  }
   611  
   612  // handleBookOrderMsg is called when a book_order notification is received.
   613  func handleBookOrderMsg(_ *Core, dc *dexConnection, msg *msgjson.Message) error {
   614  	note := new(msgjson.BookOrderNote)
   615  	err := msg.Unmarshal(note)
   616  	if err != nil {
   617  		return fmt.Errorf("book order note unmarshal error: %w", err)
   618  	}
   619  
   620  	book := dc.bookie(note.MarketID)
   621  	if book == nil {
   622  		return fmt.Errorf("no order book found with market id '%v'",
   623  			note.MarketID)
   624  	}
   625  	err = book.Book(note)
   626  	if err != nil {
   627  		return err
   628  	}
   629  	book.send(&BookUpdate{
   630  		Action:   BookOrderAction,
   631  		Host:     dc.acct.host,
   632  		MarketID: note.MarketID,
   633  		Payload:  book.minifyOrder(note.OrderID, &note.TradeNote, 0),
   634  	})
   635  	return nil
   636  }
   637  
   638  // findMarketConfig searches the stored ConfigResponse for the named market.
   639  // This must be called with cfgMtx at least read locked.
   640  func (dc *dexConnection) findMarketConfig(name string) *msgjson.Market {
   641  	if dc.cfg == nil {
   642  		return nil
   643  	}
   644  	for _, mktConf := range dc.cfg.Markets {
   645  		if mktConf.Name == name {
   646  			return mktConf
   647  		}
   648  	}
   649  	return nil
   650  }
   651  
   652  // setMarketStartEpoch revises the StartEpoch field of the named market in the
   653  // stored ConfigResponse. It optionally zeros FinalEpoch and Persist, which
   654  // should only be done at start time.
   655  func (dc *dexConnection) setMarketStartEpoch(name string, startEpoch uint64, clearFinal bool) {
   656  	dc.cfgMtx.Lock()
   657  	defer dc.cfgMtx.Unlock()
   658  	mkt := dc.findMarketConfig(name)
   659  	if mkt == nil {
   660  		return
   661  	}
   662  	mkt.StartEpoch = startEpoch
   663  	// NOTE: should only clear these if starting now.
   664  	if clearFinal {
   665  		mkt.FinalEpoch = 0
   666  		mkt.Persist = nil
   667  	}
   668  }
   669  
   670  // setMarketFinalEpoch revises the FinalEpoch and Persist fields of the named
   671  // market in the stored ConfigResponse.
   672  func (dc *dexConnection) setMarketFinalEpoch(name string, finalEpoch uint64, persist bool) {
   673  	dc.cfgMtx.Lock()
   674  	defer dc.cfgMtx.Unlock()
   675  	mkt := dc.findMarketConfig(name)
   676  	if mkt == nil {
   677  		return
   678  	}
   679  	mkt.FinalEpoch = finalEpoch
   680  	mkt.Persist = &persist
   681  }
   682  
   683  // handleTradeSuspensionMsg is called when a trade suspension notification is
   684  // received. This message may come in advance of suspension, in which case it
   685  // has a SuspendTime set, or at the time of suspension if subscribed to the
   686  // order book, in which case it has a Seq value set.
   687  func handleTradeSuspensionMsg(c *Core, dc *dexConnection, msg *msgjson.Message) error {
   688  	var sp msgjson.TradeSuspension
   689  	err := msg.Unmarshal(&sp)
   690  	if err != nil {
   691  		return fmt.Errorf("trade suspension unmarshal error: %w", err)
   692  	}
   693  
   694  	// Ensure the provided market exists for the dex.
   695  	mkt := dc.marketConfig(sp.MarketID)
   696  	if mkt == nil {
   697  		return fmt.Errorf("no market found with ID %s", sp.MarketID)
   698  	}
   699  
   700  	// Update the data in the stored ConfigResponse.
   701  	dc.setMarketFinalEpoch(sp.MarketID, sp.FinalEpoch, sp.Persist)
   702  
   703  	// SuspendTime == 0 means suspending now.
   704  	if sp.SuspendTime != 0 {
   705  		// This is just a warning about a scheduled suspension.
   706  		suspendTime := time.UnixMilli(int64(sp.SuspendTime))
   707  		subject, detail := c.formatDetails(TopicMarketSuspendScheduled, sp.MarketID, dc.acct.host, suspendTime)
   708  		c.notify(newServerNotifyNote(TopicMarketSuspendScheduled, subject, detail, db.WarningLevel))
   709  		return nil
   710  	}
   711  
   712  	topic := TopicMarketSuspended
   713  	if !sp.Persist {
   714  		topic = TopicMarketSuspendedWithPurge
   715  	}
   716  	subject, detail := c.formatDetails(topic, sp.MarketID, dc.acct.host)
   717  	c.notify(newServerNotifyNote(topic, subject, detail, db.WarningLevel))
   718  
   719  	if sp.Persist {
   720  		// No book changes. Just wait for more order notes.
   721  		return nil
   722  	}
   723  
   724  	// Clear the book and unbook/revoke own orders.
   725  	book := dc.bookie(sp.MarketID)
   726  	if book == nil {
   727  		return fmt.Errorf("no order book found with market id '%s'", sp.MarketID)
   728  	}
   729  
   730  	err = book.Reset(&msgjson.OrderBook{
   731  		MarketID: sp.MarketID,
   732  		Seq:      sp.Seq,        // forces seq reset, but should be in seq with previous
   733  		Epoch:    sp.FinalEpoch, // unused?
   734  		// Orders is nil
   735  		// BaseFeeRate = QuoteFeeRate = 0 effectively disables the book's fee
   736  		// cache until an update is received, since bestBookFeeSuggestion
   737  		// ignores zeros.
   738  	})
   739  	// Return any non-nil error, but still revoke purged orders.
   740  
   741  	// Revoke all active orders of the suspended market for the dex.
   742  	c.log.Warnf("Revoking all active orders for market %s at %s.", sp.MarketID, dc.acct.host)
   743  	updatedAssets := make(assetMap)
   744  	dc.tradeMtx.RLock()
   745  	for _, tracker := range dc.trades {
   746  		if tracker.Order.Base() == mkt.Base && tracker.Order.Quote() == mkt.Quote &&
   747  			tracker.metaData.Host == dc.acct.host && tracker.status() == order.OrderStatusBooked {
   748  			// Locally revoke the purged book order.
   749  			tracker.revoke()
   750  			subject, details := c.formatDetails(TopicOrderAutoRevoked, tracker.token(), sp.MarketID, dc.acct.host)
   751  			c.notify(newOrderNote(TopicOrderAutoRevoked, subject, details, db.WarningLevel, tracker.coreOrder()))
   752  			updatedAssets.count(tracker.fromAssetID)
   753  		}
   754  	}
   755  	dc.tradeMtx.RUnlock()
   756  
   757  	// Clear the book.
   758  	book.send(&BookUpdate{
   759  		Action:   FreshBookAction,
   760  		Host:     dc.acct.host,
   761  		MarketID: sp.MarketID,
   762  		Payload: &MarketOrderBook{
   763  			Base:  mkt.Base,
   764  			Quote: mkt.Quote,
   765  			Book:  book.book(), // empty
   766  		},
   767  	})
   768  
   769  	if len(updatedAssets) > 0 {
   770  		c.updateBalances(updatedAssets)
   771  	}
   772  
   773  	return err
   774  }
   775  
   776  // handleTradeResumptionMsg is called when a trade resumption notification is
   777  // received. This may be an orderbook message at the time of resumption, or a
   778  // notification of a newly-schedule resumption while the market is suspended
   779  // (prior to the market resume).
   780  func handleTradeResumptionMsg(c *Core, dc *dexConnection, msg *msgjson.Message) error {
   781  	var rs msgjson.TradeResumption
   782  	err := msg.Unmarshal(&rs)
   783  	if err != nil {
   784  		return fmt.Errorf("trade resumption unmarshal error: %w", err)
   785  	}
   786  
   787  	// Ensure the provided market exists for the dex.
   788  	if dc.marketConfig(rs.MarketID) == nil {
   789  		return fmt.Errorf("no market at %v found with ID %s", dc.acct.host, rs.MarketID)
   790  	}
   791  
   792  	// rs.ResumeTime == 0 means resume now.
   793  	if rs.ResumeTime != 0 {
   794  		// This is just a notice about a scheduled resumption.
   795  		dc.setMarketStartEpoch(rs.MarketID, rs.StartEpoch, false) // set the start epoch, leaving any final/persist data
   796  		resTime := time.UnixMilli(int64(rs.ResumeTime))
   797  		subject, detail := c.formatDetails(TopicMarketResumeScheduled, rs.MarketID, dc.acct.host, resTime)
   798  		c.notify(newServerNotifyNote(TopicMarketResumeScheduled, subject, detail, db.WarningLevel))
   799  		return nil
   800  	}
   801  
   802  	// Update the market's status and mark the new epoch.
   803  	dc.setMarketStartEpoch(rs.MarketID, rs.StartEpoch, true) // and clear the final/persist data
   804  	dc.epochMtx.Lock()
   805  	dc.epoch[rs.MarketID] = rs.StartEpoch
   806  	dc.epochMtx.Unlock()
   807  
   808  	// TODO: Server config change without restart is not implemented on the
   809  	// server, but it would involve either getting the config response or adding
   810  	// the entire market config to the TradeResumption payload.
   811  	//
   812  	// Fetch the updated DEX configuration.
   813  	// dc.refreshServerConfig()
   814  
   815  	subject, detail := c.formatDetails(TopicMarketResumed, rs.MarketID, dc.acct.host, rs.StartEpoch)
   816  	c.notify(newServerNotifyNote(TopicMarketResumed, subject, detail, db.Success))
   817  
   818  	// Book notes may resume at any time. Seq not set since no book changes.
   819  
   820  	return nil
   821  }
   822  
   823  func (dc *dexConnection) apiVersion() int32 {
   824  	return atomic.LoadInt32(&dc.apiVer)
   825  }
   826  
   827  // refreshServerConfig fetches and replaces server configuration data. It also
   828  // initially checks that a server's API version is one of serverAPIVers.
   829  func (dc *dexConnection) refreshServerConfig() (*msgjson.ConfigResult, error) {
   830  	// Fetch the updated DEX configuration.
   831  	cfg := new(msgjson.ConfigResult)
   832  	err := sendRequest(dc.WsConn, msgjson.ConfigRoute, nil, cfg, DefaultResponseTimeout)
   833  	if err != nil {
   834  		return nil, fmt.Errorf("unable to fetch server config: %w", err)
   835  	}
   836  
   837  	apiVer := int32(cfg.APIVersion)
   838  	dc.log.Infof("Server %v supports API version %v.", dc.acct.host, cfg.APIVersion)
   839  	atomic.StoreInt32(&dc.apiVer, apiVer)
   840  
   841  	// Check that we are able to communicate with this DEX.
   842  	var supported bool
   843  	for _, ver := range supportedAPIVers {
   844  		if apiVer == ver {
   845  			supported = true
   846  		}
   847  	}
   848  	if !supported {
   849  		err := fmt.Errorf("unsupported server API version %v", apiVer)
   850  		if apiVer > supportedAPIVers[len(supportedAPIVers)-1] {
   851  			err = fmt.Errorf("%v: %w", err, outdatedClientErr)
   852  		}
   853  		return nil, err
   854  	}
   855  
   856  	bTimeout := time.Millisecond * time.Duration(cfg.BroadcastTimeout)
   857  	tickInterval := bTimeout / tickCheckDivisions
   858  	dc.log.Debugf("Server %v broadcast timeout %v. Tick interval %v", dc.acct.host, bTimeout, tickInterval)
   859  	if dc.ticker.Dur() != tickInterval {
   860  		dc.ticker.Reset(tickInterval)
   861  	}
   862  
   863  	// Update the dex connection with the new config details, including
   864  	// StartEpoch and FinalEpoch, and rebuild the market data maps.
   865  	dc.cfgMtx.Lock()
   866  	defer dc.cfgMtx.Unlock()
   867  	dc.cfg = cfg
   868  
   869  	assets, epochs, err := generateDEXMaps(dc.acct.host, cfg)
   870  	if err != nil {
   871  		return nil, fmt.Errorf("inconsistent 'config' response: %w", err)
   872  	}
   873  
   874  	// Update dc.{epoch,assets}
   875  	dc.assetsMtx.Lock()
   876  	dc.assets = assets
   877  	dc.assetsMtx.Unlock()
   878  
   879  	// If we're fetching config and the server sends the pubkey in config, set
   880  	// the dexPubKey now. We also know that we're fetching the config for the
   881  	// first time (via connectDEX), and the dexConnection has not been assigned
   882  	// to dc.conns yet, so we can still update the acct.dexPubKey field without
   883  	// a data race.
   884  	if dc.acct.dexPubKey == nil && len(cfg.DEXPubKey) > 0 {
   885  		dc.acct.dexPubKey, err = secp256k1.ParsePubKey(cfg.DEXPubKey)
   886  		if err != nil {
   887  			return nil, fmt.Errorf("error decoding secp256k1 PublicKey from bytes: %w", err)
   888  		}
   889  	}
   890  
   891  	dc.epochMtx.Lock()
   892  	dc.epoch = epochs
   893  	dc.resolvedEpoch = utils.CopyMap(epochs)
   894  	dc.epochMtx.Unlock()
   895  
   896  	return cfg, nil
   897  }
   898  
   899  // subPriceFeed subscribes to the price_feed notification feed and primes the
   900  // initial prices.
   901  func (dc *dexConnection) subPriceFeed() {
   902  	var spots map[string]*msgjson.Spot
   903  	err := sendRequest(dc.WsConn, msgjson.PriceFeedRoute, nil, &spots, DefaultResponseTimeout)
   904  	if err != nil {
   905  		var msgErr *msgjson.Error
   906  		// Ignore old servers' errors.
   907  		if !errors.As(err, &msgErr) || msgErr.Code != msgjson.UnknownMessageType {
   908  			dc.log.Errorf("subPriceFeed: unable to fetch market overview: %v", err)
   909  		}
   910  		return
   911  	}
   912  
   913  	// We expect there to be a map in handlePriceUpdateNote.
   914  	spotsCopy := make(map[string]*msgjson.Spot, len(spots))
   915  	for mkt, spot := range spots {
   916  		spotsCopy[mkt] = spot
   917  	}
   918  	dc.notify(newSpotPriceNote(dc.acct.host, spotsCopy)) // consumers read spotsCopy async with this call
   919  
   920  	dc.spotsMtx.Lock()
   921  	dc.spots = spots
   922  	dc.spotsMtx.Unlock()
   923  }
   924  
   925  // handlePriceUpdateNote handles the price_update note that is part of the
   926  // price feed.
   927  func handlePriceUpdateNote(c *Core, dc *dexConnection, msg *msgjson.Message) error {
   928  	spot := new(msgjson.Spot)
   929  	if err := msg.Unmarshal(spot); err != nil {
   930  		return fmt.Errorf("error unmarshaling price update: %v", err)
   931  	}
   932  	mktName, err := dex.MarketName(spot.BaseID, spot.QuoteID)
   933  	if err != nil {
   934  		return nil // it's just an asset we don't support, don't insert, but don't spam
   935  	}
   936  	dc.spotsMtx.Lock()
   937  	dc.spots[mktName] = spot
   938  	dc.spotsMtx.Unlock()
   939  
   940  	dc.notify(newSpotPriceNote(dc.acct.host, map[string]*msgjson.Spot{mktName: spot}))
   941  	return nil
   942  }
   943  
   944  // handleUnbookOrderMsg is called when an unbook_order notification is
   945  // received.
   946  func handleUnbookOrderMsg(_ *Core, dc *dexConnection, msg *msgjson.Message) error {
   947  	note := new(msgjson.UnbookOrderNote)
   948  	err := msg.Unmarshal(note)
   949  	if err != nil {
   950  		return fmt.Errorf("unbook order note unmarshal error: %w", err)
   951  	}
   952  
   953  	book := dc.bookie(note.MarketID)
   954  	if book == nil {
   955  		return fmt.Errorf("no order book found with market id %q",
   956  			note.MarketID)
   957  	}
   958  	err = book.Unbook(note)
   959  	if err != nil {
   960  		return err
   961  	}
   962  	book.send(&BookUpdate{
   963  		Action:   UnbookOrderAction,
   964  		Host:     dc.acct.host,
   965  		MarketID: note.MarketID,
   966  		Payload:  &MiniOrder{Token: token(note.OrderID)},
   967  	})
   968  
   969  	return nil
   970  }
   971  
   972  // handleUpdateRemainingMsg is called when an update_remaining notification is
   973  // received.
   974  func handleUpdateRemainingMsg(_ *Core, dc *dexConnection, msg *msgjson.Message) error {
   975  	note := new(msgjson.UpdateRemainingNote)
   976  	err := msg.Unmarshal(note)
   977  	if err != nil {
   978  		return fmt.Errorf("book order note unmarshal error: %w", err)
   979  	}
   980  
   981  	book := dc.bookie(note.MarketID)
   982  	if book == nil {
   983  		return fmt.Errorf("no order book found with market id '%v'",
   984  			note.MarketID)
   985  	}
   986  	err = book.UpdateRemaining(note)
   987  	if err != nil {
   988  		return err
   989  	}
   990  	book.send(&BookUpdate{
   991  		Action:   UpdateRemainingAction,
   992  		Host:     dc.acct.host,
   993  		MarketID: note.MarketID,
   994  		Payload: &RemainderUpdate{
   995  			Token:     token(note.OrderID),
   996  			Qty:       float64(note.Remaining) / float64(book.baseUnits.Conventional.ConversionFactor),
   997  			QtyAtomic: note.Remaining,
   998  		},
   999  	})
  1000  	return nil
  1001  }
  1002  
  1003  // handleEpochReportMsg is called when an epoch_report notification is received.
  1004  func handleEpochReportMsg(c *Core, dc *dexConnection, msg *msgjson.Message) error {
  1005  	note := new(msgjson.EpochReportNote)
  1006  	err := msg.Unmarshal(note)
  1007  	if err != nil {
  1008  		return fmt.Errorf("epoch report note unmarshal error: %w", err)
  1009  	}
  1010  	book := dc.bookie(note.MarketID)
  1011  	if book == nil {
  1012  		return fmt.Errorf("no order book found with market id '%v'",
  1013  			note.MarketID)
  1014  	}
  1015  	err = book.logEpochReport(note)
  1016  	if err != nil {
  1017  		return fmt.Errorf("error logging epoch report: %w", err)
  1018  	}
  1019  	c.checkEpochResolution(dc.acct.host, note.MarketID)
  1020  	return nil
  1021  }
  1022  
  1023  // handleEpochOrderMsg is called when an epoch_order notification is
  1024  // received.
  1025  func handleEpochOrderMsg(_ *Core, dc *dexConnection, msg *msgjson.Message) error {
  1026  	note := new(msgjson.EpochOrderNote)
  1027  	err := msg.Unmarshal(note)
  1028  	if err != nil {
  1029  		return fmt.Errorf("epoch order note unmarshal error: %w", err)
  1030  	}
  1031  
  1032  	book := dc.bookie(note.MarketID)
  1033  	if book == nil {
  1034  		return fmt.Errorf("no order book found with market id %q",
  1035  			note.MarketID)
  1036  	}
  1037  
  1038  	err = book.Enqueue(note)
  1039  	if err != nil {
  1040  		return fmt.Errorf("failed to Enqueue epoch order: %w", err)
  1041  	}
  1042  
  1043  	// Send a MiniOrder for book updates.
  1044  	book.send(&BookUpdate{
  1045  		Action:   EpochOrderAction,
  1046  		Host:     dc.acct.host,
  1047  		MarketID: note.MarketID,
  1048  		Payload:  book.minifyOrder(note.OrderID, &note.TradeNote, note.Epoch),
  1049  	})
  1050  
  1051  	return nil
  1052  }