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, &note); 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  }