decred.org/dcrdex@v1.0.3/client/mm/libxc/binance.go (about)

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