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