decred.org/dcrdex@v1.0.3/client/mm/libxc/binance.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 libxc 5 6 import ( 7 "bytes" 8 "context" 9 "crypto/hmac" 10 "crypto/sha256" 11 "encoding/hex" 12 "encoding/json" 13 "errors" 14 "fmt" 15 "math" 16 "net/http" 17 "net/url" 18 "strconv" 19 "strings" 20 "sync" 21 "sync/atomic" 22 "time" 23 24 "decred.org/dcrdex/client/asset" 25 "decred.org/dcrdex/client/comms" 26 "decred.org/dcrdex/client/core" 27 "decred.org/dcrdex/client/mm/libxc/bntypes" 28 "decred.org/dcrdex/dex" 29 "decred.org/dcrdex/dex/calc" 30 "decred.org/dcrdex/dex/dexnet" 31 "decred.org/dcrdex/dex/encode" 32 "decred.org/dcrdex/dex/utils" 33 ) 34 35 // Binance API spot trading docs: 36 // https://binance-docs.github.io/apidocs/spot/en/#spot-account-trade 37 38 const ( 39 httpURL = "https://api.binance.com" 40 websocketURL = "wss://stream.binance.com:9443" 41 42 usHttpURL = "https://api.binance.us" 43 usWebsocketURL = "wss://stream.binance.us:9443" 44 45 testnetHttpURL = "https://testnet.binance.vision" 46 testnetWebsocketURL = "wss://testnet.binance.vision" 47 48 // sapi endpoints are not implemented by binance's test network. This url 49 // connects to the process at client/cmd/testbinance, which responds to the 50 // /sapi/v1/capital/config/getall endpoint. 51 fakeBinanceURL = "http://localhost:37346" 52 fakeBinanceWsURL = "ws://localhost:37346" 53 54 bnErrCodeInvalidListenKey = -1125 55 ) 56 57 // binanceOrderBook manages an orderbook for a single market. It keeps 58 // the orderbook synced and allows querying of vwap. 59 type binanceOrderBook struct { 60 mtx sync.RWMutex 61 synced atomic.Bool 62 syncChan chan struct{} 63 numSubscribers uint32 64 cm *dex.ConnectionMaster 65 66 getSnapshot func() (*bntypes.OrderbookSnapshot, error) 67 68 book *orderbook 69 updateQueue chan *bntypes.BookUpdate 70 mktID string 71 baseConversionFactor uint64 72 quoteConversionFactor uint64 73 log dex.Logger 74 75 connectedChan chan bool 76 } 77 78 func newBinanceOrderBook( 79 baseConversionFactor, quoteConversionFactor uint64, 80 mktID string, 81 getSnapshot func() (*bntypes.OrderbookSnapshot, error), 82 log dex.Logger, 83 ) *binanceOrderBook { 84 return &binanceOrderBook{ 85 book: newOrderBook(), 86 mktID: mktID, 87 updateQueue: make(chan *bntypes.BookUpdate, 1024), 88 numSubscribers: 1, 89 baseConversionFactor: baseConversionFactor, 90 quoteConversionFactor: quoteConversionFactor, 91 log: log, 92 getSnapshot: getSnapshot, 93 connectedChan: make(chan bool), 94 } 95 } 96 97 // convertBinanceBook converts bids and asks in the binance format, 98 // with the conventional quantity and rate, to the DEX message format which 99 // can be used to update the orderbook. 100 func (b *binanceOrderBook) convertBinanceBook(binanceBids, binanceAsks [][2]json.Number) (bids, asks []*obEntry, err error) { 101 convert := func(updates [][2]json.Number) ([]*obEntry, error) { 102 convertedUpdates := make([]*obEntry, 0, len(updates)) 103 104 for _, update := range updates { 105 price, err := update[0].Float64() 106 if err != nil { 107 return nil, fmt.Errorf("error parsing price: %v", err) 108 } 109 110 qty, err := update[1].Float64() 111 if err != nil { 112 return nil, fmt.Errorf("error parsing qty: %v", err) 113 } 114 115 convertedUpdates = append(convertedUpdates, &obEntry{ 116 rate: calc.MessageRateAlt(price, b.baseConversionFactor, b.quoteConversionFactor), 117 qty: uint64(qty * float64(b.baseConversionFactor)), 118 }) 119 } 120 121 return convertedUpdates, nil 122 } 123 124 bids, err = convert(binanceBids) 125 if err != nil { 126 return nil, nil, err 127 } 128 129 asks, err = convert(binanceAsks) 130 if err != nil { 131 return nil, nil, err 132 } 133 134 return bids, asks, nil 135 } 136 137 // sync does an initial sync of the orderbook. When the first update is 138 // received, it grabs a snapshot of the orderbook, and only processes updates 139 // that come after the state of the snapshot. A goroutine is started that keeps 140 // the orderbook in sync by repeating the sync process if an update is ever 141 // missed. 142 // 143 // This function runs until the context is canceled. It must be started as 144 // a new goroutine. 145 func (b *binanceOrderBook) sync(ctx context.Context) { 146 cm := dex.NewConnectionMaster(b) 147 b.mtx.Lock() 148 b.cm = cm 149 b.mtx.Unlock() 150 if err := cm.ConnectOnce(ctx); err != nil { 151 b.log.Errorf("Error connecting %s order book: %v", b.mktID, err) 152 } 153 <-b.syncChan 154 } 155 156 func (b *binanceOrderBook) Connect(ctx context.Context) (*sync.WaitGroup, error /* no errors */) { 157 const updateIDUnsynced = math.MaxUint64 158 159 // We'll run two goroutines and synchronize two local vars. 160 var syncMtx sync.Mutex 161 var syncCache []*bntypes.BookUpdate 162 syncChan := make(chan struct{}) 163 b.syncChan = syncChan 164 var updateID uint64 = updateIDUnsynced 165 acceptedUpdate := false 166 167 resyncChan := make(chan struct{}, 1) 168 169 desync := func(resync bool) { 170 // clear the sync cache, set the special ID, trigger a book refresh. 171 syncMtx.Lock() 172 defer syncMtx.Unlock() 173 syncCache = make([]*bntypes.BookUpdate, 0) 174 acceptedUpdate = false 175 if updateID != updateIDUnsynced { 176 b.synced.Store(false) 177 updateID = updateIDUnsynced 178 if resync { 179 resyncChan <- struct{}{} 180 } 181 } 182 } 183 184 acceptUpdate := func(update *bntypes.BookUpdate) bool { 185 if updateID == updateIDUnsynced { 186 // Book is still syncing. Add it to the sync cache. 187 syncCache = append(syncCache, update) 188 return true 189 } 190 191 if !acceptedUpdate { 192 // On the first update we receive, the update may straddle the last 193 // update ID of the snapshot. If the first update ID is greater 194 // than the snapshot ID + 1, it means we missed something so we 195 // must resync. If the last update ID is less than or equal to the 196 // snapshot ID, we can ignore it. 197 if update.FirstUpdateID > updateID+1 { 198 return false 199 } else if update.LastUpdateID <= updateID { 200 return true 201 } 202 // Once we've accepted the first update, the updates must be in 203 // sequence. 204 } else if update.FirstUpdateID != updateID+1 { 205 return false 206 } 207 208 acceptedUpdate = true 209 updateID = update.LastUpdateID 210 bids, asks, err := b.convertBinanceBook(update.Bids, update.Asks) 211 if err != nil { 212 b.log.Errorf("Error parsing binance book: %v", err) 213 // Data is compromised. Trigger a resync. 214 return false 215 } 216 b.book.update(bids, asks) 217 return true 218 } 219 220 processSyncCache := func(snapshotID uint64) bool { 221 syncMtx.Lock() 222 defer syncMtx.Unlock() 223 224 updateID = snapshotID 225 for _, update := range syncCache { 226 if !acceptUpdate(update) { 227 return false 228 } 229 } 230 231 b.synced.Store(true) 232 if syncChan != nil { 233 close(syncChan) 234 syncChan = nil 235 } 236 return true 237 } 238 239 syncOrderbook := func() bool { 240 snapshot, err := b.getSnapshot() 241 if err != nil { 242 b.log.Errorf("Error getting orderbook snapshot: %v", err) 243 return false 244 } 245 246 bids, asks, err := b.convertBinanceBook(snapshot.Bids, snapshot.Asks) 247 if err != nil { 248 b.log.Errorf("Error parsing binance book: %v", err) 249 return false 250 } 251 252 b.log.Debugf("Got %s orderbook snapshot with update ID %d", b.mktID, snapshot.LastUpdateID) 253 254 b.book.clear() 255 b.book.update(bids, asks) 256 257 return processSyncCache(snapshot.LastUpdateID) 258 } 259 260 var wg sync.WaitGroup 261 wg.Add(1) 262 go func() { 263 processUpdate := func(update *bntypes.BookUpdate) bool { 264 syncMtx.Lock() 265 defer syncMtx.Unlock() 266 return acceptUpdate(update) 267 } 268 269 defer wg.Done() 270 for { 271 select { 272 case update := <-b.updateQueue: 273 if !processUpdate(update) { 274 b.log.Tracef("Bad %s update with ID %d", b.mktID, update.LastUpdateID) 275 desync(true) 276 } 277 case <-ctx.Done(): 278 return 279 } 280 } 281 }() 282 283 wg.Add(1) 284 go func() { 285 defer wg.Done() 286 287 const retryFrequency = time.Second * 30 288 289 retry := time.After(0) 290 291 for { 292 select { 293 case <-retry: 294 case <-resyncChan: 295 if retry != nil { // don't hammer 296 continue 297 } 298 case connected := <-b.connectedChan: 299 if !connected { 300 b.log.Debugf("Unsyncing %s orderbook due to disconnect.", b.mktID, retryFrequency) 301 desync(false) 302 retry = nil 303 continue 304 } 305 case <-ctx.Done(): 306 return 307 } 308 309 if syncOrderbook() { 310 b.log.Infof("Synced %s orderbook", b.mktID) 311 retry = nil 312 } else { 313 b.log.Infof("Failed to sync %s orderbook. Trying again in %s", b.mktID, retryFrequency) 314 desync(false) // Clears the syncCache 315 retry = time.After(retryFrequency) 316 } 317 } 318 }() 319 320 return &wg, nil 321 } 322 323 // vwap returns the volume weighted average price for a certain quantity of the 324 // base asset. It returns an error if the orderbook is not synced. 325 func (b *binanceOrderBook) vwap(bids bool, qty uint64) (vwap, extrema uint64, filled bool, err error) { 326 b.mtx.RLock() 327 defer b.mtx.RUnlock() 328 329 if !b.synced.Load() { 330 return 0, 0, filled, ErrUnsyncedOrderbook 331 } 332 333 vwap, extrema, filled = b.book.vwap(bids, qty) 334 return 335 } 336 337 func (b *binanceOrderBook) midGap() uint64 { 338 return b.book.midGap() 339 } 340 341 // TODO: check all symbols 342 var dexToBinanceSymbol = map[string]string{ 343 "polygon": "MATIC", 344 "weth": "ETH", 345 } 346 347 var binanceToDexSymbol = make(map[string]string) 348 349 // convertBnCoin converts a binance coin symbol to a dex symbol. 350 func convertBnCoin(coin string) string { 351 symbol := strings.ToLower(coin) 352 if convertedSymbol, found := binanceToDexSymbol[strings.ToUpper(coin)]; found { 353 symbol = convertedSymbol 354 } 355 if symbol == "weth" { 356 return "eth" 357 } 358 return symbol 359 } 360 361 // binanceCoinNetworkToDexSymbol takes the coin name and its network name as 362 // returned by the binance API and returns the DEX symbol. 363 func binanceCoinNetworkToDexSymbol(coin, network string) string { 364 symbol, netSymbol := convertBnCoin(coin), convertBnCoin(network) 365 if symbol == netSymbol { 366 return symbol 367 } 368 if symbol == "eth" { 369 symbol = "weth" 370 } 371 return symbol + "." + netSymbol 372 } 373 374 func init() { 375 for key, value := range dexToBinanceSymbol { 376 binanceToDexSymbol[value] = key 377 } 378 } 379 380 func mapDexToBinanceSymbol(symbol string) string { 381 if binanceSymbol, found := dexToBinanceSymbol[strings.ToLower(symbol)]; found { 382 return binanceSymbol 383 } 384 return strings.ToUpper(symbol) 385 } 386 387 type bncAssetConfig struct { 388 assetID uint32 389 // symbol is the DEX asset symbol, always lower case 390 symbol string 391 // coin is the asset symbol on binance, always upper case. 392 // For a token like USDC, the coin field will be USDC, but 393 // symbol field will be usdc.eth. 394 coin string 395 // chain will be the same as coin for the base assets of 396 // a blockchain, but for tokens it will be the chain 397 // that the token is hosted such as "ETH". 398 chain string 399 conversionFactor uint64 400 } 401 402 func bncAssetCfg(assetID uint32) (*bncAssetConfig, error) { 403 ui, err := asset.UnitInfo(assetID) 404 if err != nil { 405 return nil, err 406 } 407 408 symbol := dex.BipIDSymbol(assetID) 409 if symbol == "" { 410 return nil, fmt.Errorf("no symbol found for asset ID %d", assetID) 411 } 412 413 parts := strings.Split(symbol, ".") 414 coin := mapDexToBinanceSymbol(parts[0]) 415 chain := coin 416 if len(parts) > 1 { 417 chain = mapDexToBinanceSymbol(parts[1]) 418 } 419 420 return &bncAssetConfig{ 421 assetID: assetID, 422 symbol: symbol, 423 coin: coin, 424 chain: chain, 425 conversionFactor: ui.Conventional.ConversionFactor, 426 }, nil 427 } 428 429 func bncAssetCfgs(baseID, quoteID uint32) (*bncAssetConfig, *bncAssetConfig, error) { 430 baseCfg, err := bncAssetCfg(baseID) 431 if err != nil { 432 return nil, nil, err 433 } 434 435 quoteCfg, err := bncAssetCfg(quoteID) 436 if err != nil { 437 return nil, nil, err 438 } 439 440 return baseCfg, quoteCfg, nil 441 } 442 443 type tradeInfo struct { 444 updaterID int 445 baseID uint32 446 quoteID uint32 447 sell bool 448 rate uint64 449 qty uint64 450 } 451 452 type withdrawInfo struct { 453 minimum uint64 454 lotSize uint64 455 } 456 457 type BinanceCodedErr struct { 458 Code int `json:"code"` 459 Msg string `json:"msg"` 460 } 461 462 func (e *BinanceCodedErr) Error() string { 463 return fmt.Sprintf("code = %d, msg = %q", e.Code, e.Msg) 464 } 465 466 func errHasBnCode(err error, code int) bool { 467 var bnErr *BinanceCodedErr 468 if errors.As(err, &bnErr) && bnErr.Code == code { 469 return true 470 } 471 return false 472 } 473 474 type binance struct { 475 log dex.Logger 476 marketsURL string 477 accountsURL string 478 wsURL string 479 apiKey string 480 secretKey string 481 knownAssets map[uint32]bool 482 net dex.Network 483 tradeIDNonce atomic.Uint32 484 tradeIDNoncePrefix dex.Bytes 485 broadcast func(interface{}) 486 isUS bool 487 488 markets atomic.Value // map[string]*binanceMarket 489 // tokenIDs maps the token's symbol to the list of bip ids of the token 490 // for each chain for which deposits and withdrawals are enabled on 491 // binance. 492 tokenIDs atomic.Value // map[string][]uint32, binance coin ID string -> assset IDs 493 minWithdraw atomic.Value // map[uint32]map[uint32]*withdrawInfo 494 495 marketSnapshotMtx sync.Mutex 496 marketSnapshot struct { 497 stamp time.Time 498 m map[string]*Market 499 } 500 501 balanceMtx sync.RWMutex 502 balances map[uint32]*ExchangeBalance 503 504 marketStreamMtx sync.RWMutex 505 marketStream comms.WsConn 506 507 marketStreamRespsMtx sync.Mutex 508 marketStreamResps map[uint64]chan<- []string 509 510 booksMtx sync.RWMutex 511 books map[string]*binanceOrderBook 512 513 tradeUpdaterMtx sync.RWMutex 514 tradeInfo map[string]*tradeInfo 515 tradeUpdaters map[int]chan *Trade 516 tradeUpdateCounter int 517 518 listenKey atomic.Value // string 519 reconnectChan chan struct{} 520 } 521 522 var _ CEX = (*binance)(nil) 523 524 // TODO: Investigate stablecoin auto-conversion. 525 // https://developers.binance.com/docs/wallet/endpoints/switch-busd-stable-coins-convertion 526 527 func newBinance(cfg *CEXConfig, binanceUS bool) *binance { 528 var marketsURL, accountsURL, wsURL string 529 530 switch cfg.Net { 531 case dex.Testnet: 532 marketsURL, accountsURL, wsURL = testnetHttpURL, fakeBinanceURL, testnetWebsocketURL 533 case dex.Simnet: 534 marketsURL, accountsURL, wsURL = fakeBinanceURL, fakeBinanceURL, fakeBinanceWsURL 535 default: //mainnet 536 if binanceUS { 537 marketsURL, accountsURL, wsURL = usHttpURL, usHttpURL, usWebsocketURL 538 } else { 539 marketsURL, accountsURL, wsURL = httpURL, httpURL, websocketURL 540 } 541 } 542 543 registeredAssets := asset.Assets() 544 knownAssets := make(map[uint32]bool, len(registeredAssets)) 545 for _, a := range registeredAssets { 546 knownAssets[a.ID] = true 547 } 548 549 bnc := &binance{ 550 log: cfg.Logger, 551 broadcast: cfg.Notify, 552 isUS: binanceUS, 553 marketsURL: marketsURL, 554 accountsURL: accountsURL, 555 wsURL: wsURL, 556 apiKey: cfg.APIKey, 557 secretKey: cfg.SecretKey, 558 knownAssets: knownAssets, 559 balances: make(map[uint32]*ExchangeBalance), 560 books: make(map[string]*binanceOrderBook), 561 net: cfg.Net, 562 tradeInfo: make(map[string]*tradeInfo), 563 tradeUpdaters: make(map[int]chan *Trade), 564 tradeIDNoncePrefix: encode.RandomBytes(10), 565 reconnectChan: make(chan struct{}), 566 marketStreamResps: make(map[uint64]chan<- []string), 567 } 568 569 bnc.markets.Store(make(map[string]*bntypes.Market)) 570 bnc.listenKey.Store("") 571 572 return bnc 573 } 574 575 // setBalances queries binance for the user's balances and stores them in the 576 // balances map. 577 func (bnc *binance) setBalances(ctx context.Context) error { 578 bnc.balanceMtx.Lock() 579 defer bnc.balanceMtx.Unlock() 580 return bnc.refreshBalances(ctx) 581 } 582 583 func (bnc *binance) refreshBalances(ctx context.Context) error { 584 var resp bntypes.Account 585 err := bnc.getAPI(ctx, "/api/v3/account", nil, true, true, &resp) 586 if err != nil { 587 return err 588 } 589 590 tokenIDsI := bnc.tokenIDs.Load() 591 if tokenIDsI == nil { 592 return errors.New("cannot set balances before coin info is fetched") 593 } 594 tokenIDs := tokenIDsI.(map[string][]uint32) 595 596 for _, bal := range resp.Balances { 597 for _, assetID := range getDEXAssetIDs(bal.Asset, tokenIDs) { 598 ui, err := asset.UnitInfo(assetID) 599 if err != nil { 600 bnc.log.Errorf("no unit info for known asset ID %d?", assetID) 601 continue 602 } 603 updatedBalance := &ExchangeBalance{ 604 Available: uint64(math.Round(bal.Free * float64(ui.Conventional.ConversionFactor))), 605 Locked: uint64(math.Round(bal.Locked * float64(ui.Conventional.ConversionFactor))), 606 } 607 currBalance, found := bnc.balances[assetID] 608 if found && *currBalance != *updatedBalance { 609 // This function is only called when the CEX is started up, and 610 // once every 10 minutes. The balance should be updated by the user 611 // data stream, so if it is updated here, it could mean there is an 612 // issue. 613 bnc.log.Warnf("%v balance was out of sync. Updating. %+v -> %+v", bal.Asset, currBalance, updatedBalance) 614 } 615 616 bnc.balances[assetID] = updatedBalance 617 } 618 } 619 620 return nil 621 } 622 623 // readCoins stores the token IDs for which deposits and withdrawals are 624 // enabled on binance and sets the minWithdraw map. 625 func (bnc *binance) readCoins(coins []*bntypes.CoinInfo) { 626 tokenIDs := make(map[string][]uint32) 627 minWithdraw := make(map[uint32]*withdrawInfo) 628 for _, nfo := range coins { 629 for _, netInfo := range nfo.NetworkList { 630 symbol := binanceCoinNetworkToDexSymbol(nfo.Coin, netInfo.Network) 631 assetID, found := dex.BipSymbolID(symbol) 632 if !found { 633 continue 634 } 635 ui, err := asset.UnitInfo(assetID) 636 if err != nil { 637 // not a registered asset 638 continue 639 } 640 if !netInfo.WithdrawEnable || !netInfo.DepositEnable { 641 bnc.log.Tracef("Skipping %s network %s because deposits and/or withdraws are not enabled.", netInfo.Coin, netInfo.Network) 642 continue 643 } 644 if tkn := asset.TokenInfo(assetID); tkn != nil { 645 tokenIDs[nfo.Coin] = append(tokenIDs[nfo.Coin], assetID) 646 } 647 minimum := uint64(math.Round(float64(ui.Conventional.ConversionFactor) * netInfo.WithdrawMin)) 648 minWithdraw[assetID] = &withdrawInfo{ 649 minimum: minimum, 650 lotSize: uint64(math.Round(netInfo.WithdrawIntegerMultiple * float64(ui.Conventional.ConversionFactor))), 651 } 652 } 653 } 654 bnc.tokenIDs.Store(tokenIDs) 655 bnc.minWithdraw.Store(minWithdraw) 656 } 657 658 // getCoinInfo retrieves binance configs then updates the user balances and 659 // the tokenIDs. 660 func (bnc *binance) getCoinInfo(ctx context.Context) error { 661 coins := make([]*bntypes.CoinInfo, 0) 662 err := bnc.getAPI(ctx, "/sapi/v1/capital/config/getall", nil, true, true, &coins) 663 if err != nil { 664 return err 665 } 666 667 bnc.readCoins(coins) 668 return nil 669 } 670 671 func (bnc *binance) getMarkets(ctx context.Context) (map[string]*bntypes.Market, error) { 672 var exchangeInfo bntypes.ExchangeInfo 673 err := bnc.getAPI(ctx, "/api/v3/exchangeInfo", nil, false, false, &exchangeInfo) 674 if err != nil { 675 return nil, err 676 } 677 678 marketsMap := make(map[string]*bntypes.Market, len(exchangeInfo.Symbols)) 679 tokenIDs := bnc.tokenIDs.Load().(map[string][]uint32) 680 681 for _, market := range exchangeInfo.Symbols { 682 dexMarkets := binanceMarketToDexMarkets(market.BaseAsset, market.QuoteAsset, tokenIDs, bnc.isUS) 683 if len(dexMarkets) == 0 { 684 continue 685 } 686 dexMkt := dexMarkets[0] 687 688 bui, _ := asset.UnitInfo(dexMkt.BaseID) 689 qui, _ := asset.UnitInfo(dexMkt.QuoteID) 690 691 var rateStepFound, lotSizeFound bool 692 for _, filter := range market.Filters { 693 if filter.Type == "PRICE_FILTER" { 694 rateStepFound = true 695 conv := float64(qui.Conventional.ConversionFactor) / float64(bui.Conventional.ConversionFactor) * calc.RateEncodingFactor 696 market.RateStep = uint64(math.Round(filter.TickSize * conv)) 697 market.MinPrice = uint64(math.Round(filter.MinPrice * conv)) 698 market.MaxPrice = uint64(math.Round(filter.MaxPrice * conv)) 699 } else if filter.Type == "LOT_SIZE" { 700 lotSizeFound = true 701 market.LotSize = uint64(math.Round(filter.StepSize * float64(bui.Conventional.ConversionFactor))) 702 market.MinQty = uint64(math.Round(filter.MinQty * float64(bui.Conventional.ConversionFactor))) 703 market.MaxQty = uint64(math.Round(filter.MaxQty * float64(bui.Conventional.ConversionFactor))) 704 } 705 if rateStepFound && lotSizeFound { 706 break 707 } 708 } 709 if !rateStepFound || !lotSizeFound { 710 bnc.log.Errorf("missing filter for market %s, rate step found = %t, lot size found = %t", dexMkt.MarketID, rateStepFound, lotSizeFound) 711 continue 712 } 713 714 marketsMap[market.Symbol] = market 715 } 716 717 bnc.markets.Store(marketsMap) 718 return marketsMap, nil 719 } 720 721 // Connect connects to the binance API. 722 func (bnc *binance) Connect(ctx context.Context) (*sync.WaitGroup, error) { 723 wg := new(sync.WaitGroup) 724 725 if err := bnc.getCoinInfo(ctx); err != nil { 726 return nil, fmt.Errorf("error getting coin info: %w", err) 727 } 728 729 if _, err := bnc.getMarkets(ctx); err != nil { 730 return nil, fmt.Errorf("error getting markets: %w", err) 731 } 732 733 if err := bnc.setBalances(ctx); err != nil { 734 return nil, fmt.Errorf("error getting balances") 735 } 736 737 if err := bnc.getUserDataStream(ctx); err != nil { 738 return nil, fmt.Errorf("error getting user data stream") 739 } 740 741 // Refresh balances periodically. This is just for safety as they should 742 // be updated based on the user data stream. 743 wg.Add(1) 744 go func() { 745 defer wg.Done() 746 ticker := time.NewTicker(time.Minute) 747 defer ticker.Stop() 748 for { 749 select { 750 case <-ticker.C: 751 err := bnc.setBalances(ctx) 752 if err != nil { 753 bnc.log.Errorf("Error fetching balances: %v", err) 754 } 755 case <-ctx.Done(): 756 return 757 } 758 } 759 }() 760 761 // Refresh the markets periodically. 762 wg.Add(1) 763 go func() { 764 defer wg.Done() 765 nextTick := time.After(time.Hour) 766 for { 767 select { 768 case <-nextTick: 769 _, err := bnc.getMarkets(ctx) 770 if err != nil { 771 bnc.log.Errorf("Error fetching markets: %v", err) 772 nextTick = time.After(time.Minute) 773 } else { 774 nextTick = time.After(time.Hour) 775 } 776 case <-ctx.Done(): 777 return 778 } 779 } 780 }() 781 782 // Refresh the coin info periodically. 783 wg.Add(1) 784 go func() { 785 defer wg.Done() 786 nextTick := time.After(time.Hour) 787 for { 788 select { 789 case <-nextTick: 790 err := bnc.getCoinInfo(ctx) 791 if err != nil { 792 bnc.log.Errorf("Error fetching markets: %v", err) 793 nextTick = time.After(time.Minute) 794 } else { 795 nextTick = time.After(time.Hour) 796 } 797 case <-ctx.Done(): 798 return 799 } 800 } 801 }() 802 803 return wg, nil 804 } 805 806 // Balance returns the balance of an asset at the CEX. 807 func (bnc *binance) Balance(assetID uint32) (*ExchangeBalance, error) { 808 assetConfig, err := bncAssetCfg(assetID) 809 if err != nil { 810 return nil, err 811 } 812 813 bnc.balanceMtx.RLock() 814 defer bnc.balanceMtx.RUnlock() 815 816 bal, found := bnc.balances[assetConfig.assetID] 817 if !found { 818 return nil, fmt.Errorf("no %q balance found", assetConfig.coin) 819 } 820 821 return bal, nil 822 } 823 824 func (bnc *binance) generateTradeID() string { 825 nonce := bnc.tradeIDNonce.Add(1) 826 nonceB := encode.Uint32Bytes(nonce) 827 return hex.EncodeToString(append(bnc.tradeIDNoncePrefix, nonceB...)) 828 } 829 830 // steppedRate rounds the rate to the nearest integer multiple of the step. 831 // The minimum returned value is step. 832 func steppedRate(r, step uint64) uint64 { 833 steps := math.Round(float64(r) / float64(step)) 834 if steps == 0 { 835 return step 836 } 837 return uint64(math.Round(steps * float64(step))) 838 } 839 840 // Trade executes a trade on the CEX. subscriptionID takes an ID returned from 841 // SubscribeTradeUpdates. 842 func (bnc *binance) Trade(ctx context.Context, baseID, quoteID uint32, sell bool, rate, qty uint64, subscriptionID int) (*Trade, error) { 843 side := "BUY" 844 if sell { 845 side = "SELL" 846 } 847 848 baseCfg, err := bncAssetCfg(baseID) 849 if err != nil { 850 return nil, fmt.Errorf("error getting asset cfg for %d: %w", baseID, err) 851 } 852 853 quoteCfg, err := bncAssetCfg(quoteID) 854 if err != nil { 855 return nil, fmt.Errorf("error getting asset cfg for %d: %w", quoteID, err) 856 } 857 858 slug := baseCfg.coin + quoteCfg.coin 859 860 marketsMap := bnc.markets.Load().(map[string]*bntypes.Market) 861 market, found := marketsMap[slug] 862 if !found { 863 return nil, fmt.Errorf("market not found: %v", slug) 864 } 865 866 if rate < market.MinPrice || rate > market.MaxPrice { 867 return nil, fmt.Errorf("rate %v is out of bounds for market %v", rate, slug) 868 } 869 rate = steppedRate(rate, market.RateStep) 870 convRate := calc.ConventionalRateAlt(rate, baseCfg.conversionFactor, quoteCfg.conversionFactor) 871 ratePrec := int(math.Round(math.Log10(calc.RateEncodingFactor * float64(baseCfg.conversionFactor) / float64(quoteCfg.conversionFactor) / float64(market.RateStep)))) 872 rateStr := strconv.FormatFloat(convRate, 'f', ratePrec, 64) 873 874 if qty < market.MinQty || qty > market.MaxQty { 875 return nil, fmt.Errorf("quantity %v is out of bounds for market %v", qty, slug) 876 } 877 steppedQty := steppedRate(qty, market.LotSize) 878 convQty := float64(steppedQty) / float64(baseCfg.conversionFactor) 879 qtyPrec := int(math.Round(math.Log10(float64(baseCfg.conversionFactor) / float64(market.LotSize)))) 880 qtyStr := strconv.FormatFloat(convQty, 'f', qtyPrec, 64) 881 882 tradeID := bnc.generateTradeID() 883 884 v := make(url.Values) 885 v.Add("symbol", slug) 886 v.Add("side", side) 887 v.Add("type", "LIMIT") 888 v.Add("timeInForce", "GTC") 889 v.Add("newClientOrderId", tradeID) 890 v.Add("quantity", qtyStr) 891 v.Add("price", rateStr) 892 893 bnc.tradeUpdaterMtx.Lock() 894 _, found = bnc.tradeUpdaters[subscriptionID] 895 if !found { 896 bnc.tradeUpdaterMtx.Unlock() 897 return nil, fmt.Errorf("no trade updater with ID %v", subscriptionID) 898 } 899 bnc.tradeInfo[tradeID] = &tradeInfo{ 900 updaterID: subscriptionID, 901 baseID: baseID, 902 quoteID: quoteID, 903 sell: sell, 904 rate: rate, 905 qty: qty, 906 } 907 bnc.tradeUpdaterMtx.Unlock() 908 909 var success bool 910 defer func() { 911 if !success { 912 bnc.tradeUpdaterMtx.Lock() 913 delete(bnc.tradeInfo, tradeID) 914 bnc.tradeUpdaterMtx.Unlock() 915 } 916 }() 917 918 var orderResponse bntypes.OrderResponse 919 err = bnc.postAPI(ctx, "/api/v3/order", v, nil, true, true, &orderResponse) 920 if err != nil { 921 return nil, err 922 } 923 924 success = true 925 926 return &Trade{ 927 ID: tradeID, 928 Sell: sell, 929 Rate: rate, 930 Qty: qty, 931 BaseID: baseID, 932 QuoteID: quoteID, 933 BaseFilled: uint64(orderResponse.ExecutedQty * float64(baseCfg.conversionFactor)), 934 QuoteFilled: uint64(orderResponse.CumulativeQuoteQty * float64(quoteCfg.conversionFactor)), 935 Complete: orderResponse.Status != "NEW" && orderResponse.Status != "PARTIALLY_FILLED", 936 }, err 937 } 938 939 // ConfirmWithdrawal checks whether a withdrawal has been completed. If the 940 // withdrawal has not yet been sent, ErrWithdrawalPending is returned. 941 func (bnc *binance) ConfirmWithdrawal(ctx context.Context, withdrawalID string, assetID uint32) (uint64, string, error) { 942 assetCfg, err := bncAssetCfg(assetID) 943 if err != nil { 944 return 0, "", fmt.Errorf("error getting symbol data for %d: %w", assetID, err) 945 } 946 947 type withdrawalHistoryStatus struct { 948 ID string `json:"id"` 949 Amount float64 `json:"amount,string"` 950 Status int `json:"status"` 951 TxID string `json:"txId"` 952 } 953 954 withdrawHistoryResponse := []*withdrawalHistoryStatus{} 955 v := make(url.Values) 956 v.Add("coin", assetCfg.coin) 957 err = bnc.getAPI(ctx, "/sapi/v1/capital/withdraw/history", v, true, true, &withdrawHistoryResponse) 958 if err != nil { 959 return 0, "", err 960 } 961 962 var status *withdrawalHistoryStatus 963 for _, s := range withdrawHistoryResponse { 964 if s.ID == withdrawalID { 965 status = s 966 break 967 } 968 } 969 if status == nil { 970 return 0, "", fmt.Errorf("withdrawal status not found for %s", withdrawalID) 971 } 972 973 bnc.log.Tracef("Withdrawal status: %+v", status) 974 975 if status.TxID == "" { 976 return 0, "", ErrWithdrawalPending 977 } 978 979 amt := status.Amount * float64(assetCfg.conversionFactor) 980 return uint64(amt), status.TxID, nil 981 } 982 983 // Withdraw withdraws funds from the CEX to a certain address. onComplete 984 // is called with the actual amount withdrawn (amt - fees) and the 985 // transaction ID of the withdrawal. 986 func (bnc *binance) Withdraw(ctx context.Context, assetID uint32, qty uint64, address string) (string, error) { 987 assetCfg, err := bncAssetCfg(assetID) 988 if err != nil { 989 return "", fmt.Errorf("error getting symbol data for %d: %w", assetID, err) 990 } 991 992 lotSize, err := bnc.withdrawLotSize(assetID) 993 if err != nil { 994 return "", fmt.Errorf("error getting withdraw lot size for %d: %w", assetID, err) 995 } 996 997 steppedQty := steppedRate(qty, lotSize) 998 convQty := float64(steppedQty) / float64(assetCfg.conversionFactor) 999 prec := int(math.Round(math.Log10(float64(assetCfg.conversionFactor) / float64(lotSize)))) 1000 qtyStr := strconv.FormatFloat(convQty, 'f', prec, 64) 1001 1002 v := make(url.Values) 1003 v.Add("coin", assetCfg.coin) 1004 v.Add("network", assetCfg.chain) 1005 v.Add("address", address) 1006 v.Add("amount", qtyStr) 1007 1008 withdrawResp := struct { 1009 ID string `json:"id"` 1010 }{} 1011 err = bnc.postAPI(ctx, "/sapi/v1/capital/withdraw/apply", nil, v, true, true, &withdrawResp) 1012 if err != nil { 1013 return "", err 1014 } 1015 1016 return withdrawResp.ID, nil 1017 } 1018 1019 // GetDepositAddress returns a deposit address for an asset. 1020 func (bnc *binance) GetDepositAddress(ctx context.Context, assetID uint32) (string, error) { 1021 assetCfg, err := bncAssetCfg(assetID) 1022 if err != nil { 1023 return "", fmt.Errorf("error getting asset cfg for %d: %w", assetID, err) 1024 } 1025 1026 v := make(url.Values) 1027 v.Add("coin", assetCfg.coin) 1028 v.Add("network", assetCfg.chain) 1029 1030 resp := struct { 1031 Address string `json:"address"` 1032 }{} 1033 err = bnc.getAPI(ctx, "/sapi/v1/capital/deposit/address", v, true, true, &resp) 1034 if err != nil { 1035 return "", err 1036 } 1037 1038 return resp.Address, nil 1039 } 1040 1041 // ConfirmDeposit is an async function that calls onConfirm when the status of 1042 // a deposit has been confirmed. 1043 func (bnc *binance) ConfirmDeposit(ctx context.Context, deposit *DepositData) (bool, uint64) { 1044 var resp []*bntypes.PendingDeposit 1045 // We'll add info for the fake server. 1046 var query url.Values 1047 if bnc.accountsURL == fakeBinanceURL { 1048 bncAsset, err := bncAssetCfg(deposit.AssetID) 1049 if err != nil { 1050 bnc.log.Errorf("Error getting asset cfg for %d: %v", deposit.AssetID, err) 1051 return false, 0 1052 } 1053 1054 query = url.Values{ 1055 "txid": []string{deposit.TxID}, 1056 "amt": []string{strconv.FormatFloat(deposit.AmountConventional, 'f', 9, 64)}, 1057 "coin": []string{bncAsset.coin}, 1058 "network": []string{bncAsset.chain}, 1059 } 1060 } 1061 // TODO: Use the "startTime" parameter to apply a reasonable limit to 1062 // this request. 1063 err := bnc.getAPI(ctx, "/sapi/v1/capital/deposit/hisrec", query, true, true, &resp) 1064 if err != nil { 1065 bnc.log.Errorf("error getting deposit status: %v", err) 1066 return false, 0 1067 } 1068 1069 for _, status := range resp { 1070 if status.TxID == deposit.TxID { 1071 switch status.Status { 1072 case bntypes.DepositStatusSuccess, bntypes.DepositStatusCredited: 1073 symbol := binanceCoinNetworkToDexSymbol(status.Coin, status.Network) 1074 assetID, found := dex.BipSymbolID(symbol) 1075 if !found { 1076 bnc.log.Errorf("Failed to find DEX asset ID for Coin: %s, Network: %s", status.Coin, status.Network) 1077 return true, 0 1078 } 1079 ui, err := asset.UnitInfo(assetID) 1080 if err != nil { 1081 bnc.log.Errorf("Failed to find unit info for asset ID %d", assetID) 1082 return true, 0 1083 } 1084 amount := uint64(status.Amount * float64(ui.Conventional.ConversionFactor)) 1085 return true, amount 1086 case bntypes.DepositStatusPending: 1087 return false, 0 1088 case bntypes.DepositStatusWaitingUserConfirm: 1089 // This shouldn't ever happen. 1090 bnc.log.Errorf("Deposit %s to binance requires user confirmation!") 1091 return true, 0 1092 case bntypes.DepositStatusWrongDeposit: 1093 return true, 0 1094 default: 1095 bnc.log.Errorf("Deposit %s to binance has an unknown status %d", status.Status) 1096 } 1097 } 1098 } 1099 1100 return false, 0 1101 } 1102 1103 // SubscribeTradeUpdates returns a channel that the caller can use to 1104 // listen for updates to a trade's status. When the subscription ID 1105 // returned from this function is passed as the updaterID argument to 1106 // Trade, then updates to the trade will be sent on the updated channel 1107 // returned from this function. 1108 func (bnc *binance) SubscribeTradeUpdates() (<-chan *Trade, func(), int) { 1109 bnc.tradeUpdaterMtx.Lock() 1110 defer bnc.tradeUpdaterMtx.Unlock() 1111 updaterID := bnc.tradeUpdateCounter 1112 bnc.tradeUpdateCounter++ 1113 updater := make(chan *Trade, 256) 1114 bnc.tradeUpdaters[updaterID] = updater 1115 1116 unsubscribe := func() { 1117 bnc.tradeUpdaterMtx.Lock() 1118 delete(bnc.tradeUpdaters, updaterID) 1119 bnc.tradeUpdaterMtx.Unlock() 1120 } 1121 1122 return updater, unsubscribe, updaterID 1123 } 1124 1125 // CancelTrade cancels a trade. 1126 func (bnc *binance) CancelTrade(ctx context.Context, baseID, quoteID uint32, tradeID string) error { 1127 baseCfg, err := bncAssetCfg(baseID) 1128 if err != nil { 1129 return fmt.Errorf("error getting asset cfg for %d: %w", baseID, err) 1130 } 1131 1132 quoteCfg, err := bncAssetCfg(quoteID) 1133 if err != nil { 1134 return fmt.Errorf("error getting asset cfg for %d: %w", quoteID, err) 1135 } 1136 1137 slug := baseCfg.coin + quoteCfg.coin 1138 1139 v := make(url.Values) 1140 v.Add("symbol", slug) 1141 v.Add("origClientOrderId", tradeID) 1142 1143 return bnc.request(ctx, "DELETE", "/api/v3/order", v, nil, true, true, nil) 1144 } 1145 1146 func (bnc *binance) Balances(ctx context.Context) (map[uint32]*ExchangeBalance, error) { 1147 bnc.balanceMtx.RLock() 1148 defer bnc.balanceMtx.RUnlock() 1149 1150 if len(bnc.balances) == 0 { 1151 if err := bnc.refreshBalances(ctx); err != nil { 1152 return nil, err 1153 } 1154 } 1155 1156 balances := make(map[uint32]*ExchangeBalance) 1157 1158 for assetID, bal := range bnc.balances { 1159 assetConfig, err := bncAssetCfg(assetID) 1160 if err != nil { 1161 continue 1162 } 1163 1164 balances[assetConfig.assetID] = bal 1165 } 1166 1167 return balances, nil 1168 } 1169 1170 func (bnc *binance) minimumWithdraws(baseID, quoteID uint32) (base uint64, quote uint64) { 1171 minsI := bnc.minWithdraw.Load() 1172 if minsI == nil { 1173 return 0, 0 1174 } 1175 mins := minsI.(map[uint32]*withdrawInfo) 1176 if baseInfo, found := mins[baseID]; found { 1177 base = baseInfo.minimum 1178 } 1179 if quoteInfo, found := mins[quoteID]; found { 1180 quote = quoteInfo.minimum 1181 } 1182 return 1183 } 1184 1185 func (bnc *binance) withdrawLotSize(assetID uint32) (uint64, error) { 1186 minsI := bnc.minWithdraw.Load() 1187 if minsI == nil { 1188 return 0, fmt.Errorf("no withdraw info") 1189 } 1190 mins := minsI.(map[uint32]*withdrawInfo) 1191 if info, found := mins[assetID]; found { 1192 return info.lotSize, nil 1193 } 1194 return 0, fmt.Errorf("no withdraw info for asset ID %d", assetID) 1195 } 1196 1197 func (bnc *binance) Markets(ctx context.Context) (map[string]*Market, error) { 1198 bnc.marketSnapshotMtx.Lock() 1199 defer bnc.marketSnapshotMtx.Unlock() 1200 1201 const snapshotTimeout = time.Minute * 30 1202 if bnc.marketSnapshot.m != nil && time.Since(bnc.marketSnapshot.stamp) < snapshotTimeout { 1203 return bnc.marketSnapshot.m, nil 1204 } 1205 1206 matches, err := bnc.MatchedMarkets(ctx) 1207 if err != nil { 1208 return nil, fmt.Errorf("error getting market list for market data request: %w", err) 1209 } 1210 1211 mkts := make(map[string][]*MarketMatch, len(matches)) 1212 for _, m := range matches { 1213 mkts[m.Slug] = append(mkts[m.Slug], m) 1214 } 1215 encSymbols, err := json.Marshal(utils.MapKeys(mkts)) 1216 if err != nil { 1217 return nil, fmt.Errorf("error encoding symbold for market data request: %w", err) 1218 } 1219 1220 q := make(url.Values) 1221 q.Set("symbols", string(encSymbols)) 1222 1223 var ds []*bntypes.MarketTicker24 1224 if err = bnc.getAPI(ctx, "/api/v3/ticker/24hr", q, false, false, &ds); err != nil { 1225 return nil, err 1226 } 1227 1228 m := make(map[string]*Market, len(ds)) 1229 for _, d := range ds { 1230 ms, found := mkts[d.Symbol] 1231 if !found { 1232 bnc.log.Errorf("Market %s not returned in market data request", d.Symbol) 1233 continue 1234 } 1235 for _, mkt := range ms { 1236 baseMinWithdraw, quoteMinWithdraw := bnc.minimumWithdraws(mkt.BaseID, mkt.QuoteID) 1237 m[mkt.MarketID] = &Market{ 1238 BaseID: mkt.BaseID, 1239 QuoteID: mkt.QuoteID, 1240 BaseMinWithdraw: baseMinWithdraw, 1241 QuoteMinWithdraw: quoteMinWithdraw, 1242 Day: &MarketDay{ 1243 Vol: d.Volume, 1244 QuoteVol: d.QuoteVolume, 1245 PriceChange: d.PriceChange, 1246 PriceChangePct: d.PriceChangePercent, 1247 AvgPrice: d.WeightedAvgPrice, 1248 LastPrice: d.LastPrice, 1249 OpenPrice: d.OpenPrice, 1250 HighPrice: d.HighPrice, 1251 LowPrice: d.LowPrice, 1252 }, 1253 } 1254 } 1255 } 1256 bnc.marketSnapshot.m = m 1257 bnc.marketSnapshot.stamp = time.Now() 1258 1259 return m, nil 1260 } 1261 1262 func (bnc *binance) MatchedMarkets(ctx context.Context) (_ []*MarketMatch, err error) { 1263 if tokenIDsI := bnc.tokenIDs.Load(); tokenIDsI == nil { 1264 if err := bnc.getCoinInfo(ctx); err != nil { 1265 return nil, fmt.Errorf("error getting coin info for token IDs: %v", err) 1266 } 1267 } 1268 tokenIDs := bnc.tokenIDs.Load().(map[string][]uint32) 1269 1270 bnMarkets := bnc.markets.Load().(map[string]*bntypes.Market) 1271 if len(bnMarkets) == 0 { 1272 bnMarkets, err = bnc.getMarkets(ctx) 1273 if err != nil { 1274 return nil, fmt.Errorf("error getting markets: %v", err) 1275 } 1276 } 1277 markets := make([]*MarketMatch, 0, len(bnMarkets)) 1278 1279 for _, mkt := range bnMarkets { 1280 dexMarkets := binanceMarketToDexMarkets(mkt.BaseAsset, mkt.QuoteAsset, tokenIDs, bnc.isUS) 1281 markets = append(markets, dexMarkets...) 1282 } 1283 1284 return markets, nil 1285 } 1286 1287 func (bnc *binance) getAPI(ctx context.Context, endpoint string, query url.Values, key, sign bool, thing interface{}) error { 1288 return bnc.request(ctx, http.MethodGet, endpoint, query, nil, key, sign, thing) 1289 } 1290 1291 func (bnc *binance) postAPI(ctx context.Context, endpoint string, query, form url.Values, key, sign bool, thing interface{}) error { 1292 return bnc.request(ctx, http.MethodPost, endpoint, query, form, key, sign, thing) 1293 } 1294 1295 func (bnc *binance) request(ctx context.Context, method, endpoint string, query, form url.Values, key, sign bool, thing interface{}) error { 1296 var fullURL string 1297 if strings.Contains(endpoint, "sapi") { 1298 fullURL = bnc.accountsURL + endpoint 1299 } else { 1300 fullURL = bnc.marketsURL + endpoint 1301 } 1302 1303 if query == nil { 1304 query = make(url.Values) 1305 } 1306 if sign { 1307 query.Add("timestamp", strconv.FormatInt(time.Now().UnixMilli(), 10)) 1308 } 1309 queryString := query.Encode() 1310 bodyString := form.Encode() 1311 header := make(http.Header, 2) 1312 body := bytes.NewBuffer(nil) 1313 if bodyString != "" { 1314 header.Set("Content-Type", "application/x-www-form-urlencoded") 1315 body = bytes.NewBufferString(bodyString) 1316 } 1317 if key || sign { 1318 header.Set("X-MBX-APIKEY", bnc.apiKey) 1319 } 1320 1321 if sign { 1322 raw := queryString + bodyString 1323 mac := hmac.New(sha256.New, []byte(bnc.secretKey)) 1324 if _, err := mac.Write([]byte(raw)); err != nil { 1325 return fmt.Errorf("hmax Write error: %w", err) 1326 } 1327 v := url.Values{} 1328 v.Set("signature", hex.EncodeToString(mac.Sum(nil))) 1329 if queryString == "" { 1330 queryString = v.Encode() 1331 } else { 1332 queryString = fmt.Sprintf("%s&%s", queryString, v.Encode()) 1333 } 1334 } 1335 if queryString != "" { 1336 fullURL = fmt.Sprintf("%s?%s", fullURL, queryString) 1337 } 1338 1339 req, err := http.NewRequestWithContext(ctx, method, fullURL, body) 1340 if err != nil { 1341 return fmt.Errorf("NewRequestWithContext error: %w", err) 1342 } 1343 1344 req.Header = header 1345 1346 var bnErr BinanceCodedErr 1347 if err := dexnet.Do(req, thing, dexnet.WithSizeLimit(1<<24), dexnet.WithErrorParsing(&bnErr)); err != nil { 1348 bnc.log.Errorf("request error from endpoint %s %q with query = %q, body = %q, bn coded error: %v, msg = %q", 1349 method, endpoint, queryString, bodyString, &bnErr, bnErr.Msg) 1350 return errors.Join(err, &bnErr) 1351 } 1352 1353 return nil 1354 } 1355 1356 func (bnc *binance) handleOutboundAccountPosition(update *bntypes.StreamUpdate) { 1357 bnc.log.Debugf("Received outboundAccountPosition: %+v", update) 1358 for _, bal := range update.Balances { 1359 bnc.log.Debugf("outboundAccountPosition balance: %+v", bal) 1360 } 1361 1362 supportedTokens := bnc.tokenIDs.Load().(map[string][]uint32) 1363 updates := make([]*BalanceUpdate, 0, len(update.Balances)) 1364 1365 processSymbol := func(symbol string, bal *bntypes.WSBalance) { 1366 for _, assetID := range getDEXAssetIDs(symbol, supportedTokens) { 1367 ui, err := asset.UnitInfo(assetID) 1368 if err != nil { 1369 bnc.log.Errorf("no unit info for known asset ID %d?", assetID) 1370 return 1371 } 1372 oldBal := bnc.balances[assetID] 1373 newBal := &ExchangeBalance{ 1374 Available: uint64(math.Round(bal.Free * float64(ui.Conventional.ConversionFactor))), 1375 Locked: uint64(math.Round(bal.Locked * float64(ui.Conventional.ConversionFactor))), 1376 } 1377 bnc.balances[assetID] = newBal 1378 if oldBal != nil && *oldBal != *newBal { 1379 updates = append(updates, &BalanceUpdate{ 1380 AssetID: assetID, 1381 Balance: newBal, 1382 }) 1383 } 1384 } 1385 } 1386 1387 bnc.balanceMtx.Lock() 1388 for _, bal := range update.Balances { 1389 processSymbol(bal.Asset, bal) 1390 if bal.Asset == "ETH" { 1391 processSymbol("WETH", bal) 1392 } 1393 } 1394 bnc.balanceMtx.Unlock() 1395 1396 for _, u := range updates { 1397 bnc.broadcast(u) 1398 } 1399 } 1400 1401 func (bnc *binance) getTradeUpdater(tradeID string) (chan *Trade, *tradeInfo, error) { 1402 bnc.tradeUpdaterMtx.RLock() 1403 defer bnc.tradeUpdaterMtx.RUnlock() 1404 1405 tradeInfo, found := bnc.tradeInfo[tradeID] 1406 if !found { 1407 return nil, nil, fmt.Errorf("info not found for trade ID %v", tradeID) 1408 } 1409 updater, found := bnc.tradeUpdaters[tradeInfo.updaterID] 1410 if !found { 1411 return nil, nil, fmt.Errorf("no updater with ID %v", tradeID) 1412 } 1413 1414 return updater, tradeInfo, nil 1415 } 1416 1417 func (bnc *binance) removeTradeUpdater(tradeID string) { 1418 bnc.tradeUpdaterMtx.RLock() 1419 defer bnc.tradeUpdaterMtx.RUnlock() 1420 delete(bnc.tradeInfo, tradeID) 1421 } 1422 1423 func (bnc *binance) handleExecutionReport(update *bntypes.StreamUpdate) { 1424 bnc.log.Debugf("Received executionReport: %+v", update) 1425 1426 status := update.CurrentOrderStatus 1427 var id string 1428 if status == "CANCELED" { 1429 id = update.CancelledOrderID 1430 } else { 1431 id = update.ClientOrderID 1432 } 1433 1434 updater, tradeInfo, err := bnc.getTradeUpdater(id) 1435 if err != nil { 1436 bnc.log.Errorf("Error getting trade updater: %v", err) 1437 return 1438 } 1439 1440 complete := status != "NEW" && status != "PARTIALLY_FILLED" 1441 1442 baseCfg, err := bncAssetCfg(tradeInfo.baseID) 1443 if err != nil { 1444 bnc.log.Errorf("Error getting asset cfg for %d: %v", tradeInfo.baseID, err) 1445 return 1446 } 1447 1448 quoteCfg, err := bncAssetCfg(tradeInfo.quoteID) 1449 if err != nil { 1450 bnc.log.Errorf("Error getting asset cfg for %d: %v", tradeInfo.quoteID, err) 1451 return 1452 } 1453 1454 updater <- &Trade{ 1455 ID: id, 1456 Complete: complete, 1457 Rate: tradeInfo.rate, 1458 Qty: tradeInfo.qty, 1459 BaseFilled: uint64(update.Filled * float64(baseCfg.conversionFactor)), 1460 QuoteFilled: uint64(update.QuoteFilled * float64(quoteCfg.conversionFactor)), 1461 BaseID: tradeInfo.baseID, 1462 QuoteID: tradeInfo.quoteID, 1463 Sell: tradeInfo.sell, 1464 } 1465 1466 if complete { 1467 bnc.removeTradeUpdater(id) 1468 } 1469 } 1470 1471 func (bnc *binance) handleListenKeyExpired(update *bntypes.StreamUpdate) { 1472 bnc.log.Debugf("Received listenKeyExpired: %+v", update) 1473 expireTime := time.Unix(update.E/1000, 0) 1474 bnc.log.Errorf("Listen key %v expired at %v. Attempting to reconnect and get a new one.", update.ListenKey, expireTime) 1475 bnc.reconnectChan <- struct{}{} 1476 } 1477 1478 func (bnc *binance) handleUserDataStreamUpdate(b []byte) { 1479 bnc.log.Tracef("Received user data stream update: %s", string(b)) 1480 1481 var msg *bntypes.StreamUpdate 1482 if err := json.Unmarshal(b, &msg); err != nil { 1483 bnc.log.Errorf("Error unmarshaling user data stream update: %v\nRaw message: %s", err, string(b)) 1484 return 1485 } 1486 1487 switch msg.EventType { 1488 case "outboundAccountPosition": 1489 bnc.handleOutboundAccountPosition(msg) 1490 case "executionReport": 1491 bnc.handleExecutionReport(msg) 1492 case "listenKeyExpired": 1493 bnc.handleListenKeyExpired(msg) 1494 } 1495 } 1496 1497 func (bnc *binance) getListenID(ctx context.Context) (string, error) { 1498 var resp *bntypes.DataStreamKey 1499 if err := bnc.postAPI(ctx, "/api/v3/userDataStream", nil, nil, true, false, &resp); err != nil { 1500 return "", err 1501 } 1502 bnc.listenKey.Store(resp.ListenKey) 1503 return resp.ListenKey, nil 1504 } 1505 1506 func (bnc *binance) getUserDataStream(ctx context.Context) (err error) { 1507 newConn := func() (*dex.ConnectionMaster, error) { 1508 listenKey, err := bnc.getListenID(ctx) 1509 if err != nil { 1510 return nil, err 1511 } 1512 1513 conn, err := comms.NewWsConn(&comms.WsCfg{ 1514 URL: bnc.wsURL + "/ws/" + listenKey, 1515 PingWait: time.Minute * 4, 1516 EchoPingData: true, 1517 ReconnectSync: func() { 1518 bnc.log.Debugf("Binance reconnected") 1519 }, 1520 Logger: bnc.log.SubLogger("BNCWS"), 1521 RawHandler: bnc.handleUserDataStreamUpdate, 1522 ConnectHeaders: http.Header{"X-MBX-APIKEY": []string{bnc.apiKey}}, 1523 }) 1524 if err != nil { 1525 return nil, fmt.Errorf("NewWsConn error: %w", err) 1526 } 1527 1528 cm := dex.NewConnectionMaster(conn) 1529 if err = cm.ConnectOnce(ctx); err != nil { 1530 return nil, err 1531 } 1532 1533 return cm, nil 1534 } 1535 1536 cm, err := newConn() 1537 if err != nil { 1538 return fmt.Errorf("error initializing connection: %v", err) 1539 } 1540 1541 go func() { 1542 // A single connection to stream.binance.com is only valid for 24 hours; 1543 // expect to be disconnected at the 24 hour mark. 1544 reconnect := time.After(time.Hour * 12) 1545 // Keepalive a user data stream to prevent a time out. User data streams 1546 // will close after 60 minutes. It's recommended to send a ping about 1547 // every 30 minutes. 1548 keepAlive := time.NewTicker(time.Minute * 30) 1549 defer keepAlive.Stop() 1550 1551 retryKeepAlive := make(<-chan time.Time) 1552 1553 connected := true // do not keep alive on a failed connection 1554 1555 doReconnect := func() { 1556 if cm != nil { 1557 cm.Disconnect() 1558 } 1559 cm, err = newConn() 1560 if err != nil { 1561 connected = false 1562 bnc.log.Errorf("Error reconnecting: %v", err) 1563 reconnect = time.After(time.Second * 30) 1564 } else { 1565 connected = true 1566 reconnect = time.After(time.Hour * 12) 1567 } 1568 } 1569 1570 doKeepAlive := func() { 1571 if !connected { 1572 bnc.log.Warn("Cannot keep binance connection alive because we are disconnected. Trying again in 10 seconds.") 1573 retryKeepAlive = time.After(time.Second * 10) 1574 return 1575 } 1576 q := make(url.Values) 1577 q.Add("listenKey", bnc.listenKey.Load().(string)) 1578 // Doing a PUT on a listenKey will extend its validity for 60 minutes. 1579 if err := bnc.request(ctx, http.MethodPut, "/api/v3/userDataStream", q, nil, true, false, nil); err != nil { 1580 if errHasBnCode(err, bnErrCodeInvalidListenKey) { 1581 bnc.log.Warnf("Invalid listen key. Reconnecting...") 1582 doReconnect() 1583 return 1584 } 1585 bnc.log.Errorf("Error sending keep-alive request: %v. Trying again in 10 seconds", err) 1586 retryKeepAlive = time.After(time.Second * 10) 1587 return 1588 } 1589 bnc.log.Debug("Binance connection keep alive sent successfully.") 1590 } 1591 1592 for { 1593 select { 1594 case <-bnc.reconnectChan: 1595 doReconnect() 1596 case <-reconnect: 1597 doReconnect() 1598 case <-retryKeepAlive: 1599 doKeepAlive() 1600 case <-keepAlive.C: 1601 doKeepAlive() 1602 case <-ctx.Done(): 1603 return 1604 } 1605 } 1606 }() 1607 1608 return nil 1609 } 1610 1611 var subscribeID uint64 1612 1613 func binanceMktID(baseCfg, quoteCfg *bncAssetConfig) string { 1614 return baseCfg.coin + quoteCfg.coin 1615 } 1616 1617 func marketDataStreamID(mktID string) string { 1618 return strings.ToLower(mktID) + "@depth" 1619 } 1620 1621 // subUnsubDepth sends a subscription or unsubscription request to the market 1622 // data stream. 1623 // The marketStreamMtx MUST be held when calling this function. 1624 func (bnc *binance) subUnsubDepth(subscribe bool, mktStreamID string) error { 1625 method := "SUBSCRIBE" 1626 if !subscribe { 1627 method = "UNSUBSCRIBE" 1628 } 1629 1630 req := &bntypes.StreamSubscription{ 1631 Method: method, 1632 Params: []string{mktStreamID}, 1633 ID: atomic.AddUint64(&subscribeID, 1), 1634 } 1635 1636 b, err := json.Marshal(req) 1637 if err != nil { 1638 return fmt.Errorf("error marshaling subscription stream request: %w", err) 1639 } 1640 1641 bnc.log.Debugf("Sending %v for market %v", method, mktStreamID) 1642 if err := bnc.marketStream.SendRaw(b); err != nil { 1643 return fmt.Errorf("error sending subscription stream request: %w", err) 1644 } 1645 1646 return nil 1647 } 1648 1649 func (bnc *binance) handleMarketDataNote(b []byte) { 1650 var note *bntypes.BookNote 1651 if err := json.Unmarshal(b, ¬e); err != nil { 1652 bnc.log.Errorf("Error unmarshaling book note: %v", err) 1653 return 1654 } 1655 if note == nil { 1656 bnc.log.Debugf("Market data update does not parse to a note: %s", string(b)) 1657 return 1658 } 1659 1660 if note.Data == nil { 1661 var waitingResp bool 1662 bnc.marketStreamRespsMtx.Lock() 1663 if ch, exists := bnc.marketStreamResps[note.ID]; exists { 1664 waitingResp = true 1665 timeout := time.After(time.Second * 5) 1666 select { 1667 case ch <- note.Result: 1668 case <-timeout: 1669 bnc.log.Errorf("Noone waiting for market stream result id %d", note.ID) 1670 } 1671 } 1672 bnc.marketStreamRespsMtx.Unlock() 1673 if !waitingResp { 1674 bnc.log.Debugf("No data in market data update: %s", string(b)) 1675 } 1676 return 1677 } 1678 1679 parts := strings.Split(note.StreamName, "@") 1680 if len(parts) != 2 || parts[1] != "depth" { 1681 bnc.log.Errorf("Unknown stream name %q", note.StreamName) 1682 return 1683 } 1684 slug := parts[0] // will be lower-case 1685 mktID := strings.ToUpper(slug) 1686 1687 bnc.booksMtx.Lock() 1688 defer bnc.booksMtx.Unlock() 1689 1690 book := bnc.books[mktID] 1691 if book == nil { 1692 bnc.log.Errorf("No book for stream %q", note.StreamName) 1693 return 1694 } 1695 book.updateQueue <- note.Data 1696 } 1697 1698 func (bnc *binance) getOrderbookSnapshot(ctx context.Context, mktSymbol string) (*bntypes.OrderbookSnapshot, error) { 1699 v := make(url.Values) 1700 v.Add("symbol", strings.ToUpper(mktSymbol)) 1701 v.Add("limit", "1000") 1702 var resp bntypes.OrderbookSnapshot 1703 return &resp, bnc.getAPI(ctx, "/api/v3/depth", v, false, false, &resp) 1704 } 1705 1706 // subscribeToAdditionalMarketDataStream is called when a new market is 1707 // subscribed to after the market data stream connection has already been 1708 // established. 1709 func (bnc *binance) subscribeToAdditionalMarketDataStream(ctx context.Context, baseID, quoteID uint32) (err error) { 1710 baseCfg, quoteCfg, err := bncAssetCfgs(baseID, quoteID) 1711 if err != nil { 1712 return fmt.Errorf("error getting asset cfg for %d: %w", baseID, err) 1713 } 1714 1715 mktID := binanceMktID(baseCfg, quoteCfg) 1716 streamID := marketDataStreamID(mktID) 1717 1718 defer func() { 1719 bnc.marketStream.UpdateURL(bnc.streamURL()) 1720 }() 1721 1722 bnc.booksMtx.Lock() 1723 defer bnc.booksMtx.Unlock() 1724 1725 book, found := bnc.books[mktID] 1726 if found { 1727 book.mtx.Lock() 1728 book.numSubscribers++ 1729 book.mtx.Unlock() 1730 return nil 1731 } 1732 1733 if err := bnc.subUnsubDepth(true, streamID); err != nil { 1734 return fmt.Errorf("error subscribing to %s: %v", streamID, err) 1735 } 1736 1737 getSnapshot := func() (*bntypes.OrderbookSnapshot, error) { 1738 return bnc.getOrderbookSnapshot(ctx, mktID) 1739 } 1740 book = newBinanceOrderBook(baseCfg.conversionFactor, quoteCfg.conversionFactor, mktID, getSnapshot, bnc.log) 1741 bnc.books[mktID] = book 1742 book.sync(ctx) 1743 1744 return nil 1745 } 1746 1747 func (bnc *binance) streams() []string { 1748 bnc.booksMtx.RLock() 1749 defer bnc.booksMtx.RUnlock() 1750 streamNames := make([]string, 0, len(bnc.books)) 1751 for mktID := range bnc.books { 1752 streamNames = append(streamNames, marketDataStreamID(mktID)) 1753 } 1754 return streamNames 1755 } 1756 1757 func (bnc *binance) streamURL() string { 1758 return fmt.Sprintf("%s/stream?streams=%s", bnc.wsURL, strings.Join(bnc.streams(), "/")) 1759 } 1760 1761 // checkSubs will query binance for current market subscriptions and compare 1762 // that to what subscriptions we should have. If there is a discrepancy a 1763 // warning is logged and the market subbed or unsubbed. 1764 func (bnc *binance) checkSubs(ctx context.Context) error { 1765 bnc.marketStreamMtx.Lock() 1766 defer bnc.marketStreamMtx.Unlock() 1767 streams := bnc.streams() 1768 if len(streams) == 0 { 1769 return nil 1770 } 1771 1772 method := "LIST_SUBSCRIPTIONS" 1773 id := atomic.AddUint64(&subscribeID, 1) 1774 1775 resp := make(chan []string, 1) 1776 bnc.marketStreamRespsMtx.Lock() 1777 bnc.marketStreamResps[id] = resp 1778 bnc.marketStreamRespsMtx.Unlock() 1779 1780 defer func() { 1781 bnc.marketStreamRespsMtx.Lock() 1782 delete(bnc.marketStreamResps, id) 1783 bnc.marketStreamRespsMtx.Unlock() 1784 }() 1785 1786 req := &bntypes.StreamSubscription{ 1787 Method: method, 1788 ID: id, 1789 } 1790 1791 b, err := json.Marshal(req) 1792 if err != nil { 1793 return fmt.Errorf("error marshaling subscription stream request: %w", err) 1794 } 1795 1796 bnc.log.Debugf("Sending %v", method) 1797 if err := bnc.marketStream.SendRaw(b); err != nil { 1798 return fmt.Errorf("error sending subscription stream request: %w", err) 1799 } 1800 1801 timeout := time.After(time.Second * 5) 1802 var subs []string 1803 select { 1804 case subs = <-resp: 1805 case <-timeout: 1806 return fmt.Errorf("market stream result id %d did not come.", id) 1807 case <-ctx.Done(): 1808 return nil 1809 } 1810 1811 var sub []string 1812 unsub := make([]string, len(subs)) 1813 for i, s := range subs { 1814 unsub[i] = strings.ToLower(s) 1815 } 1816 1817 out: 1818 for _, us := range streams { 1819 for i, them := range unsub { 1820 if us == them { 1821 unsub[i] = unsub[len(unsub)-1] 1822 unsub = unsub[:len(unsub)-1] 1823 continue out 1824 } 1825 } 1826 sub = append(sub, us) 1827 } 1828 1829 for _, s := range sub { 1830 bnc.log.Warnf("Subbing to previously unsubbed stream %s", s) 1831 if err := bnc.subUnsubDepth(true, s); err != nil { 1832 bnc.log.Errorf("Error subscribing to %s: %v", s, err) 1833 } 1834 } 1835 1836 for _, s := range unsub { 1837 bnc.log.Warnf("Unsubbing to previously subbed stream %s", s) 1838 if err := bnc.subUnsubDepth(false, s); err != nil { 1839 bnc.log.Errorf("Error unsubscribing to %s: %v", s, err) 1840 } 1841 } 1842 1843 return nil 1844 } 1845 1846 // connectToMarketDataStream is called when the first market is subscribed to. 1847 // It creates a connection to the market data stream and starts a goroutine 1848 // to reconnect every 12 hours, as Binance will close the stream every 24 1849 // hours. Additional markets are subscribed to by calling 1850 // subscribeToAdditionalMarketDataStream. 1851 func (bnc *binance) connectToMarketDataStream(ctx context.Context, baseID, quoteID uint32) error { 1852 reconnectC := make(chan struct{}) 1853 checkSubsC := make(chan struct{}) 1854 1855 newConnection := func() (*dex.ConnectionMaster, error) { 1856 // Need to send key but not signature 1857 connectEventFunc := func(cs comms.ConnectionStatus) { 1858 if cs != comms.Disconnected && cs != comms.Connected { 1859 return 1860 } 1861 // If disconnected, set all books to unsynced so bots 1862 // will not place new orders. 1863 connected := cs == comms.Connected 1864 bnc.booksMtx.RLock() 1865 defer bnc.booksMtx.RUnlock() 1866 for _, b := range bnc.books { 1867 select { 1868 case b.connectedChan <- connected: 1869 default: 1870 } 1871 } 1872 } 1873 conn, err := comms.NewWsConn(&comms.WsCfg{ 1874 URL: bnc.streamURL(), 1875 // Binance Docs: The websocket server will send a ping frame every 3 1876 // minutes. If the websocket server does not receive a pong frame 1877 // back from the connection within a 10 minute period, the connection 1878 // will be disconnected. Unsolicited pong frames are allowed. 1879 PingWait: time.Minute * 4, 1880 EchoPingData: true, 1881 ReconnectSync: func() { 1882 bnc.log.Debugf("Binance reconnected") 1883 select { 1884 case checkSubsC <- struct{}{}: 1885 default: 1886 } 1887 }, 1888 ConnectEventFunc: connectEventFunc, 1889 Logger: bnc.log.SubLogger("BNCBOOK"), 1890 RawHandler: bnc.handleMarketDataNote, 1891 }) 1892 if err != nil { 1893 return nil, err 1894 } 1895 1896 bnc.marketStream = conn 1897 cm := dex.NewConnectionMaster(conn) 1898 if err = cm.ConnectOnce(ctx); err != nil { 1899 return nil, fmt.Errorf("websocketHandler remote connect: %v", err) 1900 } 1901 1902 return cm, nil 1903 } 1904 1905 // Add the initial book to the books map 1906 baseCfg, quoteCfg, err := bncAssetCfgs(baseID, quoteID) 1907 if err != nil { 1908 return err 1909 } 1910 mktID := binanceMktID(baseCfg, quoteCfg) 1911 bnc.booksMtx.Lock() 1912 getSnapshot := func() (*bntypes.OrderbookSnapshot, error) { 1913 return bnc.getOrderbookSnapshot(ctx, mktID) 1914 } 1915 book := newBinanceOrderBook(baseCfg.conversionFactor, quoteCfg.conversionFactor, mktID, getSnapshot, bnc.log) 1916 bnc.books[mktID] = book 1917 bnc.booksMtx.Unlock() 1918 1919 // Create initial connection to the market data stream 1920 cm, err := newConnection() 1921 if err != nil { 1922 return fmt.Errorf("error connecting to market data stream : %v", err) 1923 } 1924 1925 book.sync(ctx) 1926 1927 // Start a goroutine to reconnect every 12 hours 1928 go func() { 1929 reconnect := func() error { 1930 bnc.marketStreamMtx.Lock() 1931 defer bnc.marketStreamMtx.Unlock() 1932 oldCm := cm 1933 cm, err = newConnection() 1934 if err != nil { 1935 return err 1936 } 1937 1938 if oldCm != nil { 1939 oldCm.Disconnect() 1940 } 1941 return nil 1942 } 1943 1944 checkSubsInterval := time.Minute 1945 checkSubs := time.After(checkSubsInterval) 1946 reconnectTimer := time.After(time.Hour * 12) 1947 for { 1948 select { 1949 case <-reconnectC: 1950 if err := reconnect(); err != nil { 1951 bnc.log.Errorf("Error reconnecting: %v", err) 1952 reconnectTimer = time.After(time.Second * 30) 1953 checkSubs = make(<-chan time.Time) 1954 continue 1955 } 1956 checkSubs = time.After(checkSubsInterval) 1957 case <-reconnectTimer: 1958 if err := reconnect(); err != nil { 1959 bnc.log.Errorf("Error refreshing connection: %v", err) 1960 reconnectTimer = time.After(time.Second * 30) 1961 checkSubs = make(<-chan time.Time) 1962 continue 1963 } 1964 reconnectTimer = time.After(time.Hour * 12) 1965 checkSubs = time.After(checkSubsInterval) 1966 case <-checkSubs: 1967 if err := bnc.checkSubs(ctx); err != nil { 1968 bnc.log.Errorf("Error checking subscriptions: %v", err) 1969 } 1970 checkSubs = time.After(checkSubsInterval) 1971 case <-checkSubsC: 1972 if err := bnc.checkSubs(ctx); err != nil { 1973 bnc.log.Errorf("Error checking subscriptions: %v", err) 1974 } 1975 checkSubs = time.After(checkSubsInterval) 1976 case <-ctx.Done(): 1977 bnc.marketStreamMtx.Lock() 1978 bnc.marketStream = nil 1979 bnc.marketStreamMtx.Unlock() 1980 if cm != nil { 1981 cm.Disconnect() 1982 } 1983 return 1984 } 1985 } 1986 }() 1987 1988 return nil 1989 } 1990 1991 // UnsubscribeMarket unsubscribes from order book updates on a market. 1992 func (bnc *binance) UnsubscribeMarket(baseID, quoteID uint32) (err error) { 1993 baseCfg, quoteCfg, err := bncAssetCfgs(baseID, quoteID) 1994 if err != nil { 1995 return err 1996 } 1997 mktID := binanceMktID(baseCfg, quoteCfg) 1998 streamID := marketDataStreamID(mktID) 1999 2000 bnc.marketStreamMtx.Lock() 2001 defer bnc.marketStreamMtx.Unlock() 2002 2003 conn := bnc.marketStream 2004 if conn == nil { 2005 return fmt.Errorf("can't unsubscribe. no stream - %p", bnc) 2006 } 2007 2008 var unsubscribe bool 2009 var closer *dex.ConnectionMaster 2010 2011 bnc.booksMtx.Lock() 2012 defer func() { 2013 bnc.booksMtx.Unlock() 2014 2015 conn.UpdateURL(bnc.streamURL()) 2016 2017 if closer != nil { 2018 closer.Disconnect() 2019 } 2020 2021 if unsubscribe { 2022 if err := bnc.subUnsubDepth(false, streamID); err != nil { 2023 bnc.log.Errorf("error unsubscribing from market data stream", err) 2024 } 2025 } 2026 }() 2027 2028 book, found := bnc.books[mktID] 2029 if !found { 2030 unsubscribe = true 2031 return nil 2032 } 2033 2034 book.mtx.Lock() 2035 book.numSubscribers-- 2036 if book.numSubscribers == 0 { 2037 unsubscribe = true 2038 delete(bnc.books, mktID) 2039 closer = book.cm 2040 } 2041 book.mtx.Unlock() 2042 2043 return nil 2044 } 2045 2046 // SubscribeMarket subscribes to order book updates on a market. This must 2047 // be called before calling VWAP. 2048 func (bnc *binance) SubscribeMarket(ctx context.Context, baseID, quoteID uint32) error { 2049 bnc.marketStreamMtx.Lock() 2050 defer bnc.marketStreamMtx.Unlock() 2051 2052 if bnc.marketStream == nil { 2053 bnc.connectToMarketDataStream(ctx, baseID, quoteID) 2054 } 2055 2056 return bnc.subscribeToAdditionalMarketDataStream(ctx, baseID, quoteID) 2057 } 2058 2059 func (bnc *binance) book(baseID, quoteID uint32) (*binanceOrderBook, error) { 2060 baseCfg, quoteCfg, err := bncAssetCfgs(baseID, quoteID) 2061 if err != nil { 2062 return nil, err 2063 } 2064 mktID := binanceMktID(baseCfg, quoteCfg) 2065 2066 bnc.booksMtx.RLock() 2067 book, found := bnc.books[mktID] 2068 bnc.booksMtx.RUnlock() 2069 if !found { 2070 return nil, fmt.Errorf("no book for market %s", mktID) 2071 } 2072 return book, nil 2073 } 2074 2075 func (bnc *binance) Book(baseID, quoteID uint32) (buys, sells []*core.MiniOrder, _ error) { 2076 book, err := bnc.book(baseID, quoteID) 2077 if err != nil { 2078 return nil, nil, err 2079 } 2080 bids, asks := book.book.snap() 2081 bFactor := float64(book.baseConversionFactor) 2082 convertSide := func(side []*obEntry, sell bool) []*core.MiniOrder { 2083 ords := make([]*core.MiniOrder, len(side)) 2084 for i, e := range side { 2085 ords[i] = &core.MiniOrder{ 2086 Qty: float64(e.qty) / bFactor, 2087 QtyAtomic: e.qty, 2088 Rate: calc.ConventionalRateAlt(e.rate, book.baseConversionFactor, book.quoteConversionFactor), 2089 MsgRate: e.rate, 2090 Sell: sell, 2091 } 2092 } 2093 return ords 2094 } 2095 buys = convertSide(bids, false) 2096 sells = convertSide(asks, true) 2097 return 2098 } 2099 2100 // VWAP returns the volume weighted average price for a certain quantity 2101 // of the base asset on a market. SubscribeMarket must be called, and the 2102 // market must be synced before results can be expected. 2103 func (bnc *binance) VWAP(baseID, quoteID uint32, sell bool, qty uint64) (avgPrice, extrema uint64, filled bool, err error) { 2104 book, err := bnc.book(baseID, quoteID) 2105 if err != nil { 2106 return 0, 0, false, err 2107 } 2108 return book.vwap(!sell, qty) 2109 } 2110 2111 func (bnc *binance) MidGap(baseID, quoteID uint32) uint64 { 2112 book, err := bnc.book(baseID, quoteID) 2113 if err != nil { 2114 bnc.log.Errorf("Error getting order book for (%d, %d): %v", baseID, quoteID, err) 2115 return 0 2116 } 2117 return book.midGap() 2118 } 2119 2120 // TradeStatus returns the current status of a trade. 2121 func (bnc *binance) TradeStatus(ctx context.Context, tradeID string, baseID, quoteID uint32) (*Trade, error) { 2122 baseAsset, err := bncAssetCfg(baseID) 2123 if err != nil { 2124 return nil, err 2125 } 2126 2127 quoteAsset, err := bncAssetCfg(quoteID) 2128 if err != nil { 2129 return nil, err 2130 } 2131 2132 v := make(url.Values) 2133 v.Add("symbol", baseAsset.coin+quoteAsset.coin) 2134 v.Add("origClientOrderId", tradeID) 2135 2136 var resp bntypes.BookedOrder 2137 err = bnc.getAPI(ctx, "/api/v3/order", v, true, true, &resp) 2138 if err != nil { 2139 return nil, err 2140 } 2141 2142 return &Trade{ 2143 ID: tradeID, 2144 Sell: resp.Side == "SELL", 2145 Rate: calc.MessageRateAlt(resp.Price, baseAsset.conversionFactor, quoteAsset.conversionFactor), 2146 Qty: uint64(resp.OrigQty * float64(baseAsset.conversionFactor)), 2147 BaseID: baseID, 2148 QuoteID: quoteID, 2149 BaseFilled: uint64(resp.ExecutedQty * float64(baseAsset.conversionFactor)), 2150 QuoteFilled: uint64(resp.CumulativeQuoteQty * float64(quoteAsset.conversionFactor)), 2151 Complete: resp.Status != "NEW" && resp.Status != "PARTIALLY_FILLED", 2152 }, nil 2153 } 2154 2155 func getDEXAssetIDs(coin string, tokenIDs map[string][]uint32) []uint32 { 2156 dexSymbol := convertBnCoin(coin) 2157 2158 isRegistered := func(assetID uint32) bool { 2159 _, err := asset.UnitInfo(assetID) 2160 return err == nil 2161 } 2162 2163 assetIDs := make([]uint32, 0, 1) 2164 if assetID, found := dex.BipSymbolID(dexSymbol); found { 2165 // Only registered assets. 2166 if isRegistered(assetID) { 2167 assetIDs = append(assetIDs, assetID) 2168 } 2169 } 2170 2171 if tokenIDs, found := tokenIDs[coin]; found { 2172 for _, tokenID := range tokenIDs { 2173 if isRegistered(tokenID) { 2174 assetIDs = append(assetIDs, tokenID) 2175 } 2176 } 2177 } 2178 2179 return assetIDs 2180 } 2181 2182 func assetDisabled(isUS bool, assetID uint32) bool { 2183 switch dex.BipIDSymbol(assetID) { 2184 case "zec": 2185 return !isUS // exchange addresses not yet implemented 2186 } 2187 return false 2188 } 2189 2190 // dexMarkets returns all the possible dex markets for this binance market. 2191 // A symbol represents a single market on the CEX, but tokens on the DEX 2192 // have a different assetID for each network they are on, therefore they will 2193 // match multiple markets as defined using assetID. 2194 func binanceMarketToDexMarkets(binanceBaseSymbol, binanceQuoteSymbol string, tokenIDs map[string][]uint32, isUS bool) []*MarketMatch { 2195 var baseAssetIDs, quoteAssetIDs []uint32 2196 2197 baseAssetIDs = getDEXAssetIDs(binanceBaseSymbol, tokenIDs) 2198 if len(baseAssetIDs) == 0 { 2199 return nil 2200 } 2201 2202 quoteAssetIDs = getDEXAssetIDs(binanceQuoteSymbol, tokenIDs) 2203 if len(quoteAssetIDs) == 0 { 2204 return nil 2205 } 2206 2207 markets := make([]*MarketMatch, 0, len(baseAssetIDs)*len(quoteAssetIDs)) 2208 for _, baseID := range baseAssetIDs { 2209 for _, quoteID := range quoteAssetIDs { 2210 if assetDisabled(isUS, baseID) || assetDisabled(isUS, quoteID) { 2211 continue 2212 } 2213 markets = append(markets, &MarketMatch{ 2214 Slug: binanceBaseSymbol + binanceQuoteSymbol, 2215 MarketID: dex.BipIDSymbol(baseID) + "_" + dex.BipIDSymbol(quoteID), 2216 BaseID: baseID, 2217 QuoteID: quoteID, 2218 }) 2219 } 2220 } 2221 2222 return markets 2223 }