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

     1  package mm
     2  
     3  import (
     4  	"context"
     5  	"encoding/hex"
     6  	"fmt"
     7  	"math/rand"
     8  	"reflect"
     9  	"sync"
    10  	"testing"
    11  	"time"
    12  
    13  	"decred.org/dcrdex/client/asset"
    14  	"decred.org/dcrdex/client/core"
    15  	"decred.org/dcrdex/client/db"
    16  	"decred.org/dcrdex/client/mm/libxc"
    17  	"decred.org/dcrdex/client/orderbook"
    18  	"decred.org/dcrdex/dex"
    19  	"decred.org/dcrdex/dex/order"
    20  
    21  	_ "decred.org/dcrdex/client/asset/btc"     // register btc asset
    22  	_ "decred.org/dcrdex/client/asset/dcr"     // register dcr asset
    23  	_ "decred.org/dcrdex/client/asset/eth"     // register eth asset
    24  	_ "decred.org/dcrdex/client/asset/polygon" // register polygon asset
    25  )
    26  
    27  func init() {
    28  	rand.Seed(time.Now().UnixNano())
    29  }
    30  
    31  type tBookFeed struct {
    32  	c chan *core.BookUpdate
    33  }
    34  
    35  func (t *tBookFeed) Next() <-chan *core.BookUpdate { return t.c }
    36  func (t *tBookFeed) Close()                        {}
    37  func (t *tBookFeed) Candles(dur string) error      { return nil }
    38  
    39  var _ core.BookFeed = (*tBookFeed)(nil)
    40  
    41  type tCoin struct {
    42  	coinID []byte
    43  	value  uint64
    44  }
    45  
    46  var _ asset.Coin = (*tCoin)(nil)
    47  
    48  func (c *tCoin) ID() dex.Bytes {
    49  	return c.coinID
    50  }
    51  func (c *tCoin) String() string {
    52  	return hex.EncodeToString(c.coinID)
    53  }
    54  func (c *tCoin) Value() uint64 {
    55  	return c.value
    56  }
    57  func (c *tCoin) TxID() string {
    58  	return hex.EncodeToString(c.coinID)
    59  }
    60  
    61  type sendArgs struct {
    62  	assetID  uint32
    63  	value    uint64
    64  	address  string
    65  	subtract bool
    66  }
    67  
    68  type tCore struct {
    69  	assetBalances     map[uint32]*core.WalletBalance
    70  	assetBalanceErr   error
    71  	market            *core.Market
    72  	singleLotSellFees *OrderFees
    73  	singleLotBuyFees  *OrderFees
    74  	singleLotFeesErr  error
    75  	multiTradeResult  []*core.MultiTradeResult
    76  	noteFeed          chan core.Notification
    77  	isAccountLocker   map[uint32]bool
    78  	isWithdrawer      map[uint32]bool
    79  	isDynamicSwapper  map[uint32]bool
    80  	cancelsPlaced     []order.OrderID
    81  	multiTradesPlaced []*core.MultiTradeForm
    82  	maxFundingFees    uint64
    83  	book              *orderbook.OrderBook
    84  	bookFeed          *tBookFeed
    85  	sends             []*sendArgs
    86  	sendCoin          *tCoin
    87  	newDepositAddress string
    88  	orders            map[order.OrderID]*core.Order
    89  	walletTxsMtx      sync.Mutex
    90  	walletTxs         map[string]*asset.WalletTransaction
    91  	fiatRates         map[uint32]float64
    92  	userParcels       uint32
    93  	parcelLimit       uint32
    94  	exchange          *core.Exchange
    95  	walletStates      map[uint32]*core.WalletState
    96  }
    97  
    98  func newTCore() *tCore {
    99  	return &tCore{
   100  		assetBalances:    make(map[uint32]*core.WalletBalance),
   101  		noteFeed:         make(chan core.Notification),
   102  		isAccountLocker:  make(map[uint32]bool),
   103  		isWithdrawer:     make(map[uint32]bool),
   104  		isDynamicSwapper: make(map[uint32]bool),
   105  		cancelsPlaced:    make([]order.OrderID, 0),
   106  		bookFeed: &tBookFeed{
   107  			c: make(chan *core.BookUpdate, 1),
   108  		},
   109  		walletTxs:    make(map[string]*asset.WalletTransaction),
   110  		book:         &orderbook.OrderBook{},
   111  		walletStates: make(map[uint32]*core.WalletState),
   112  	}
   113  }
   114  
   115  var _ clientCore = (*tCore)(nil)
   116  
   117  func (c *tCore) NotificationFeed() *core.NoteFeed {
   118  	return &core.NoteFeed{C: c.noteFeed}
   119  }
   120  func (c *tCore) ExchangeMarket(host string, base, quote uint32) (*core.Market, error) {
   121  	return c.market, nil
   122  }
   123  
   124  func (t *tCore) SyncBook(host string, base, quote uint32) (*orderbook.OrderBook, core.BookFeed, error) {
   125  	return t.book, t.bookFeed, nil
   126  }
   127  func (*tCore) SupportedAssets() map[uint32]*core.SupportedAsset {
   128  	return nil
   129  }
   130  func (c *tCore) SingleLotFees(form *core.SingleLotFeesForm) (uint64, uint64, uint64, error) {
   131  	if c.singleLotFeesErr != nil {
   132  		return 0, 0, 0, c.singleLotFeesErr
   133  	}
   134  	if c.singleLotSellFees == nil && c.singleLotBuyFees == nil {
   135  		return 0, 0, 0, fmt.Errorf("no fees set")
   136  	}
   137  
   138  	if form.Sell {
   139  		return c.singleLotSellFees.Max.Swap, c.singleLotSellFees.Max.Redeem, c.singleLotSellFees.Max.Refund, nil
   140  	}
   141  	return c.singleLotBuyFees.Max.Swap, c.singleLotBuyFees.Max.Redeem, c.singleLotBuyFees.Max.Refund, nil
   142  }
   143  func (c *tCore) Cancel(oidB dex.Bytes) error {
   144  	var oid order.OrderID
   145  	copy(oid[:], oidB)
   146  	c.cancelsPlaced = append(c.cancelsPlaced, oid)
   147  	return nil
   148  }
   149  func (c *tCore) AssetBalance(assetID uint32) (*core.WalletBalance, error) {
   150  	return c.assetBalances[assetID], c.assetBalanceErr
   151  }
   152  func (c *tCore) MultiTrade(pw []byte, forms *core.MultiTradeForm) []*core.MultiTradeResult {
   153  	c.multiTradesPlaced = append(c.multiTradesPlaced, forms)
   154  	return c.multiTradeResult
   155  }
   156  func (c *tCore) WalletTraits(assetID uint32) (asset.WalletTrait, error) {
   157  	isAccountLocker := c.isAccountLocker[assetID]
   158  	isWithdrawer := c.isWithdrawer[assetID]
   159  	isDynamicSwapper := c.isDynamicSwapper[assetID]
   160  
   161  	var traits asset.WalletTrait
   162  	if isAccountLocker {
   163  		traits |= asset.WalletTraitAccountLocker
   164  	}
   165  	if isWithdrawer {
   166  		traits |= asset.WalletTraitWithdrawer
   167  	}
   168  	if isDynamicSwapper {
   169  		traits |= asset.WalletTraitDynamicSwapper
   170  	}
   171  
   172  	return traits, nil
   173  }
   174  func (c *tCore) MaxFundingFees(fromAsset uint32, host string, numTrades uint32, options map[string]string) (uint64, error) {
   175  	return c.maxFundingFees, nil
   176  }
   177  func (c *tCore) Login(pw []byte) error {
   178  	return nil
   179  }
   180  func (c *tCore) OpenWallet(assetID uint32, pw []byte) error {
   181  	return nil
   182  }
   183  func (c *tCore) WalletTransaction(assetID uint32, txID string) (*asset.WalletTransaction, error) {
   184  	c.walletTxsMtx.Lock()
   185  	defer c.walletTxsMtx.Unlock()
   186  	return c.walletTxs[txID], nil
   187  }
   188  
   189  func (c *tCore) Network() dex.Network {
   190  	return dex.Simnet
   191  }
   192  
   193  func (c *tCore) FiatConversionRates() map[uint32]float64 {
   194  	return c.fiatRates
   195  }
   196  func (c *tCore) Broadcast(core.Notification) {}
   197  func (c *tCore) TradingLimits(host string) (userParcels, parcelLimit uint32, err error) {
   198  	return c.userParcels, c.parcelLimit, nil
   199  }
   200  
   201  func (c *tCore) Send(pw []byte, assetID uint32, value uint64, address string, subtract bool) (asset.Coin, error) {
   202  	c.sends = append(c.sends, &sendArgs{
   203  		assetID:  assetID,
   204  		value:    value,
   205  		address:  address,
   206  		subtract: subtract,
   207  	})
   208  	return c.sendCoin, nil
   209  }
   210  func (c *tCore) NewDepositAddress(assetID uint32) (string, error) {
   211  	return c.newDepositAddress, nil
   212  }
   213  func (c *tCore) Order(id dex.Bytes) (*core.Order, error) {
   214  	var oid order.OrderID
   215  	copy(oid[:], id)
   216  	if o, found := c.orders[oid]; found {
   217  		return o, nil
   218  	}
   219  	return nil, fmt.Errorf("order %s not found", id)
   220  }
   221  
   222  func (c *tCore) Exchange(host string) (*core.Exchange, error) {
   223  	return c.exchange, nil
   224  }
   225  
   226  func (c *tCore) WalletState(assetID uint32) *core.WalletState {
   227  	return c.walletStates[assetID]
   228  }
   229  
   230  func (c *tCore) setWalletsAndExchange(m *core.Market) {
   231  	c.walletStates[m.BaseID] = &core.WalletState{
   232  		PeerCount: 1,
   233  		Synced:    true,
   234  	}
   235  	c.walletStates[m.QuoteID] = &core.WalletState{
   236  		PeerCount: 1,
   237  		Synced:    true,
   238  	}
   239  	c.exchange = &core.Exchange{
   240  		Auth: core.ExchangeAuth{
   241  			EffectiveTier: 2,
   242  		},
   243  	}
   244  }
   245  
   246  func (c *tCore) setAssetBalances(balances map[uint32]uint64) {
   247  	c.assetBalances = make(map[uint32]*core.WalletBalance)
   248  	for assetID, bal := range balances {
   249  		c.assetBalances[assetID] = &core.WalletBalance{
   250  			Balance: &db.Balance{
   251  				Balance: asset.Balance{
   252  					Available: bal,
   253  				},
   254  			},
   255  		}
   256  	}
   257  }
   258  
   259  type dexOrder struct {
   260  	rate uint64
   261  	qty  uint64
   262  	sell bool
   263  }
   264  
   265  type tBotCoreAdaptor struct {
   266  	clientCore
   267  	tCore *tCore
   268  
   269  	balances         map[uint32]*BotBalance
   270  	groupedBuys      map[uint64][]*core.Order
   271  	groupedSells     map[uint64][]*core.Order
   272  	orderUpdates     chan *core.Order
   273  	buyFees          *OrderFees
   274  	sellFees         *OrderFees
   275  	fiatExchangeRate uint64
   276  	buyFeesInBase    uint64
   277  	sellFeesInBase   uint64
   278  	buyFeesInQuote   uint64
   279  	sellFeesInQuote  uint64
   280  	maxBuyQty        uint64
   281  	maxSellQty       uint64
   282  	lastTradePlaced  *dexOrder
   283  	tradeResult      *core.Order
   284  }
   285  
   286  func (c *tBotCoreAdaptor) DEXBalance(assetID uint32) (*BotBalance, error) {
   287  	if c.tCore.assetBalanceErr != nil {
   288  		return nil, c.tCore.assetBalanceErr
   289  	}
   290  	return c.balances[assetID], nil
   291  }
   292  
   293  func (c *tBotCoreAdaptor) GroupedBookedOrders() (buys, sells map[uint64][]*core.Order) {
   294  	return c.groupedBuys, c.groupedSells
   295  }
   296  
   297  func (c *tBotCoreAdaptor) CancelAllOrders() bool { return false }
   298  
   299  func (c *tBotCoreAdaptor) ExchangeRateFromFiatSources() uint64 {
   300  	return c.fiatExchangeRate
   301  }
   302  
   303  func (c *tBotCoreAdaptor) OrderFees() (buyFees, sellFees *OrderFees, err error) {
   304  	return c.buyFees, c.sellFees, nil
   305  }
   306  
   307  func (c *tBotCoreAdaptor) SubscribeOrderUpdates() (updates <-chan *core.Order) {
   308  	return c.orderUpdates
   309  }
   310  
   311  func (c *tBotCoreAdaptor) OrderFeesInUnits(sell, base bool, rate uint64) (uint64, error) {
   312  	if sell && base {
   313  		return c.sellFeesInBase, nil
   314  	}
   315  	if sell && !base {
   316  		return c.sellFeesInQuote, nil
   317  	}
   318  	if !sell && base {
   319  		return c.buyFeesInBase, nil
   320  	}
   321  	return c.buyFeesInQuote, nil
   322  }
   323  
   324  func (c *tBotCoreAdaptor) SufficientBalanceForDEXTrade(rate, qty uint64, sell bool) (bool, error) {
   325  	if sell {
   326  		return qty <= c.maxSellQty, nil
   327  	}
   328  	return qty <= c.maxBuyQty, nil
   329  }
   330  
   331  func (c *tBotCoreAdaptor) DEXTrade(rate, qty uint64, sell bool) (*core.Order, error) {
   332  	c.lastTradePlaced = &dexOrder{
   333  		rate: rate,
   334  		qty:  qty,
   335  		sell: sell,
   336  	}
   337  	return c.tradeResult, nil
   338  }
   339  
   340  func (u *tBotCoreAdaptor) registerFeeGap(s *FeeGapStats) {}
   341  
   342  func (u *tBotCoreAdaptor) checkBotHealth() bool {
   343  	return true
   344  }
   345  
   346  func newTBotCoreAdaptor(c *tCore) *tBotCoreAdaptor {
   347  	return &tBotCoreAdaptor{
   348  		clientCore:   c,
   349  		tCore:        c,
   350  		orderUpdates: make(chan *core.Order),
   351  	}
   352  }
   353  
   354  var _ botCoreAdaptor = (*tBotCoreAdaptor)(nil)
   355  
   356  type tOrderBook struct {
   357  	midGap    uint64
   358  	midGapErr error
   359  
   360  	bidsVWAP map[uint64]vwapResult
   361  	asksVWAP map[uint64]vwapResult
   362  	vwapErr  error
   363  }
   364  
   365  var _ dexOrderBook = (*tOrderBook)(nil)
   366  
   367  func (t *tOrderBook) VWAP(numLots, _ uint64, sell bool) (avg, extrema uint64, filled bool, err error) {
   368  	if t.vwapErr != nil {
   369  		return 0, 0, false, t.vwapErr
   370  	}
   371  
   372  	if sell {
   373  		res, found := t.asksVWAP[numLots]
   374  		if !found {
   375  			return 0, 0, false, nil
   376  		}
   377  		return res.avg, res.extrema, true, nil
   378  	}
   379  
   380  	res, found := t.bidsVWAP[numLots]
   381  	if !found {
   382  		return 0, 0, false, nil
   383  	}
   384  	return res.avg, res.extrema, true, nil
   385  }
   386  
   387  func (o *tOrderBook) MidGap() (uint64, error) {
   388  	if o.midGapErr != nil {
   389  		return 0, o.midGapErr
   390  	}
   391  	return o.midGap, nil
   392  }
   393  
   394  type tOracle struct {
   395  	marketPrice float64
   396  }
   397  
   398  func (o *tOracle) getMarketPrice(base, quote uint32) float64 {
   399  	return o.marketPrice
   400  }
   401  
   402  type vwapResult struct {
   403  	avg     uint64
   404  	extrema uint64
   405  }
   406  
   407  type withdrawArgs struct {
   408  	address string
   409  	amt     uint64
   410  	assetID uint32
   411  	txID    string
   412  }
   413  
   414  type tCEX struct {
   415  	bidsVWAP             map[uint64]vwapResult
   416  	asksVWAP             map[uint64]vwapResult
   417  	vwapErr              error
   418  	balances             map[uint32]*libxc.ExchangeBalance
   419  	balanceErr           error
   420  	tradeID              string
   421  	tradeErr             error
   422  	lastTrade            *libxc.Trade
   423  	cancelledTrades      []string
   424  	cancelTradeErr       error
   425  	tradeUpdates         chan *libxc.Trade
   426  	tradeUpdatesID       int
   427  	depositAddress       string
   428  	withdrawals          []*withdrawArgs
   429  	confirmWithdrawalMtx sync.Mutex
   430  	confirmWithdrawal    *withdrawArgs
   431  	withdrawalID         string
   432  	confirmDepositMtx    sync.Mutex
   433  	confirmedDeposit     *uint64
   434  	tradeStatus          *libxc.Trade
   435  }
   436  
   437  func newTCEX() *tCEX {
   438  	return &tCEX{
   439  		bidsVWAP:        make(map[uint64]vwapResult),
   440  		asksVWAP:        make(map[uint64]vwapResult),
   441  		balances:        make(map[uint32]*libxc.ExchangeBalance),
   442  		cancelledTrades: make([]string, 0),
   443  		tradeUpdates:    make(chan *libxc.Trade),
   444  	}
   445  }
   446  
   447  var _ libxc.CEX = (*tCEX)(nil)
   448  
   449  func (c *tCEX) Connect(ctx context.Context) (*sync.WaitGroup, error) {
   450  	return &sync.WaitGroup{}, nil
   451  }
   452  func (c *tCEX) Balances(ctx context.Context) (map[uint32]*libxc.ExchangeBalance, error) {
   453  	return nil, nil
   454  }
   455  func (c *tCEX) MatchedMarkets(ctx context.Context) ([]*libxc.MarketMatch, error) {
   456  	return nil, nil
   457  }
   458  func (c *tCEX) Markets(ctx context.Context) (map[string]*libxc.Market, error) {
   459  	return nil, nil
   460  }
   461  func (c *tCEX) Balance(assetID uint32) (*libxc.ExchangeBalance, error) {
   462  	return c.balances[assetID], c.balanceErr
   463  }
   464  func (c *tCEX) Trade(ctx context.Context, baseID, quoteID uint32, sell bool, rate, qty uint64, updaterID int) (*libxc.Trade, error) {
   465  	if c.tradeErr != nil {
   466  		return nil, c.tradeErr
   467  	}
   468  	c.lastTrade = &libxc.Trade{
   469  		ID:      c.tradeID,
   470  		BaseID:  baseID,
   471  		QuoteID: quoteID,
   472  		Rate:    rate,
   473  		Sell:    sell,
   474  		Qty:     qty,
   475  	}
   476  	return c.lastTrade, nil
   477  }
   478  func (c *tCEX) CancelTrade(ctx context.Context, seID, quoteID uint32, tradeID string) error {
   479  	if c.cancelTradeErr != nil {
   480  		return c.cancelTradeErr
   481  	}
   482  	c.cancelledTrades = append(c.cancelledTrades, tradeID)
   483  	return nil
   484  }
   485  func (c *tCEX) SubscribeMarket(ctx context.Context, baseID, quoteID uint32) error {
   486  	return nil
   487  }
   488  func (c *tCEX) UnsubscribeMarket(baseID, quoteID uint32) error {
   489  	return nil
   490  }
   491  func (c *tCEX) VWAP(baseID, quoteID uint32, sell bool, qty uint64) (vwap, extrema uint64, filled bool, err error) {
   492  	if c.vwapErr != nil {
   493  		return 0, 0, false, c.vwapErr
   494  	}
   495  
   496  	if sell {
   497  		res, found := c.asksVWAP[qty]
   498  		if !found {
   499  			return 0, 0, false, nil
   500  		}
   501  		return res.avg, res.extrema, true, nil
   502  	}
   503  
   504  	res, found := c.bidsVWAP[qty]
   505  	if !found {
   506  		return 0, 0, false, nil
   507  	}
   508  	return res.avg, res.extrema, true, nil
   509  }
   510  func (c *tCEX) MidGap(baseID, quoteID uint32) uint64 { return 0 }
   511  func (c *tCEX) SubscribeTradeUpdates() (<-chan *libxc.Trade, func(), int) {
   512  	return c.tradeUpdates, func() {}, c.tradeUpdatesID
   513  }
   514  func (c *tCEX) GetDepositAddress(ctx context.Context, assetID uint32) (string, error) {
   515  	return c.depositAddress, nil
   516  }
   517  
   518  func (c *tCEX) Withdraw(ctx context.Context, assetID uint32, qty uint64, address string) (string, error) {
   519  	c.withdrawals = append(c.withdrawals, &withdrawArgs{
   520  		address: address,
   521  		amt:     qty,
   522  		assetID: assetID,
   523  	})
   524  
   525  	return c.withdrawalID, nil
   526  }
   527  
   528  func (c *tCEX) ConfirmWithdrawal(ctx context.Context, withdrawalID string, assetID uint32) (uint64, string, error) {
   529  	c.confirmWithdrawalMtx.Lock()
   530  	defer c.confirmWithdrawalMtx.Unlock()
   531  
   532  	if c.confirmWithdrawal == nil {
   533  		return 0, "", libxc.ErrWithdrawalPending
   534  	}
   535  	return c.confirmWithdrawal.amt, c.confirmWithdrawal.txID, nil
   536  }
   537  
   538  func (c *tCEX) ConfirmDeposit(ctx context.Context, deposit *libxc.DepositData) (bool, uint64) {
   539  	c.confirmDepositMtx.Lock()
   540  	defer c.confirmDepositMtx.Unlock()
   541  
   542  	if c.confirmedDeposit != nil {
   543  		return true, *c.confirmedDeposit
   544  	}
   545  	return false, 0
   546  }
   547  
   548  func (c *tCEX) TradeStatus(ctx context.Context, id string, baseID, quoteID uint32) (*libxc.Trade, error) {
   549  	return c.tradeStatus, nil
   550  }
   551  
   552  func (c *tCEX) Book(baseID, quoteID uint32) (buys, sells []*core.MiniOrder, _ error) {
   553  	return nil, nil, nil
   554  }
   555  
   556  type prepareRebalanceResult struct {
   557  	rebalance   int64
   558  	cexReserves uint64
   559  	dexReserves uint64
   560  }
   561  
   562  type tBotCexAdaptor struct {
   563  	balances        map[uint32]*BotBalance
   564  	balanceErr      error
   565  	tradeID         string
   566  	tradeErr        error
   567  	lastTrade       *libxc.Trade
   568  	cancelledTrades []string
   569  	cancelTradeErr  error
   570  	tradeUpdates    chan *libxc.Trade
   571  	maxBuyQty       uint64
   572  	maxSellQty      uint64
   573  }
   574  
   575  func newTBotCEXAdaptor() *tBotCexAdaptor {
   576  	return &tBotCexAdaptor{
   577  		balances:        make(map[uint32]*BotBalance),
   578  		cancelledTrades: make([]string, 0),
   579  		tradeUpdates:    make(chan *libxc.Trade),
   580  	}
   581  }
   582  
   583  var _ botCexAdaptor = (*tBotCexAdaptor)(nil)
   584  
   585  var tLogger = dex.StdOutLogger("mm_TEST", dex.LevelInfo)
   586  
   587  func (c *tBotCexAdaptor) CEXBalance(assetID uint32) (*BotBalance, error) {
   588  	return c.balances[assetID], c.balanceErr
   589  }
   590  func (c *tBotCexAdaptor) CancelTrade(ctx context.Context, baseID, quoteID uint32, tradeID string) error {
   591  	if c.cancelTradeErr != nil {
   592  		return c.cancelTradeErr
   593  	}
   594  	c.cancelledTrades = append(c.cancelledTrades, tradeID)
   595  	return nil
   596  }
   597  func (c *tBotCexAdaptor) SubscribeMarket(ctx context.Context, baseID, quoteID uint32) error {
   598  	return nil
   599  }
   600  func (c *tBotCexAdaptor) SubscribeTradeUpdates() (updates <-chan *libxc.Trade) {
   601  	return c.tradeUpdates
   602  }
   603  func (c *tBotCexAdaptor) CEXTrade(ctx context.Context, baseID, quoteID uint32, sell bool, rate, qty uint64) (*libxc.Trade, error) {
   604  	if c.tradeErr != nil {
   605  		return nil, c.tradeErr
   606  	}
   607  
   608  	c.lastTrade = &libxc.Trade{
   609  		ID:      c.tradeID,
   610  		BaseID:  baseID,
   611  		QuoteID: quoteID,
   612  		Rate:    rate,
   613  		Sell:    sell,
   614  		Qty:     qty,
   615  	}
   616  	return c.lastTrade, nil
   617  }
   618  func (c *tBotCexAdaptor) FreeUpFunds(assetID uint32, cex bool, amt uint64, currEpoch uint64) {
   619  }
   620  
   621  func (c *tBotCexAdaptor) MidGap(baseID, quoteID uint32) uint64 { return 0 }
   622  func (c *tBotCexAdaptor) SufficientBalanceForCEXTrade(baseID, quoteID uint32, sell bool, rate, qty uint64) bool {
   623  	if sell {
   624  		return qty <= c.maxSellQty
   625  	}
   626  	return qty <= c.maxBuyQty
   627  }
   628  
   629  func (c *tBotCexAdaptor) Book() (_, _ []*core.MiniOrder, _ error) { return nil, nil, nil }
   630  
   631  type tExchangeAdaptor struct {
   632  	dexBalances map[uint32]*BotBalance
   633  	cexBalances map[uint32]*BotBalance
   634  	cfg         *BotConfig
   635  }
   636  
   637  var _ bot = (*tExchangeAdaptor)(nil)
   638  
   639  func (t *tExchangeAdaptor) Connect(ctx context.Context) (*sync.WaitGroup, error) {
   640  	return &sync.WaitGroup{}, nil
   641  }
   642  
   643  func (t *tExchangeAdaptor) refreshAllPendingEvents(context.Context) {}
   644  func (t *tExchangeAdaptor) balances() map[uint32]*BotBalances {
   645  	return nil
   646  }
   647  func (t *tExchangeAdaptor) DEXBalance(assetID uint32) *BotBalance {
   648  	if t.dexBalances[assetID] == nil {
   649  		return &BotBalance{}
   650  	}
   651  	return t.dexBalances[assetID]
   652  }
   653  func (t *tExchangeAdaptor) CEXBalance(assetID uint32) *BotBalance {
   654  	if t.cexBalances[assetID] == nil {
   655  		return &BotBalance{}
   656  	}
   657  	return t.cexBalances[assetID]
   658  }
   659  func (t *tExchangeAdaptor) stats() *RunStats { return nil }
   660  func (t *tExchangeAdaptor) updateConfig(cfg *BotConfig) error {
   661  	t.cfg = cfg
   662  	return nil
   663  }
   664  func (t *tExchangeAdaptor) updateInventory(diffs *BotInventoryDiffs) {}
   665  func (t *tExchangeAdaptor) timeStart() int64                         { return 0 }
   666  func (t *tExchangeAdaptor) Book() (buys, sells []*core.MiniOrder, _ error) {
   667  	return nil, nil, nil
   668  }
   669  func (t *tExchangeAdaptor) sendStatsUpdate()                {}
   670  func (t *tExchangeAdaptor) withPause(func() error) error    { return nil }
   671  func (t *tExchangeAdaptor) botCfg() *BotConfig              { return t.cfg }
   672  func (t *tExchangeAdaptor) latestEpoch() *EpochReport       { return &EpochReport{} }
   673  func (t *tExchangeAdaptor) latestCEXProblems() *CEXProblems { return nil }
   674  
   675  func TestAvailableBalances(t *testing.T) {
   676  	ctx, cancel := context.WithCancel(context.Background())
   677  	defer cancel()
   678  
   679  	tCore := newTCore()
   680  
   681  	ethBtc := &MarketWithHost{
   682  		Host:    "dex.com",
   683  		BaseID:  60,
   684  		QuoteID: 0,
   685  	}
   686  
   687  	dcrBtc := &MarketWithHost{
   688  		Host:    "dex.com",
   689  		BaseID:  42,
   690  		QuoteID: 0,
   691  	}
   692  
   693  	dcrUsdc := &MarketWithHost{
   694  		Host:    "dex.com",
   695  		BaseID:  42,
   696  		QuoteID: 60001,
   697  	}
   698  
   699  	btcUsdc := &MarketWithHost{
   700  		Host:    "dex.com",
   701  		BaseID:  0,
   702  		QuoteID: 60001,
   703  	}
   704  
   705  	cfg := &MarketMakingConfig{
   706  		BotConfigs: []*BotConfig{
   707  			{
   708  				Host:    "dex.com",
   709  				BaseID:  42,
   710  				QuoteID: 0,
   711  			},
   712  			{
   713  				Host:    "dex.com",
   714  				BaseID:  60,
   715  				QuoteID: 0,
   716  				CEXName: libxc.Binance,
   717  			},
   718  			{
   719  				Host:    "dex.com",
   720  				BaseID:  0,
   721  				QuoteID: 60001,
   722  				CEXName: libxc.Binance,
   723  			},
   724  			{
   725  				Host:    "dex.com",
   726  				BaseID:  42,
   727  				QuoteID: 60001,
   728  				CEXName: libxc.BinanceUS,
   729  			},
   730  		},
   731  		CexConfigs: []*CEXConfig{
   732  			{
   733  				Name: libxc.Binance,
   734  			},
   735  			{
   736  				Name: libxc.BinanceUS,
   737  			},
   738  		},
   739  	}
   740  
   741  	binance := newTCEX()
   742  	binance.balances = map[uint32]*libxc.ExchangeBalance{
   743  		60:    {Available: 9e5},
   744  		0:     {Available: 8e5},
   745  		60001: {Available: 6e5},
   746  	}
   747  
   748  	binanceUS := newTCEX()
   749  	binanceUS.balances = map[uint32]*libxc.ExchangeBalance{
   750  		42:    {Available: 7e5},
   751  		60001: {Available: 6e5},
   752  	}
   753  
   754  	mm := MarketMaker{
   755  		ctx:         ctx,
   756  		log:         tLogger,
   757  		core:        tCore,
   758  		defaultCfg:  cfg,
   759  		runningBots: make(map[MarketWithHost]*runningBot),
   760  	}
   761  
   762  	mm.cexes = map[string]*centralizedExchange{
   763  		libxc.Binance:   {CEX: binance, CEXConfig: &CEXConfig{Name: libxc.Binance}},
   764  		libxc.BinanceUS: {CEX: binanceUS, CEXConfig: &CEXConfig{Name: libxc.BinanceUS}},
   765  	}
   766  
   767  	tCore.setAssetBalances(map[uint32]uint64{
   768  		42:    9e5,
   769  		60:    8e5,
   770  		0:     7e5,
   771  		60001: 6e5,
   772  	})
   773  
   774  	checkAvailableBalances := func(mkt *MarketWithHost, expDex, expCex map[uint32]uint64) {
   775  		t.Helper()
   776  		dexBalances, cexBalances, err := mm.AvailableBalances(mkt, nil)
   777  		if err != nil {
   778  			t.Fatalf("unexpected error: %v", err)
   779  		}
   780  		if !reflect.DeepEqual(dexBalances, expDex) {
   781  			t.Fatalf("unexpected dex balances. wanted %v, got %v", expDex, dexBalances)
   782  		}
   783  		if !reflect.DeepEqual(cexBalances, expCex) {
   784  			t.Fatalf("unexpected cex balances. wanted %v, got %v", expCex, cexBalances)
   785  		}
   786  	}
   787  
   788  	// No running bots
   789  	checkAvailableBalances(dcrBtc, map[uint32]uint64{42: 9e5, 0: 7e5}, map[uint32]uint64{})
   790  	checkAvailableBalances(ethBtc, map[uint32]uint64{60: 8e5, 0: 7e5}, map[uint32]uint64{60: 9e5, 0: 8e5})
   791  	checkAvailableBalances(btcUsdc, map[uint32]uint64{0: 7e5, 60: 8e5, 60001: 6e5}, map[uint32]uint64{0: 8e5, 60001: 6e5})
   792  	checkAvailableBalances(dcrUsdc, map[uint32]uint64{42: 9e5, 60: 8e5, 60001: 6e5}, map[uint32]uint64{42: 7e5, 60001: 6e5})
   793  
   794  	rb := &runningBot{
   795  		bot: &tExchangeAdaptor{
   796  			dexBalances: map[uint32]*BotBalance{
   797  				60:    {Available: 1e5},
   798  				0:     {Available: 4e5},
   799  				60001: {Available: 2e5},
   800  			},
   801  			cexBalances: map[uint32]*BotBalance{
   802  				60001: {Available: 2e5},
   803  				0:     {Available: 3e5},
   804  			},
   805  			cfg: cfg.BotConfigs[1],
   806  		},
   807  	}
   808  	mm.runningBots[*btcUsdc] = rb
   809  
   810  	checkAvailableBalances(dcrBtc, map[uint32]uint64{42: 9e5, 0: 3e5}, map[uint32]uint64{})
   811  	checkAvailableBalances(ethBtc, map[uint32]uint64{60: 7e5, 0: 3e5}, map[uint32]uint64{60: 9e5, 0: 5e5})
   812  	checkAvailableBalances(btcUsdc, map[uint32]uint64{0: 3e5, 60: 7e5, 60001: 4e5}, map[uint32]uint64{0: 5e5, 60001: 4e5})
   813  	checkAvailableBalances(dcrUsdc, map[uint32]uint64{42: 9e5, 60: 7e5, 60001: 4e5}, map[uint32]uint64{42: 7e5, 60001: 6e5})
   814  }