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