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(¬e.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, ¬e.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, ¬e.TradeNote, note.Epoch), 1049 }) 1050 1051 return nil 1052 }