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