decred.org/dcrdex@v1.0.5/client/webserver/live_test.go (about)

     1  //go:build live && !nolgpl
     2  
     3  // Run a test server with
     4  // go test -v -tags live -run Server -timeout 60m
     5  // test server will run for 1 hour and serve randomness.
     6  
     7  package webserver
     8  
     9  import (
    10  	"context"
    11  	"encoding/binary"
    12  	"encoding/hex"
    13  	"encoding/json"
    14  	"fmt"
    15  	"math"
    16  	mrand "math/rand"
    17  	"sort"
    18  	"strconv"
    19  	"strings"
    20  	"sync"
    21  	"sync/atomic"
    22  	"testing"
    23  	"time"
    24  
    25  	"decred.org/dcrdex/client/asset"
    26  	"decred.org/dcrdex/client/asset/btc"
    27  	"decred.org/dcrdex/client/asset/dcr"
    28  	"decred.org/dcrdex/client/asset/eth"
    29  	"decred.org/dcrdex/client/asset/ltc"
    30  	"decred.org/dcrdex/client/asset/polygon"
    31  	"decred.org/dcrdex/client/asset/zec"
    32  	"decred.org/dcrdex/client/comms"
    33  	"decred.org/dcrdex/client/core"
    34  	"decred.org/dcrdex/client/db"
    35  	"decred.org/dcrdex/client/mm"
    36  	"decred.org/dcrdex/client/mm/libxc"
    37  	"decred.org/dcrdex/client/mnemonic"
    38  	"decred.org/dcrdex/client/orderbook"
    39  	"decred.org/dcrdex/dex"
    40  	"decred.org/dcrdex/dex/calc"
    41  	"decred.org/dcrdex/dex/candles"
    42  	"decred.org/dcrdex/dex/encode"
    43  	"decred.org/dcrdex/dex/msgjson"
    44  	dexbch "decred.org/dcrdex/dex/networks/bch"
    45  	dexbtc "decred.org/dcrdex/dex/networks/btc"
    46  	"decred.org/dcrdex/dex/order"
    47  	ordertest "decred.org/dcrdex/dex/order/test"
    48  	"golang.org/x/text/cases"
    49  	"golang.org/x/text/language"
    50  )
    51  
    52  const (
    53  	firstDEX           = "somedex.com"
    54  	secondDEX          = "thisdexwithalongname.com"
    55  	unsupportedAssetID = 141 // kmd
    56  )
    57  
    58  var (
    59  	tCtx                  context.Context
    60  	maxDelay                     = time.Second * 4
    61  	epochDuration                = time.Second * 30 // milliseconds
    62  	feedPeriod                   = time.Second * 10
    63  	creationPendingAsset  uint32 = 0xFFFFFFFF
    64  	forceDisconnectWallet bool
    65  	wipeWalletBalance     bool
    66  	gapWidthFactor        = 1.0 // Should be 0 < gapWidthFactor <= 1.0
    67  	randomPokes           = false
    68  	randomNotes           = false
    69  	numUserOrders         = 10
    70  	conversionFactor      = dexbtc.UnitInfo.Conventional.ConversionFactor
    71  	delayBalance          = false
    72  	doubleCreateAsyncErr  = false
    73  	randomizeOrdersCount  = false
    74  	initErrors            = false
    75  	mmConnectErrors       = false
    76  	enableActions         = false
    77  	actions               []*asset.ActionRequiredNote
    78  
    79  	rand   = mrand.New(mrand.NewSource(time.Now().UnixNano()))
    80  	titler = cases.Title(language.AmericanEnglish)
    81  )
    82  
    83  func dummySettings() map[string]string {
    84  	return map[string]string{
    85  		"rpcuser":     "dexuser",
    86  		"rpcpassword": "dexpass",
    87  		"rpcbind":     "127.0.0.1:54321",
    88  		"rpcport":     "",
    89  		"fallbackfee": "20",
    90  		"txsplit":     "0",
    91  		"username":    "dexuser",
    92  		"password":    "dexpass",
    93  		"rpclisten":   "127.0.0.1:54321",
    94  		"rpccert":     "/home/me/dex/rpc.cert",
    95  	}
    96  }
    97  
    98  func randomDelay() {
    99  	time.Sleep(time.Duration(rand.Float64() * float64(maxDelay)))
   100  }
   101  
   102  // A random number with a random order of magnitude.
   103  func randomMagnitude(low, high int) float64 {
   104  	exponent := rand.Intn(high-low) + low
   105  	mantissa := rand.Float64() * 10
   106  	return mantissa * math.Pow10(exponent)
   107  }
   108  
   109  func userOrders(mktID string) (ords []*core.Order) {
   110  	orderCount := rand.Intn(numUserOrders)
   111  	for i := 0; i < orderCount; i++ {
   112  		midGap, maxQty := getMarketStats(mktID)
   113  		sell := rand.Intn(2) > 0
   114  		ord := randomOrder(sell, maxQty, midGap, gapWidthFactor*midGap, false)
   115  		qty := uint64(ord.Qty * 1e8)
   116  		filled := uint64(rand.Float64() * float64(qty))
   117  		orderType := order.OrderType(rand.Intn(2) + 1)
   118  		status := order.OrderStatusEpoch
   119  		epoch := uint64(time.Now().UnixMilli()) / uint64(epochDuration.Milliseconds())
   120  		isLimit := orderType == order.LimitOrderType
   121  		if rand.Float32() > 0.5 {
   122  			epoch -= 1
   123  			if isLimit {
   124  				status = order.OrderStatusBooked
   125  			} else {
   126  				status = order.OrderStatusExecuted
   127  			}
   128  		}
   129  		var tif order.TimeInForce
   130  		var rate uint64
   131  		if isLimit {
   132  			rate = uint64(ord.Rate * 1e8)
   133  			if rand.Float32() < 0.25 {
   134  				tif = order.ImmediateTiF
   135  			} else {
   136  				tif = order.StandingTiF
   137  			}
   138  		}
   139  		ords = append(ords, &core.Order{
   140  			ID:     ordertest.RandomOrderID().Bytes(),
   141  			Type:   orderType,
   142  			Stamp:  uint64(time.Now().UnixMilli()) - uint64(rand.Float64()*600_000),
   143  			Status: status,
   144  			Epoch:  epoch,
   145  			Rate:   rate,
   146  			Qty:    qty,
   147  			Sell:   sell,
   148  			Filled: filled,
   149  			Matches: []*core.Match{
   150  				{
   151  					MatchID: ordertest.RandomMatchID().Bytes(),
   152  					Rate:    uint64(ord.Rate * 1e8),
   153  					Qty:     uint64(rand.Float64() * float64(filled)),
   154  					Status:  order.MatchComplete,
   155  				},
   156  			},
   157  			TimeInForce: tif,
   158  		})
   159  	}
   160  	return
   161  }
   162  
   163  var marketStats = make(map[string][2]float64)
   164  
   165  func getMarketStats(mktID string) (midGap, maxQty float64) {
   166  	stats := marketStats[mktID]
   167  	return stats[0], stats[1]
   168  }
   169  
   170  func mkMrkt(base, quote string) *core.Market {
   171  	baseID, _ := dex.BipSymbolID(base)
   172  	quoteID, _ := dex.BipSymbolID(quote)
   173  	mktID := base + "_" + quote
   174  	assetOrder := rand.Intn(5) + 6
   175  	lotSize := uint64(math.Pow10(assetOrder)) * uint64(rand.Intn(9)+1)
   176  	rateStep := lotSize / 1e3
   177  	if _, exists := marketStats[mktID]; !exists {
   178  		midGap := float64(rateStep) * float64(rand.Intn(1e6))
   179  		maxQty := float64(lotSize) * float64(rand.Intn(1e3))
   180  		marketStats[mktID] = [2]float64{midGap, maxQty}
   181  	}
   182  
   183  	rate := uint64(rand.Intn(1e3)) * rateStep
   184  	change24 := rand.Float64()*0.3 - .15
   185  
   186  	mkt := &core.Market{
   187  		Name:            fmt.Sprintf("%s_%s", base, quote),
   188  		BaseID:          baseID,
   189  		BaseSymbol:      base,
   190  		QuoteID:         quoteID,
   191  		QuoteSymbol:     quote,
   192  		LotSize:         lotSize,
   193  		ParcelSize:      10,
   194  		RateStep:        rateStep,
   195  		MarketBuyBuffer: rand.Float64() + 1,
   196  		EpochLen:        uint64(epochDuration.Milliseconds()),
   197  		SpotPrice: &msgjson.Spot{
   198  			Stamp:   uint64(time.Now().UnixMilli()),
   199  			BaseID:  baseID,
   200  			QuoteID: quoteID,
   201  			Rate:    rate,
   202  			// BookVolume: ,
   203  			Change24: change24,
   204  			Vol24:    lotSize * uint64(50000*rand.Float32()),
   205  		},
   206  	}
   207  
   208  	if (baseID != unsupportedAssetID) && (quoteID != unsupportedAssetID) {
   209  		mkt.Orders = userOrders(mktID)
   210  	}
   211  
   212  	return mkt
   213  }
   214  
   215  func mkSupportedAsset(symbol string, state *core.WalletState) *core.SupportedAsset {
   216  	assetID, _ := dex.BipSymbolID(symbol)
   217  	winfo := winfos[assetID]
   218  	var name string
   219  	var unitInfo dex.UnitInfo
   220  	if winfo == nil {
   221  		name = tinfos[assetID].Name
   222  		unitInfo = tinfos[assetID].UnitInfo
   223  	} else {
   224  		name = winfo.Name
   225  		unitInfo = winfo.UnitInfo
   226  	}
   227  
   228  	return &core.SupportedAsset{
   229  		ID:                    assetID,
   230  		Symbol:                symbol,
   231  		Info:                  winfo,
   232  		Wallet:                state,
   233  		Token:                 tinfos[assetID],
   234  		Name:                  name,
   235  		UnitInfo:              unitInfo,
   236  		WalletCreationPending: assetID == atomic.LoadUint32(&creationPendingAsset),
   237  	}
   238  }
   239  
   240  func mkDexAsset(symbol string) *dex.Asset {
   241  	assetID, _ := dex.BipSymbolID(symbol)
   242  	ui, err := asset.UnitInfo(assetID)
   243  	if err != nil /* unknown asset*/ {
   244  		ui = dex.UnitInfo{
   245  			AtomicUnit: "Sats",
   246  			Conventional: dex.Denomination{
   247  				ConversionFactor: 1e8,
   248  				Unit:             strings.ToUpper(symbol),
   249  			},
   250  		}
   251  	}
   252  	a := &dex.Asset{
   253  		ID:         assetID,
   254  		Symbol:     symbol,
   255  		Version:    0,
   256  		MaxFeeRate: uint64(rand.Intn(10) + 1),
   257  		SwapConf:   uint32(rand.Intn(5) + 2),
   258  		UnitInfo:   ui,
   259  	}
   260  	return a
   261  }
   262  
   263  func mkid(b, q uint32) string {
   264  	return unbip(b) + "_" + unbip(q)
   265  }
   266  
   267  func getEpoch() uint64 {
   268  	return uint64(time.Now().UnixMilli()) / uint64(epochDuration.Milliseconds())
   269  }
   270  
   271  func randomOrder(sell bool, maxQty, midGap, marketWidth float64, epoch bool) *core.MiniOrder {
   272  	var epochIdx uint64
   273  	var rate float64
   274  	var limitRate = midGap - rand.Float64()*marketWidth
   275  	if sell {
   276  		limitRate = midGap + rand.Float64()*marketWidth
   277  	}
   278  	if epoch {
   279  		epochIdx = getEpoch()
   280  		// Epoch orders might be market orders.
   281  		if rand.Float32() < 0.5 {
   282  			rate = limitRate
   283  		}
   284  	} else {
   285  		rate = limitRate
   286  	}
   287  
   288  	qty := uint64(math.Exp(-rand.Float64()*5) * maxQty)
   289  
   290  	return &core.MiniOrder{
   291  		Qty:       float64(qty) / float64(conversionFactor),
   292  		QtyAtomic: qty,
   293  		Rate:      float64(rate) / float64(conversionFactor),
   294  		MsgRate:   uint64(rate),
   295  		Sell:      sell,
   296  		Token:     nextToken(),
   297  		Epoch:     epochIdx,
   298  	}
   299  }
   300  
   301  func miniOrderFromCoreOrder(ord *core.Order) *core.MiniOrder {
   302  	var epoch uint64 = 555
   303  	if ord.Status > order.OrderStatusEpoch {
   304  		epoch = 0
   305  	}
   306  	return &core.MiniOrder{
   307  		Qty:       float64(ord.Qty) / float64(conversionFactor),
   308  		QtyAtomic: ord.Qty,
   309  		Rate:      float64(ord.Rate) / float64(conversionFactor),
   310  		MsgRate:   ord.Rate,
   311  		Sell:      ord.Sell,
   312  		Token:     ord.ID[:4].String(),
   313  		Epoch:     epoch,
   314  	}
   315  }
   316  
   317  var dexAssets = map[uint32]*dex.Asset{
   318  	0:                  mkDexAsset("btc"),
   319  	2:                  mkDexAsset("ltc"),
   320  	42:                 mkDexAsset("dcr"),
   321  	22:                 mkDexAsset("mona"),
   322  	28:                 mkDexAsset("vtc"),
   323  	unsupportedAssetID: mkDexAsset("kmd"),
   324  	3:                  mkDexAsset("doge"),
   325  	145:                mkDexAsset("bch"),
   326  	60:                 mkDexAsset("eth"),
   327  	60001:              mkDexAsset("usdc.eth"),
   328  	133:                mkDexAsset("zec"),
   329  	966001:             mkDexAsset("usdc.polygon"),
   330  }
   331  
   332  var tExchanges = map[string]*core.Exchange{
   333  	firstDEX: {
   334  		Host:   "somedex.com",
   335  		Assets: dexAssets,
   336  		AcctID: "abcdef0123456789",
   337  		Markets: map[string]*core.Market{
   338  			mkid(42, 0):       mkMrkt("dcr", "btc"),
   339  			mkid(145, 42):     mkMrkt("bch", "dcr"),
   340  			mkid(60, 42):      mkMrkt("eth", "dcr"),
   341  			mkid(2, 42):       mkMrkt("ltc", "dcr"),
   342  			mkid(3, 42):       mkMrkt("doge", "dcr"),
   343  			mkid(22, 42):      mkMrkt("mona", "dcr"),
   344  			mkid(28, 0):       mkMrkt("vtc", "btc"),
   345  			mkid(60001, 42):   mkMrkt("usdc.eth", "dcr"),
   346  			mkid(60, 60001):   mkMrkt("eth", "usdc.eth"),
   347  			mkid(133, 966001): mkMrkt("zec", "usdc.polygon"),
   348  		},
   349  		ConnectionStatus: comms.Connected,
   350  		CandleDurs:       []string{"1h", "24h"},
   351  		Auth: core.ExchangeAuth{
   352  			PendingBonds: []*core.PendingBondState{},
   353  			BondAssetID:  42,
   354  			TargetTier:   0,
   355  			MaxBondedAmt: 100e8,
   356  		},
   357  		BondAssets: map[string]*core.BondAsset{
   358  			"dcr": {
   359  				ID:    42,
   360  				Confs: 2,
   361  				Amt:   1,
   362  			},
   363  		},
   364  		ViewOnly: true,
   365  		MaxScore: 60,
   366  	},
   367  	secondDEX: {
   368  		Host:   "thisdexwithalongname.com",
   369  		Assets: dexAssets,
   370  		AcctID: "0123456789abcdef",
   371  		Markets: map[string]*core.Market{
   372  			mkid(42, 28):                 mkMrkt("dcr", "vtc"),
   373  			mkid(0, 2):                   mkMrkt("btc", "ltc"),
   374  			mkid(22, unsupportedAssetID): mkMrkt("mona", "kmd"),
   375  		},
   376  		ConnectionStatus: comms.Connected,
   377  		CandleDurs:       []string{"5m", "1h", "24h"},
   378  		Auth: core.ExchangeAuth{
   379  			PendingBonds: []*core.PendingBondState{},
   380  			BondAssetID:  42,
   381  			TargetTier:   0,
   382  			MaxBondedAmt: 100e8,
   383  		},
   384  		BondAssets: map[string]*core.BondAsset{
   385  			"dcr": {
   386  				ID:    42,
   387  				Confs: 2,
   388  				Amt:   1,
   389  			},
   390  		},
   391  		ViewOnly: true,
   392  	},
   393  }
   394  
   395  type tCoin struct {
   396  	id       []byte
   397  	confs    uint32
   398  	confsErr error
   399  }
   400  
   401  func (c *tCoin) ID() dex.Bytes {
   402  	return c.id
   403  }
   404  
   405  func (c *tCoin) String() string {
   406  	return hex.EncodeToString(c.id)
   407  }
   408  
   409  func (c *tCoin) TxID() string {
   410  	return hex.EncodeToString(c.id)
   411  }
   412  
   413  func (c *tCoin) Value() uint64 {
   414  	return 0
   415  }
   416  
   417  func (c *tCoin) Confirmations(context.Context) (uint32, error) {
   418  	return c.confs, c.confsErr
   419  }
   420  
   421  type tWalletState struct {
   422  	walletType   string
   423  	open         bool
   424  	running      bool
   425  	disabled     bool
   426  	settings     map[string]string
   427  	syncProgress uint32
   428  }
   429  
   430  type tBookFeed struct {
   431  	core *TCore
   432  	c    chan *core.BookUpdate
   433  }
   434  
   435  func (t *tBookFeed) Next() <-chan *core.BookUpdate {
   436  	return t.c
   437  }
   438  
   439  func (t *tBookFeed) Close() {}
   440  
   441  func (t *tBookFeed) Candles(dur string) error {
   442  	t.core.sendCandles(dur)
   443  	return nil
   444  }
   445  
   446  type TCore struct {
   447  	inited    bool
   448  	mtx       sync.RWMutex
   449  	wallets   map[uint32]*tWalletState
   450  	balances  map[uint32]*core.WalletBalance
   451  	dexAddr   string
   452  	marketID  string
   453  	base      uint32
   454  	quote     uint32
   455  	candleDur struct {
   456  		dur time.Duration
   457  		str string
   458  	}
   459  
   460  	bookFeed    *tBookFeed
   461  	killFeed    context.CancelFunc
   462  	buys        map[string]*core.MiniOrder
   463  	sells       map[string]*core.MiniOrder
   464  	noteFeed    chan core.Notification
   465  	orderMtx    sync.Mutex
   466  	epochOrders []*core.BookUpdate
   467  	fiatSources map[string]bool
   468  	validAddr   bool
   469  	lang        string
   470  }
   471  
   472  // TDriver implements the interface required of all exchange wallets.
   473  type TDriver struct{}
   474  
   475  func (drv *TDriver) Exists(walletType, dataDir string, settings map[string]string, net dex.Network) (bool, error) {
   476  	return true, nil
   477  }
   478  
   479  func (drv *TDriver) Create(*asset.CreateWalletParams) error {
   480  	return nil
   481  }
   482  
   483  func (*TDriver) Open(*asset.WalletConfig, dex.Logger, dex.Network) (asset.Wallet, error) {
   484  	return nil, nil
   485  }
   486  
   487  func (*TDriver) DecodeCoinID(coinID []byte) (string, error) {
   488  	return asset.DecodeCoinID(0, coinID) // btc decoder
   489  }
   490  
   491  func (*TDriver) Info() *asset.WalletInfo {
   492  	return &asset.WalletInfo{
   493  		SupportedVersions: []uint32{0},
   494  		UnitInfo: dex.UnitInfo{
   495  			Conventional: dex.Denomination{
   496  				ConversionFactor: 1e8,
   497  			},
   498  		},
   499  	}
   500  }
   501  
   502  func newTCore() *TCore {
   503  	return &TCore{
   504  		wallets: make(map[uint32]*tWalletState),
   505  		balances: map[uint32]*core.WalletBalance{
   506  			0:      randomWalletBalance(0),
   507  			2:      randomWalletBalance(2),
   508  			42:     randomWalletBalance(42),
   509  			22:     randomWalletBalance(22),
   510  			3:      randomWalletBalance(3),
   511  			28:     randomWalletBalance(28),
   512  			60:     randomWalletBalance(60),
   513  			145:    randomWalletBalance(145),
   514  			60001:  randomWalletBalance(60001),
   515  			966:    randomWalletBalance(966),
   516  			966001: randomWalletBalance(966001),
   517  			133:    randomWalletBalance(133),
   518  		},
   519  		noteFeed: make(chan core.Notification, 1),
   520  		fiatSources: map[string]bool{
   521  			"dcrdata":     true,
   522  			"Messari":     true,
   523  			"Coinpaprika": true,
   524  		},
   525  		lang: "en-US",
   526  	}
   527  }
   528  
   529  func (c *TCore) trySend(u *core.BookUpdate) {
   530  	select {
   531  	case c.bookFeed.c <- u:
   532  	default:
   533  	}
   534  }
   535  
   536  func (c *TCore) Network() dex.Network { return dex.Mainnet }
   537  
   538  func (c *TCore) Exchanges() map[string]*core.Exchange { return tExchanges }
   539  
   540  func (c *TCore) Exchange(host string) (*core.Exchange, error) {
   541  	exchange, ok := tExchanges[host]
   542  	if !ok {
   543  		return nil, fmt.Errorf("no exchange at %v", host)
   544  	}
   545  	return exchange, nil
   546  }
   547  
   548  func (c *TCore) InitializeClient(pw []byte, seed *string) (string, error) {
   549  	randomDelay()
   550  	c.inited = true
   551  	var mnemonicSeed string
   552  	if seed == nil {
   553  		_, mnemonicSeed = mnemonic.New()
   554  	}
   555  	return mnemonicSeed, nil
   556  }
   557  func (c *TCore) GetDEXConfig(host string, certI any) (*core.Exchange, error) {
   558  	if xc := tExchanges[host]; xc != nil {
   559  		return xc, nil
   560  	}
   561  	return tExchanges[firstDEX], nil
   562  }
   563  
   564  func (c *TCore) AddDEX(appPW []byte, dexAddr string, certI any) error {
   565  	randomDelay()
   566  	if initErrors {
   567  		return fmt.Errorf("forced init error")
   568  	}
   569  	return nil
   570  }
   571  
   572  // DiscoverAccount - use secondDEX = "thisdexwithalongname.com" to get paid = true.
   573  func (c *TCore) DiscoverAccount(dexAddr string, pw []byte, certI any) (*core.Exchange, bool, error) {
   574  	xc := tExchanges[dexAddr]
   575  	if xc == nil {
   576  		xc = tExchanges[firstDEX]
   577  	}
   578  	if dexAddr == secondDEX {
   579  		// c.reg = &core.RegisterForm{}
   580  	}
   581  	return tExchanges[firstDEX], dexAddr == secondDEX, nil
   582  }
   583  func (c *TCore) PostBond(form *core.PostBondForm) (*core.PostBondResult, error) {
   584  	xc, exists := tExchanges[form.Addr]
   585  	if !exists {
   586  		return nil, fmt.Errorf("server %q not known", form.Addr)
   587  	}
   588  	symbol := dex.BipIDSymbol(*form.Asset)
   589  	ba := xc.BondAssets[symbol]
   590  	tier := form.Bond / ba.Amt
   591  	xc.Auth.BondAssetID = *form.Asset
   592  	xc.Auth.TargetTier = tier
   593  	xc.Auth.Rep.BondedTier = int64(tier)
   594  	xc.Auth.EffectiveTier = int64(tier)
   595  	xc.ViewOnly = false
   596  	return &core.PostBondResult{
   597  		BondID:      "abc",
   598  		ReqConfirms: uint16(ba.Confs),
   599  	}, nil
   600  }
   601  func (c *TCore) RedeemPrepaidBond(appPW []byte, code []byte, host string, certI any) (tier uint64, err error) {
   602  	return 1, nil
   603  }
   604  func (c *TCore) UpdateBondOptions(form *core.BondOptionsForm) error {
   605  	xc := tExchanges[form.Host]
   606  	xc.ViewOnly = false
   607  	xc.Auth.TargetTier = *form.TargetTier
   608  	xc.Auth.BondAssetID = *form.BondAssetID
   609  	xc.Auth.EffectiveTier = int64(*form.TargetTier)
   610  	xc.Auth.Rep.BondedTier = int64(*form.TargetTier)
   611  	return nil
   612  }
   613  func (c *TCore) BondsFeeBuffer(assetID uint32) (uint64, error) {
   614  	return 222, nil
   615  }
   616  func (c *TCore) ValidateAddress(address string, assetID uint32) (bool, error) {
   617  	return len(address) > 10, nil
   618  }
   619  func (c *TCore) EstimateSendTxFee(addr string, assetID uint32, value uint64, subtract, maxWithdraw bool) (fee uint64, isValidAddress bool, err error) {
   620  	return uint64(float64(value) * 0.01), len(addr) > 10, nil
   621  }
   622  func (c *TCore) Login([]byte) error  { return nil }
   623  func (c *TCore) IsInitialized() bool { return c.inited }
   624  func (c *TCore) Logout() error       { return nil }
   625  func (c *TCore) Notifications(n int) (notes, pokes []*db.Notification, _ error) {
   626  	return []*db.Notification{}, []*db.Notification{}, nil
   627  }
   628  
   629  var orderAssets = []string{"dcr", "btc", "ltc", "doge", "mona", "vtc", "usdc.eth"}
   630  
   631  func (c *TCore) Orders(filter *core.OrderFilter) ([]*core.Order, error) {
   632  	var spacing uint64 = 60 * 60 * 1000 / 2 // half an hour
   633  	t := uint64(time.Now().UnixMilli())
   634  
   635  	if randomizeOrdersCount {
   636  		if rand.Float32() < 0.25 {
   637  			return []*core.Order{}, nil
   638  		}
   639  		filter.N = rand.Intn(filter.N + 1)
   640  	}
   641  
   642  	cords := make([]*core.Order, 0, filter.N)
   643  	for i := 0; i < int(filter.N); i++ {
   644  		cord := makeCoreOrder()
   645  
   646  		cord.Stamp = t
   647  		// Make it a little older.
   648  		t -= spacing
   649  
   650  		cords = append(cords, cord)
   651  	}
   652  	return cords, nil
   653  }
   654  
   655  func (c *TCore) MaxBuy(host string, base, quote uint32, rate uint64) (*core.MaxOrderEstimate, error) {
   656  	mktID, _ := dex.MarketName(base, quote)
   657  	lotSize := tExchanges[host].Markets[mktID].LotSize
   658  	midGap, maxQty := getMarketStats(mktID)
   659  	ord := randomOrder(rand.Float32() > 0.5, maxQty, midGap, gapWidthFactor*midGap, false)
   660  	qty := ord.QtyAtomic
   661  	quoteQty := calc.BaseToQuote(rate, qty)
   662  	return &core.MaxOrderEstimate{
   663  		Swap: &asset.SwapEstimate{
   664  			Lots:               qty / lotSize,
   665  			Value:              quoteQty,
   666  			MaxFees:            quoteQty / 100,
   667  			RealisticWorstCase: quoteQty / 200,
   668  			RealisticBestCase:  quoteQty / 300,
   669  		},
   670  		Redeem: &asset.RedeemEstimate{
   671  			RealisticWorstCase: qty / 300,
   672  			RealisticBestCase:  qty / 400,
   673  		},
   674  	}, nil
   675  }
   676  
   677  func (c *TCore) MaxSell(host string, base, quote uint32) (*core.MaxOrderEstimate, error) {
   678  	mktID, _ := dex.MarketName(base, quote)
   679  	lotSize := tExchanges[host].Markets[mktID].LotSize
   680  	midGap, maxQty := getMarketStats(mktID)
   681  	ord := randomOrder(rand.Float32() > 0.5, maxQty, midGap, gapWidthFactor*midGap, false)
   682  	qty := ord.QtyAtomic
   683  
   684  	quoteQty := calc.BaseToQuote(uint64(midGap), qty)
   685  
   686  	return &core.MaxOrderEstimate{
   687  		Swap: &asset.SwapEstimate{
   688  			Lots:               qty / lotSize,
   689  			Value:              qty,
   690  			MaxFees:            qty / 100,
   691  			RealisticWorstCase: qty / 200,
   692  			RealisticBestCase:  qty / 300,
   693  		},
   694  		Redeem: &asset.RedeemEstimate{
   695  			RealisticWorstCase: quoteQty / 300,
   696  			RealisticBestCase:  quoteQty / 400,
   697  		},
   698  	}, nil
   699  }
   700  
   701  func (c *TCore) PreOrder(*core.TradeForm) (*core.OrderEstimate, error) {
   702  	return &core.OrderEstimate{
   703  		Swap: &asset.PreSwap{
   704  			Estimate: &asset.SwapEstimate{
   705  				Lots:               5,
   706  				Value:              5e8,
   707  				MaxFees:            1600,
   708  				RealisticWorstCase: 12010,
   709  				RealisticBestCase:  6008,
   710  			},
   711  			Options: []*asset.OrderOption{
   712  				{
   713  					ConfigOption: asset.ConfigOption{
   714  						Key:          "moredough",
   715  						DisplayName:  "Get More Dough",
   716  						Description:  "Cast a magical incantation to double the amount of XYZ received.",
   717  						DefaultValue: "true",
   718  					},
   719  					Boolean: &asset.BooleanConfig{
   720  						Reason: "Cuz why not?",
   721  					},
   722  				},
   723  				{
   724  					ConfigOption: asset.ConfigOption{
   725  						Key:          "awesomeness",
   726  						DisplayName:  "More Awesomeness",
   727  						Description:  "Crank up the awesomeness for next-level trading.",
   728  						DefaultValue: "1.0",
   729  					},
   730  					XYRange: &asset.XYRange{
   731  						Start: asset.XYRangePoint{
   732  							Label: "Low",
   733  							X:     1,
   734  							Y:     3,
   735  						},
   736  						End: asset.XYRangePoint{
   737  							Label: "High",
   738  							X:     10,
   739  							Y:     30,
   740  						},
   741  						XUnit: "X",
   742  						YUnit: "kBTC",
   743  					},
   744  				},
   745  			},
   746  		},
   747  		Redeem: &asset.PreRedeem{
   748  			Estimate: &asset.RedeemEstimate{
   749  				RealisticBestCase:  2800,
   750  				RealisticWorstCase: 6500,
   751  			},
   752  			Options: []*asset.OrderOption{
   753  				{
   754  					ConfigOption: asset.ConfigOption{
   755  						Key:          "lesshassle",
   756  						DisplayName:  "Smoother Experience",
   757  						Description:  "Select this option for a super-elite VIP DEX experience.",
   758  						DefaultValue: "false",
   759  					},
   760  					Boolean: &asset.BooleanConfig{
   761  						Reason: "Half the time, twice the service",
   762  					},
   763  				},
   764  			},
   765  		},
   766  	}, nil
   767  }
   768  
   769  func (c *TCore) AccountExport(pw []byte, host string) (*core.Account, []*db.Bond, error) {
   770  	return nil, nil, nil
   771  }
   772  func (c *TCore) AccountImport(pw []byte, account *core.Account, bond []*db.Bond) error {
   773  	return nil
   774  }
   775  func (c *TCore) ToggleAccountStatus(pw []byte, host string, disable bool) error { return nil }
   776  
   777  func (c *TCore) TxHistory(assetID uint32, n int, refID *string, past bool) ([]*asset.WalletTransaction, error) {
   778  	return nil, nil
   779  }
   780  
   781  func coreCoin() *core.Coin {
   782  	b := make([]byte, 36)
   783  	copy(b[:], encode.RandomBytes(32))
   784  	binary.BigEndian.PutUint32(b[32:], uint32(rand.Intn(15)))
   785  	return core.NewCoin(0, b)
   786  }
   787  
   788  func coreSwapCoin() *core.Coin {
   789  	c := coreCoin()
   790  	c.SetConfirmations(int64(rand.Intn(3)), 2)
   791  	return c
   792  }
   793  
   794  func makeCoreOrder() *core.Order {
   795  	// sell := rand.Float32() < 0.5
   796  	// center := randomMagnitude(-2, 4)
   797  	// ord := randomOrder(sell, randomMagnitude(-2, 4), center, gapWidthFactor*center, true)
   798  	host := firstDEX
   799  	if rand.Float32() > 0.5 {
   800  		host = secondDEX
   801  	}
   802  	mkts := make([]*core.Market, 0, len(tExchanges[host].Markets))
   803  	for _, mkt := range tExchanges[host].Markets {
   804  		if mkt.BaseID == unsupportedAssetID || mkt.QuoteID == unsupportedAssetID {
   805  			continue
   806  		}
   807  		mkts = append(mkts, mkt)
   808  	}
   809  	mkt := mkts[rand.Intn(len(mkts))]
   810  	rate := uint64(rand.Intn(1e3)) * mkt.RateStep
   811  	baseQty := uint64(rand.Intn(1e3)) * mkt.LotSize
   812  	isMarket := rand.Float32() > 0.5
   813  	sell := rand.Float32() > 0.5
   814  	numMatches := rand.Intn(13)
   815  	orderQty := baseQty
   816  	orderRate := rate
   817  	matchQ := baseQty / 13
   818  	matchQ -= matchQ % mkt.LotSize
   819  	tif := order.TimeInForce(rand.Intn(int(order.StandingTiF)))
   820  
   821  	if isMarket {
   822  		orderRate = 0
   823  		if !sell {
   824  			orderQty = calc.BaseToQuote(rate, baseQty)
   825  		}
   826  	}
   827  
   828  	numCoins := rand.Intn(5) + 1
   829  	fundingCoins := make([]*core.Coin, 0, numCoins)
   830  	for i := 0; i < numCoins; i++ {
   831  		coinID := make([]byte, 36)
   832  		copy(coinID[:], encode.RandomBytes(32))
   833  		coinID[35] = byte(rand.Intn(8))
   834  		fundingCoins = append(fundingCoins, core.NewCoin(0, coinID))
   835  	}
   836  
   837  	status := order.OrderStatus(rand.Intn(int(order.OrderStatusRevoked-1))) + 1
   838  
   839  	stamp := func() uint64 {
   840  		return uint64(time.Now().Add(-time.Second * time.Duration(rand.Intn(60*60))).UnixMilli())
   841  	}
   842  
   843  	cord := &core.Order{
   844  		Host:        host,
   845  		BaseID:      mkt.BaseID,
   846  		BaseSymbol:  mkt.BaseSymbol,
   847  		QuoteID:     mkt.QuoteID,
   848  		QuoteSymbol: mkt.QuoteSymbol,
   849  		MarketID:    mkt.BaseSymbol + "_" + mkt.QuoteSymbol,
   850  		Type:        order.OrderType(rand.Intn(int(order.MarketOrderType))) + 1,
   851  		Stamp:       stamp(),
   852  		ID:          ordertest.RandomOrderID().Bytes(),
   853  		Status:      status,
   854  		Qty:         orderQty,
   855  		Sell:        sell,
   856  		Filled:      uint64(rand.Float64() * float64(orderQty)),
   857  		Canceled:    status == order.OrderStatusCanceled,
   858  		Rate:        orderRate,
   859  		TimeInForce: tif,
   860  		FeesPaid: &core.FeeBreakdown{
   861  			Swap:       orderQty / 100,
   862  			Redemption: mkt.RateStep * 100,
   863  		},
   864  		FundingCoins: fundingCoins,
   865  	}
   866  
   867  	for i := 0; i < numMatches; i++ {
   868  		userMatch := ordertest.RandomUserMatch()
   869  		matchQty := matchQ
   870  		if i == numMatches-1 {
   871  			matchQty = baseQty - (matchQ * (uint64(numMatches) - 1))
   872  		}
   873  		status := userMatch.Status
   874  		side := userMatch.Side
   875  		match := &core.Match{
   876  			MatchID: userMatch.MatchID[:],
   877  			Status:  userMatch.Status,
   878  			Rate:    rate,
   879  			Qty:     matchQty,
   880  			Side:    userMatch.Side,
   881  			Stamp:   stamp(),
   882  		}
   883  
   884  		if (status >= order.MakerSwapCast && side == order.Maker) ||
   885  			(status >= order.TakerSwapCast && side == order.Taker) {
   886  
   887  			match.Swap = coreSwapCoin()
   888  		}
   889  
   890  		refund := rand.Float32() < 0.1
   891  		if refund {
   892  			match.Refund = coreCoin()
   893  		} else {
   894  			if (status >= order.TakerSwapCast && side == order.Maker) ||
   895  				(status >= order.MakerSwapCast && side == order.Taker) {
   896  
   897  				match.CounterSwap = coreSwapCoin()
   898  			}
   899  
   900  			if (status >= order.MakerRedeemed && side == order.Maker) ||
   901  				(status >= order.MatchComplete && side == order.Taker) {
   902  
   903  				match.Redeem = coreCoin()
   904  			}
   905  
   906  			if (status >= order.MakerRedeemed && side == order.Taker) ||
   907  				(status >= order.MatchComplete && side == order.Maker) {
   908  
   909  				match.CounterRedeem = coreCoin()
   910  			}
   911  		}
   912  
   913  		cord.Matches = append(cord.Matches, match)
   914  	}
   915  	return cord
   916  }
   917  
   918  func (c *TCore) Order(dex.Bytes) (*core.Order, error) {
   919  	return makeCoreOrder(), nil
   920  }
   921  
   922  func (c *TCore) SyncBook(dexAddr string, base, quote uint32) (*orderbook.OrderBook, core.BookFeed, error) {
   923  	mktID, _ := dex.MarketName(base, quote)
   924  	c.mtx.Lock()
   925  	c.dexAddr = dexAddr
   926  	c.marketID = mktID
   927  	c.base = base
   928  	c.quote = quote
   929  	c.candleDur.dur = 0
   930  	c.mtx.Unlock()
   931  
   932  	xc := tExchanges[dexAddr]
   933  	mkt := xc.Markets[mkid(base, quote)]
   934  
   935  	usrOrds := tExchanges[dexAddr].Markets[mktID].Orders
   936  	isUserOrder := func(tkn string) bool {
   937  		for _, ord := range usrOrds {
   938  			if tkn == ord.ID[:4].String() {
   939  				return true
   940  			}
   941  		}
   942  		return false
   943  	}
   944  
   945  	if c.bookFeed != nil {
   946  		c.killFeed()
   947  	}
   948  
   949  	c.bookFeed = &tBookFeed{
   950  		core: c,
   951  		c:    make(chan *core.BookUpdate, 1),
   952  	}
   953  	var ctx context.Context
   954  	ctx, c.killFeed = context.WithCancel(tCtx)
   955  	go func() {
   956  		tick := time.NewTicker(feedPeriod)
   957  	out:
   958  		for {
   959  			select {
   960  			case <-tick.C:
   961  				// Send a random order to the order feed. Slighly biased away from
   962  				// unbook_order and towards book_order.
   963  				r := rand.Float32()
   964  				switch {
   965  				case r < 0.80:
   966  					// Book order
   967  					sell := rand.Float32() < 0.5
   968  					midGap, maxQty := getMarketStats(mktID)
   969  					ord := randomOrder(sell, maxQty, midGap, gapWidthFactor*midGap, true)
   970  					c.orderMtx.Lock()
   971  					side := c.buys
   972  					if sell {
   973  						side = c.sells
   974  					}
   975  					side[ord.Token] = ord
   976  					epochOrder := &core.BookUpdate{
   977  						Action:   msgjson.EpochOrderRoute,
   978  						Host:     c.dexAddr,
   979  						MarketID: mktID,
   980  						Payload:  ord,
   981  					}
   982  					c.trySend(epochOrder)
   983  					c.epochOrders = append(c.epochOrders, epochOrder)
   984  					c.orderMtx.Unlock()
   985  				default:
   986  					// Unbook order
   987  					sell := rand.Float32() < 0.5
   988  					c.orderMtx.Lock()
   989  					side := c.buys
   990  					if sell {
   991  						side = c.sells
   992  					}
   993  					var tkn string
   994  					for tkn = range side {
   995  						break
   996  					}
   997  					if tkn == "" {
   998  						c.orderMtx.Unlock()
   999  						continue
  1000  					}
  1001  					if isUserOrder(tkn) {
  1002  						// Our own order. Don't remove.
  1003  						c.orderMtx.Unlock()
  1004  						continue
  1005  					}
  1006  					delete(side, tkn)
  1007  					c.orderMtx.Unlock()
  1008  
  1009  					c.trySend(&core.BookUpdate{
  1010  						Action:   msgjson.UnbookOrderRoute,
  1011  						Host:     c.dexAddr,
  1012  						MarketID: mktID,
  1013  						Payload:  &core.MiniOrder{Token: tkn},
  1014  					})
  1015  				}
  1016  
  1017  				// Send a candle update.
  1018  				c.mtx.RLock()
  1019  				dur := c.candleDur.dur
  1020  				durStr := c.candleDur.str
  1021  				c.mtx.RUnlock()
  1022  				if dur == 0 {
  1023  					continue
  1024  				}
  1025  				c.trySend(&core.BookUpdate{
  1026  					Action:   core.CandleUpdateAction,
  1027  					Host:     dexAddr,
  1028  					MarketID: mktID,
  1029  					Payload: &core.CandleUpdate{
  1030  						Dur:          durStr,
  1031  						DurMilliSecs: uint64(dur.Milliseconds()),
  1032  						Candle:       candle(mkt, dur, time.Now()),
  1033  					},
  1034  				})
  1035  
  1036  			case <-ctx.Done():
  1037  				break out
  1038  			}
  1039  
  1040  		}
  1041  	}()
  1042  
  1043  	c.bookFeed.c <- &core.BookUpdate{
  1044  		Action:   core.FreshBookAction,
  1045  		Host:     dexAddr,
  1046  		MarketID: mktID,
  1047  		Payload: &core.MarketOrderBook{
  1048  			Base:  base,
  1049  			Quote: quote,
  1050  			Book:  c.book(dexAddr, mktID),
  1051  		},
  1052  	}
  1053  
  1054  	return nil, c.bookFeed, nil
  1055  }
  1056  
  1057  func candle(mkt *core.Market, dur time.Duration, stamp time.Time) *msgjson.Candle {
  1058  	high, low, start, end, vol := candleStats(mkt.LotSize, mkt.RateStep, dur, stamp)
  1059  	quoteVol := calc.BaseToQuote(end, vol)
  1060  
  1061  	return &msgjson.Candle{
  1062  		StartStamp:  uint64(stamp.Truncate(dur).UnixMilli()),
  1063  		EndStamp:    uint64(stamp.UnixMilli()),
  1064  		MatchVolume: vol,
  1065  		QuoteVolume: quoteVol,
  1066  		HighRate:    high,
  1067  		LowRate:     low,
  1068  		StartRate:   start,
  1069  		EndRate:     end,
  1070  	}
  1071  }
  1072  
  1073  func candleStats(lotSize, rateStep uint64, candleDur time.Duration, stamp time.Time) (high, low, start, end, vol uint64) {
  1074  	freq := math.Pi * 2 / float64(candleDur.Milliseconds()*20)
  1075  	maxVol := 1e5 * float64(lotSize)
  1076  	volFactor := (math.Sin(float64(stamp.UnixMilli())*freq/2) + 1) / 2
  1077  	vol = uint64(maxVol * volFactor)
  1078  
  1079  	waveFactor := (math.Sin(float64(stamp.UnixMilli())*freq) + 1) / 2
  1080  	priceVariation := 1e5 * float64(rateStep)
  1081  	priceFloor := 0.5 * priceVariation
  1082  	startWaveFactor := (math.Sin(float64(stamp.Truncate(candleDur).UnixMilli())*freq) + 1) / 2
  1083  	start = uint64(startWaveFactor*priceVariation + priceFloor)
  1084  	end = uint64(waveFactor*priceVariation + priceFloor)
  1085  
  1086  	if start > end {
  1087  		diff := (start - end) / 2
  1088  		high = start + diff
  1089  		low = end - diff
  1090  	} else {
  1091  		diff := (end - start) / 2
  1092  		high = end + diff
  1093  		low = start - diff
  1094  	}
  1095  	return
  1096  }
  1097  
  1098  func (c *TCore) sendCandles(durStr string) {
  1099  	randomDelay()
  1100  	dur, err := time.ParseDuration(durStr)
  1101  	if err != nil {
  1102  		panic("sendCandles ParseDuration error: " + err.Error())
  1103  	}
  1104  
  1105  	c.mtx.RLock()
  1106  	c.candleDur.dur = dur
  1107  	c.candleDur.str = durStr
  1108  	dexAddr := c.dexAddr
  1109  	mktID := c.marketID
  1110  	xc := tExchanges[c.dexAddr]
  1111  	mkt := xc.Markets[mkid(c.base, c.quote)]
  1112  	c.mtx.RUnlock()
  1113  
  1114  	tNow := time.Now()
  1115  	iStartTime := tNow.Add(-dur * candles.CacheSize).Truncate(dur)
  1116  	candles := make([]msgjson.Candle, 0, candles.CacheSize)
  1117  
  1118  	for iStartTime.Before(tNow) {
  1119  		candles = append(candles, *candle(mkt, dur, iStartTime.Add(dur-1)))
  1120  		iStartTime = iStartTime.Add(dur)
  1121  	}
  1122  
  1123  	c.bookFeed.c <- &core.BookUpdate{
  1124  		Action:   core.FreshCandlesAction,
  1125  		Host:     dexAddr,
  1126  		MarketID: mktID,
  1127  		Payload: &core.CandlesPayload{
  1128  			Dur:          durStr,
  1129  			DurMilliSecs: uint64(dur.Milliseconds()),
  1130  			Candles:      candles,
  1131  		},
  1132  	}
  1133  }
  1134  
  1135  var numBuys = 80
  1136  var numSells = 80
  1137  var tokenCounter uint32
  1138  
  1139  func nextToken() string {
  1140  	return strconv.Itoa(int(atomic.AddUint32(&tokenCounter, 1)))
  1141  }
  1142  
  1143  // Book randomizes an order book.
  1144  func (c *TCore) book(dexAddr, mktID string) *core.OrderBook {
  1145  	midGap, maxQty := getMarketStats(mktID)
  1146  	// Set the market width to about 5% of midGap.
  1147  	var buys, sells []*core.MiniOrder
  1148  	c.orderMtx.Lock()
  1149  	c.buys = make(map[string]*core.MiniOrder, numBuys)
  1150  	c.sells = make(map[string]*core.MiniOrder, numSells)
  1151  	c.epochOrders = nil
  1152  
  1153  	mkt := tExchanges[dexAddr].Markets[mktID]
  1154  	for _, ord := range mkt.Orders {
  1155  		if ord.Status != order.OrderStatusBooked {
  1156  			continue
  1157  		}
  1158  		ord := miniOrderFromCoreOrder(ord)
  1159  		if ord.Sell {
  1160  			sells = append(sells, ord)
  1161  			c.sells[ord.Token] = ord
  1162  		} else {
  1163  			buys = append(buys, ord)
  1164  			c.buys[ord.Token] = ord
  1165  		}
  1166  	}
  1167  
  1168  	for i := 0; i < numSells; i++ {
  1169  		ord := randomOrder(true, maxQty, midGap, gapWidthFactor*midGap, false)
  1170  		sells = append(sells, ord)
  1171  		c.sells[ord.Token] = ord
  1172  	}
  1173  	for i := 0; i < numBuys; i++ {
  1174  		// For buys the rate must be smaller than midGap.
  1175  		ord := randomOrder(false, maxQty, midGap, gapWidthFactor*midGap, false)
  1176  		buys = append(buys, ord)
  1177  		c.buys[ord.Token] = ord
  1178  	}
  1179  	recentMatches := make([]*orderbook.MatchSummary, 0, 25)
  1180  	tNow := time.Now()
  1181  	for i := 0; i < 25; i++ {
  1182  		ord := randomOrder(rand.Float32() > 0.5, maxQty, midGap, gapWidthFactor*midGap, false)
  1183  		recentMatches = append(recentMatches, &orderbook.MatchSummary{
  1184  			Rate:  ord.MsgRate,
  1185  			Qty:   ord.QtyAtomic,
  1186  			Stamp: uint64(tNow.Add(-time.Duration(i) * time.Minute).UnixMilli()),
  1187  			Sell:  ord.Sell,
  1188  		})
  1189  	}
  1190  	c.orderMtx.Unlock()
  1191  	sort.Slice(buys, func(i, j int) bool { return buys[i].Rate > buys[j].Rate })
  1192  	sort.Slice(sells, func(i, j int) bool { return sells[i].Rate < sells[j].Rate })
  1193  	return &core.OrderBook{
  1194  		Buys:          buys,
  1195  		Sells:         sells,
  1196  		RecentMatches: recentMatches,
  1197  	}
  1198  }
  1199  
  1200  func (c *TCore) Unsync(dex string, base, quote uint32) {
  1201  	if c.bookFeed != nil {
  1202  		c.killFeed()
  1203  	}
  1204  }
  1205  
  1206  func randomWalletBalance(assetID uint32) *core.WalletBalance {
  1207  	avail := randomBalance()
  1208  	if assetID == 42 && avail < 20e8 {
  1209  		// Make Decred >= 1e8, to accommodate the registration fee.
  1210  		avail = 20e8
  1211  	}
  1212  
  1213  	return &core.WalletBalance{
  1214  		Balance: &db.Balance{
  1215  			Balance: asset.Balance{
  1216  				Available: avail,
  1217  				Immature:  randomBalance(),
  1218  				Locked:    randomBalance(),
  1219  			},
  1220  			Stamp: time.Now().Add(-time.Duration(int64(2 * float64(time.Hour) * rand.Float64()))),
  1221  		},
  1222  		ContractLocked: randomBalance(),
  1223  		BondLocked:     randomBalance(),
  1224  	}
  1225  }
  1226  
  1227  func randomBalance() uint64 {
  1228  	return uint64(rand.Float64() * math.Pow10(rand.Intn(4)+8))
  1229  
  1230  }
  1231  
  1232  func randomBalanceNote(assetID uint32) *core.BalanceNote {
  1233  	return &core.BalanceNote{
  1234  		Notification: db.NewNotification(core.NoteTypeBalance, core.TopicBalanceUpdated, "", "", db.Data),
  1235  		AssetID:      assetID,
  1236  		Balance:      randomWalletBalance(assetID),
  1237  	}
  1238  }
  1239  
  1240  // random number logarithmically between [0, 10^x].
  1241  func tenToThe(x int) float64 {
  1242  	return math.Pow(10, float64(x)*rand.Float64())
  1243  }
  1244  
  1245  func (c *TCore) AssetBalance(assetID uint32) (*core.WalletBalance, error) {
  1246  	balNote := randomBalanceNote(assetID)
  1247  	balNote.Balance.Stamp = time.Now()
  1248  	c.noteFeed <- balNote
  1249  	// c.mtx.Lock()
  1250  	// c.balances[assetID] = balNote.Balances
  1251  	// c.mtx.Unlock()
  1252  	return balNote.Balance, nil
  1253  }
  1254  
  1255  func (c *TCore) AckNotes(ids []dex.Bytes) {}
  1256  
  1257  var configOpts = []*asset.ConfigOption{
  1258  	{
  1259  		DisplayName: "RPC Server",
  1260  		Description: "RPC Server",
  1261  		Key:         "rpc_server",
  1262  	},
  1263  }
  1264  var winfos = map[uint32]*asset.WalletInfo{
  1265  	0:  btc.WalletInfo,
  1266  	2:  ltc.WalletInfo,
  1267  	42: dcr.WalletInfo,
  1268  	22: {
  1269  		SupportedVersions: []uint32{0},
  1270  		UnitInfo: dex.UnitInfo{
  1271  			AtomicUnit: "atoms",
  1272  			Conventional: dex.Denomination{
  1273  				Unit:             "MONA",
  1274  				ConversionFactor: 1e8,
  1275  			},
  1276  		},
  1277  		Name: "Monacoin",
  1278  		AvailableWallets: []*asset.WalletDefinition{
  1279  			{
  1280  				Type:   "1",
  1281  				Tab:    "Native",
  1282  				Seeded: true,
  1283  			},
  1284  			{
  1285  				Type:       "2",
  1286  				Tab:        "External",
  1287  				ConfigOpts: configOpts,
  1288  			},
  1289  		},
  1290  	},
  1291  	3: {
  1292  		SupportedVersions: []uint32{0},
  1293  		UnitInfo: dex.UnitInfo{
  1294  			AtomicUnit: "atoms",
  1295  			Conventional: dex.Denomination{
  1296  				Unit:             "DOGE",
  1297  				ConversionFactor: 1e8,
  1298  			},
  1299  		},
  1300  		Name: "Dogecoin",
  1301  		AvailableWallets: []*asset.WalletDefinition{{
  1302  			Type:       "2",
  1303  			Tab:        "External",
  1304  			ConfigOpts: configOpts,
  1305  		}},
  1306  	},
  1307  	28: {
  1308  		SupportedVersions: []uint32{0},
  1309  		UnitInfo: dex.UnitInfo{
  1310  			AtomicUnit: "Sats",
  1311  			Conventional: dex.Denomination{
  1312  				Unit:             "VTC",
  1313  				ConversionFactor: 1e8,
  1314  			},
  1315  		},
  1316  		Name: "Vertcoin",
  1317  		AvailableWallets: []*asset.WalletDefinition{{
  1318  			Type:       "2",
  1319  			Tab:        "External",
  1320  			ConfigOpts: configOpts,
  1321  		}},
  1322  	},
  1323  	60: &eth.WalletInfo,
  1324  	145: {
  1325  		SupportedVersions: []uint32{0},
  1326  		Name:              "Bitcoin Cash",
  1327  		UnitInfo:          dexbch.UnitInfo,
  1328  		AvailableWallets: []*asset.WalletDefinition{{
  1329  			Type:       "2",
  1330  			Tab:        "External",
  1331  			ConfigOpts: configOpts,
  1332  		}},
  1333  	},
  1334  	133: zec.WalletInfo,
  1335  	966: &polygon.WalletInfo,
  1336  }
  1337  
  1338  var tinfos map[uint32]*asset.Token
  1339  
  1340  func unitInfo(assetID uint32) dex.UnitInfo {
  1341  	if tinfo, found := tinfos[assetID]; found {
  1342  		return tinfo.UnitInfo
  1343  	}
  1344  	return winfos[assetID].UnitInfo
  1345  }
  1346  
  1347  func (c *TCore) WalletState(assetID uint32) *core.WalletState {
  1348  	c.mtx.RLock()
  1349  	defer c.mtx.RUnlock()
  1350  	return c.walletState(assetID)
  1351  }
  1352  
  1353  // walletState should be called with the c.mtx at least RLock'ed.
  1354  func (c *TCore) walletState(assetID uint32) *core.WalletState {
  1355  	w := c.wallets[assetID]
  1356  	if w == nil {
  1357  		return nil
  1358  	}
  1359  
  1360  	traits := asset.WalletTrait(rand.Uint32())
  1361  	if assetID == 42 {
  1362  		traits |= asset.WalletTraitFundsMixer | asset.WalletTraitTicketBuyer
  1363  	}
  1364  
  1365  	syncPct := atomic.LoadUint32(&w.syncProgress)
  1366  	return &core.WalletState{
  1367  		Symbol:       unbip(assetID),
  1368  		AssetID:      assetID,
  1369  		WalletType:   w.walletType,
  1370  		Open:         w.open,
  1371  		Running:      w.running,
  1372  		Address:      ordertest.RandomAddress(),
  1373  		Balance:      c.balances[assetID],
  1374  		Units:        unitInfo(assetID).AtomicUnit,
  1375  		Encrypted:    true,
  1376  		PeerCount:    10,
  1377  		Synced:       syncPct == 100,
  1378  		SyncProgress: float32(syncPct) / 100,
  1379  		SyncStatus:   &asset.SyncStatus{Synced: syncPct == 100, TargetHeight: 100, Blocks: uint64(syncPct)},
  1380  		Traits:       traits,
  1381  	}
  1382  }
  1383  
  1384  func (c *TCore) CreateWallet(appPW, walletPW []byte, form *core.WalletForm) error {
  1385  	randomDelay()
  1386  	if initErrors {
  1387  		return fmt.Errorf("forced init error")
  1388  	}
  1389  	c.mtx.Lock()
  1390  	defer c.mtx.Unlock()
  1391  
  1392  	// If this is a token, simulate parent syncing.
  1393  	token := asset.TokenInfo(form.AssetID)
  1394  	if token == nil || form.ParentForm == nil {
  1395  		c.createWallet(form, false)
  1396  		return nil
  1397  	}
  1398  
  1399  	atomic.StoreUint32(&creationPendingAsset, form.AssetID)
  1400  
  1401  	synced := c.createWallet(form.ParentForm, false)
  1402  
  1403  	c.noteFeed <- &core.WalletCreationNote{
  1404  		Notification: db.NewNotification(core.NoteTypeCreateWallet, core.TopicCreationQueued, "", "", db.Data),
  1405  		AssetID:      form.AssetID,
  1406  	}
  1407  
  1408  	go func() {
  1409  		<-synced
  1410  		defer atomic.StoreUint32(&creationPendingAsset, 0xFFFFFFFF)
  1411  		if doubleCreateAsyncErr {
  1412  			c.noteFeed <- &core.WalletCreationNote{
  1413  				Notification: db.NewNotification(core.NoteTypeCreateWallet, core.TopicQueuedCreationFailed,
  1414  					"Test Error", "This failed because doubleCreateAsyncErr is true in live_test.go", db.Data),
  1415  				AssetID: form.AssetID,
  1416  			}
  1417  			return
  1418  		}
  1419  		c.createWallet(form, true)
  1420  	}()
  1421  	return nil
  1422  }
  1423  
  1424  func (c *TCore) createWallet(form *core.WalletForm, synced bool) (done chan struct{}) {
  1425  	done = make(chan struct{})
  1426  
  1427  	tWallet := &tWalletState{
  1428  		walletType: form.Type,
  1429  		running:    true,
  1430  		open:       true,
  1431  		settings:   form.Config,
  1432  	}
  1433  	c.wallets[form.AssetID] = tWallet
  1434  
  1435  	w := c.walletState(form.AssetID)
  1436  	var regFee uint64
  1437  	r, found := tExchanges[firstDEX].BondAssets[w.Symbol]
  1438  	if found {
  1439  		regFee = r.Amt
  1440  	}
  1441  
  1442  	sendWalletState := func() {
  1443  		wCopy := *w
  1444  		c.noteFeed <- &core.WalletStateNote{
  1445  			Notification: db.NewNotification(core.NoteTypeWalletState, core.TopicWalletState, "", "", db.Data),
  1446  			Wallet:       &wCopy,
  1447  		}
  1448  	}
  1449  
  1450  	defer func() {
  1451  		sendWalletState()
  1452  		if asset.TokenInfo(form.AssetID) != nil {
  1453  			c.noteFeed <- &core.WalletCreationNote{
  1454  				Notification: db.NewNotification(core.NoteTypeCreateWallet, core.TopicQueuedCreationSuccess, "", "", db.Data),
  1455  				AssetID:      form.AssetID,
  1456  			}
  1457  		}
  1458  		if delayBalance {
  1459  			time.AfterFunc(time.Second*10, func() {
  1460  				avail := w.Balance.Available
  1461  				if avail < regFee {
  1462  					avail = 2 * regFee
  1463  					w.Balance.Available = avail
  1464  				}
  1465  				w.Balance.Available = regFee
  1466  				c.noteFeed <- &core.BalanceNote{
  1467  					Notification: db.NewNotification(core.NoteTypeBalance, core.TopicBalanceUpdated, "", "", db.Data),
  1468  					AssetID:      form.AssetID,
  1469  					Balance: &core.WalletBalance{
  1470  						Balance: &db.Balance{
  1471  							Balance: asset.Balance{
  1472  								Available: avail,
  1473  							},
  1474  							Stamp: time.Now(),
  1475  						},
  1476  					},
  1477  				}
  1478  			})
  1479  		}
  1480  	}()
  1481  
  1482  	if !delayBalance {
  1483  		w.Balance.Available = regFee
  1484  	}
  1485  
  1486  	w.Synced = synced
  1487  	if synced {
  1488  		atomic.StoreUint32(&tWallet.syncProgress, 100)
  1489  		w.SyncProgress = 1
  1490  		close(done)
  1491  		return
  1492  	}
  1493  
  1494  	w.SyncProgress = 0.0
  1495  
  1496  	tStart := time.Now()
  1497  	syncDuration := float64(time.Second * 6)
  1498  
  1499  	syncProgress := func() float32 {
  1500  		progress := float64(time.Since(tStart)) / syncDuration
  1501  		if progress > 1 {
  1502  			progress = 1
  1503  		}
  1504  		return float32(progress)
  1505  	}
  1506  
  1507  	setProgress := func() bool {
  1508  		progress := syncProgress()
  1509  		atomic.StoreUint32(&tWallet.syncProgress, uint32(math.Round(float64(progress)*100)))
  1510  		c.mtx.Lock()
  1511  		defer c.mtx.Unlock()
  1512  		w.SyncProgress = progress
  1513  		synced := progress == 1
  1514  		w.Synced = synced
  1515  		sendWalletState()
  1516  		return synced
  1517  	}
  1518  
  1519  	go func() {
  1520  		defer close(done)
  1521  		for {
  1522  			select {
  1523  			case <-time.After(time.Millisecond * 1013):
  1524  				if setProgress() {
  1525  					return
  1526  				}
  1527  			case <-tCtx.Done():
  1528  				return
  1529  			}
  1530  		}
  1531  	}()
  1532  
  1533  	return
  1534  }
  1535  
  1536  func (c *TCore) RescanWallet(assetID uint32, force bool) error {
  1537  	return nil
  1538  }
  1539  
  1540  func (c *TCore) OpenWallet(assetID uint32, pw []byte) error {
  1541  	c.mtx.RLock()
  1542  	defer c.mtx.RUnlock()
  1543  	wallet := c.wallets[assetID]
  1544  	if wallet == nil {
  1545  		return fmt.Errorf("attempting to open non-existent test wallet for asset ID %d", assetID)
  1546  	}
  1547  	if wallet.disabled {
  1548  		return fmt.Errorf("wallet is disabled")
  1549  	}
  1550  	wallet.running = true
  1551  	wallet.open = true
  1552  	return nil
  1553  }
  1554  
  1555  func (c *TCore) ConnectWallet(assetID uint32) error {
  1556  	c.mtx.RLock()
  1557  	defer c.mtx.RUnlock()
  1558  	wallet := c.wallets[assetID]
  1559  	if wallet == nil {
  1560  		return fmt.Errorf("attempting to connect to non-existent test wallet for asset ID %d", assetID)
  1561  	}
  1562  	if wallet.disabled {
  1563  		return fmt.Errorf("wallet is disabled")
  1564  	}
  1565  	wallet.running = true
  1566  	return nil
  1567  }
  1568  
  1569  func (c *TCore) CloseWallet(assetID uint32) error {
  1570  	c.mtx.RLock()
  1571  	defer c.mtx.RUnlock()
  1572  	wallet := c.wallets[assetID]
  1573  	if wallet == nil {
  1574  		return fmt.Errorf("attempting to close non-existent test wallet")
  1575  	}
  1576  
  1577  	if forceDisconnectWallet {
  1578  		wallet.running = false
  1579  	}
  1580  
  1581  	wallet.open = false
  1582  	return nil
  1583  }
  1584  
  1585  func (c *TCore) Wallets() []*core.WalletState {
  1586  	c.mtx.RLock()
  1587  	defer c.mtx.RUnlock()
  1588  	states := make([]*core.WalletState, 0, len(c.wallets))
  1589  	for assetID, wallet := range c.wallets {
  1590  		states = append(states, &core.WalletState{
  1591  			Symbol:    unbip(assetID),
  1592  			AssetID:   assetID,
  1593  			Open:      wallet.open,
  1594  			Running:   wallet.running,
  1595  			Disabled:  wallet.disabled,
  1596  			Address:   ordertest.RandomAddress(),
  1597  			Balance:   c.balances[assetID],
  1598  			Units:     unitInfo(assetID).AtomicUnit,
  1599  			Encrypted: true,
  1600  			Traits:    asset.WalletTrait(rand.Uint32()),
  1601  		})
  1602  	}
  1603  	return states
  1604  }
  1605  func (c *TCore) AccelerateOrder(pw []byte, oidB dex.Bytes, newFeeRate uint64) (string, error) {
  1606  	return "", nil
  1607  }
  1608  func (c *TCore) AccelerationEstimate(oidB dex.Bytes, newFeeRate uint64) (uint64, error) {
  1609  	return 0, nil
  1610  }
  1611  func (c *TCore) PreAccelerateOrder(oidB dex.Bytes) (*core.PreAccelerate, error) {
  1612  	return nil, nil
  1613  }
  1614  func (c *TCore) WalletSettings(assetID uint32) (map[string]string, error) {
  1615  	return c.wallets[assetID].settings, nil
  1616  }
  1617  
  1618  func (c *TCore) ReconfigureWallet(aPW, nPW []byte, form *core.WalletForm) error {
  1619  	c.wallets[form.AssetID].settings = form.Config
  1620  	return nil
  1621  }
  1622  
  1623  func (c *TCore) ToggleWalletStatus(assetID uint32, disable bool) error {
  1624  	w, ok := c.wallets[assetID]
  1625  	if !ok {
  1626  		return fmt.Errorf("wallet with id %d not found", assetID)
  1627  	}
  1628  
  1629  	var err error
  1630  	if disable {
  1631  		err = c.CloseWallet(assetID)
  1632  		c.mtx.Lock()
  1633  		w.disabled = disable
  1634  		c.mtx.Unlock()
  1635  	} else {
  1636  		c.mtx.Lock()
  1637  		w.disabled = disable
  1638  		c.mtx.Unlock()
  1639  		err = c.OpenWallet(assetID, []byte(""))
  1640  	}
  1641  	if err != nil {
  1642  		return err
  1643  	}
  1644  
  1645  	return nil
  1646  }
  1647  
  1648  func (c *TCore) ChangeAppPass(appPW, newAppPW []byte) error {
  1649  	return nil
  1650  }
  1651  
  1652  func (c *TCore) ResetAppPass(newAppPW []byte, seed string) error {
  1653  	return nil
  1654  }
  1655  
  1656  func (c *TCore) NewDepositAddress(assetID uint32) (string, error) {
  1657  	return ordertest.RandomAddress(), nil
  1658  }
  1659  
  1660  func (c *TCore) AddressUsed(assetID uint32, addr string) (bool, error) {
  1661  	return rand.Float32() > 0.5, nil
  1662  }
  1663  
  1664  func (c *TCore) SetWalletPassword(appPW []byte, assetID uint32, newPW []byte) error { return nil }
  1665  
  1666  func (c *TCore) User() *core.User {
  1667  	user := &core.User{
  1668  		Exchanges:   tExchanges,
  1669  		Initialized: c.inited,
  1670  		Assets:      c.SupportedAssets(),
  1671  		FiatRates: map[uint32]float64{
  1672  			0:      64_551.61, // btc
  1673  			2:      59.08,     // ltc
  1674  			42:     25.46,     // dcr
  1675  			22:     0.5117,    // mona
  1676  			28:     0.1599,    // vtc
  1677  			141:    0.2048,    // kmd
  1678  			3:      0.06769,   // doge
  1679  			145:    114.68,    // bch
  1680  			60:     1_209.51,  // eth
  1681  			60001:  0.999,     // usdc.eth
  1682  			133:    26.75,
  1683  			966:    0.7001,
  1684  			966001: 1.001,
  1685  		},
  1686  		Actions: actions,
  1687  	}
  1688  	return user
  1689  }
  1690  
  1691  func (c *TCore) AutoWalletConfig(assetID uint32, walletType string) (map[string]string, error) {
  1692  	return map[string]string{
  1693  		"username": "tacotime",
  1694  		"password": "abc123",
  1695  	}, nil
  1696  }
  1697  
  1698  func (c *TCore) SupportedAssets() map[uint32]*core.SupportedAsset {
  1699  	c.mtx.RLock()
  1700  	defer c.mtx.RUnlock()
  1701  	return map[uint32]*core.SupportedAsset{
  1702  		0:      mkSupportedAsset("btc", c.walletState(0)),
  1703  		42:     mkSupportedAsset("dcr", c.walletState(42)),
  1704  		2:      mkSupportedAsset("ltc", c.walletState(2)),
  1705  		22:     mkSupportedAsset("mona", c.walletState(22)),
  1706  		3:      mkSupportedAsset("doge", c.walletState(3)),
  1707  		28:     mkSupportedAsset("vtc", c.walletState(28)),
  1708  		60:     mkSupportedAsset("eth", c.walletState(60)),
  1709  		145:    mkSupportedAsset("bch", c.walletState(145)),
  1710  		60001:  mkSupportedAsset("usdc.eth", c.walletState(60001)),
  1711  		966:    mkSupportedAsset("polygon", c.walletState(966)),
  1712  		966001: mkSupportedAsset("usdc.polygon", c.walletState(966001)),
  1713  		133:    mkSupportedAsset("zec", c.walletState(133)),
  1714  	}
  1715  }
  1716  
  1717  func (c *TCore) Send(pw []byte, assetID uint32, value uint64, address string, subtract bool) (asset.Coin, error) {
  1718  	return &tCoin{id: []byte{0xde, 0xc7, 0xed}}, nil
  1719  }
  1720  func (c *TCore) Trade(pw []byte, form *core.TradeForm) (*core.Order, error) {
  1721  	return c.trade(form), nil
  1722  }
  1723  func (c *TCore) TradeAsync(pw []byte, form *core.TradeForm) (*core.InFlightOrder, error) {
  1724  	return &core.InFlightOrder{
  1725  		Order:       c.trade(form),
  1726  		TemporaryID: uint64(rand.Int63()),
  1727  	}, nil
  1728  }
  1729  func (c *TCore) trade(form *core.TradeForm) *core.Order {
  1730  	c.OpenWallet(form.Quote, []byte(""))
  1731  	c.OpenWallet(form.Base, []byte(""))
  1732  	oType := order.LimitOrderType
  1733  	if !form.IsLimit {
  1734  		oType = order.MarketOrderType
  1735  	}
  1736  	return &core.Order{
  1737  		ID:    ordertest.RandomOrderID().Bytes(),
  1738  		Type:  oType,
  1739  		Stamp: uint64(time.Now().UnixMilli()),
  1740  		Rate:  form.Rate,
  1741  		Qty:   form.Qty,
  1742  		Sell:  form.Sell,
  1743  	}
  1744  }
  1745  
  1746  func (c *TCore) Cancel(oid dex.Bytes) error {
  1747  	for _, xc := range tExchanges {
  1748  		for _, mkt := range xc.Markets {
  1749  			for _, ord := range mkt.Orders {
  1750  				if ord.ID.String() == oid.String() {
  1751  					ord.Cancelling = true
  1752  				}
  1753  			}
  1754  		}
  1755  	}
  1756  	return nil
  1757  }
  1758  
  1759  func (c *TCore) NotificationFeed() *core.NoteFeed {
  1760  	return &core.NoteFeed{
  1761  		C: c.noteFeed,
  1762  	}
  1763  }
  1764  
  1765  func (c *TCore) runEpochs() {
  1766  	epochTick := time.NewTimer(time.Second).C
  1767  out:
  1768  	for {
  1769  		select {
  1770  		case <-epochTick:
  1771  			epochTick = time.NewTimer(epochDuration - time.Since(time.Now().Truncate(epochDuration))).C
  1772  			c.mtx.RLock()
  1773  			dexAddr := c.dexAddr
  1774  			mktID := c.marketID
  1775  			baseID := c.base
  1776  			quoteID := c.quote
  1777  			baseConnected := false
  1778  			if w := c.wallets[baseID]; w != nil && w.running {
  1779  				baseConnected = true
  1780  			}
  1781  			quoteConnected := false
  1782  			if w := c.wallets[quoteID]; w != nil && w.running {
  1783  				quoteConnected = true
  1784  			}
  1785  			c.mtx.RUnlock()
  1786  
  1787  			if c.dexAddr == "" {
  1788  				continue
  1789  			}
  1790  
  1791  			c.noteFeed <- &core.EpochNotification{
  1792  				Host:         dexAddr,
  1793  				MarketID:     mktID,
  1794  				Notification: db.NewNotification(core.NoteTypeEpoch, core.TopicEpoch, "", "", db.Data),
  1795  				Epoch:        getEpoch(),
  1796  			}
  1797  
  1798  			rateStep := tExchanges[dexAddr].Markets[mktID].RateStep
  1799  			rate := uint64(rand.Intn(1e3)) * rateStep
  1800  			change24 := rand.Float64()*0.3 - .15
  1801  
  1802  			c.noteFeed <- &core.SpotPriceNote{
  1803  				Host:         dexAddr,
  1804  				Notification: db.NewNotification(core.NoteTypeSpots, core.TopicSpotsUpdate, "", "", db.Data),
  1805  				Spots: map[string]*msgjson.Spot{mktID: {
  1806  					Stamp:   uint64(time.Now().UnixMilli()),
  1807  					BaseID:  baseID,
  1808  					QuoteID: quoteID,
  1809  					Rate:    rate,
  1810  					// BookVolume: ,
  1811  					Change24: change24,
  1812  					// Vol24: ,
  1813  				}},
  1814  			}
  1815  
  1816  			// randomize the balance
  1817  			if baseID != unsupportedAssetID && baseConnected { // komodo unsupported
  1818  				c.noteFeed <- randomBalanceNote(baseID)
  1819  			}
  1820  			if quoteID != unsupportedAssetID && quoteConnected { // komodo unsupported
  1821  				c.noteFeed <- randomBalanceNote(quoteID)
  1822  			}
  1823  
  1824  			c.orderMtx.Lock()
  1825  			// Send limit orders as newly booked.
  1826  			for _, o := range c.epochOrders {
  1827  				miniOrder := o.Payload.(*core.MiniOrder)
  1828  				if miniOrder.Rate > 0 {
  1829  					miniOrder.Epoch = 0
  1830  					o.Action = msgjson.BookOrderRoute
  1831  					c.trySend(o)
  1832  					if miniOrder.Sell {
  1833  						c.sells[miniOrder.Token] = miniOrder
  1834  					} else {
  1835  						c.buys[miniOrder.Token] = miniOrder
  1836  					}
  1837  				}
  1838  			}
  1839  			c.epochOrders = nil
  1840  			c.orderMtx.Unlock()
  1841  
  1842  			// Small chance of randomly generating a required action
  1843  			if enableActions && rand.Float32() < 0.05 {
  1844  				c.noteFeed <- &core.WalletNote{
  1845  					Notification: db.NewNotification(core.NoteTypeWalletNote, core.TopicWalletNotification, "", "", db.Data),
  1846  					Payload:      makeRequiredAction(baseID, "missingNonces"),
  1847  				}
  1848  			}
  1849  		case <-tCtx.Done():
  1850  			break out
  1851  		}
  1852  	}
  1853  }
  1854  
  1855  var (
  1856  	randChars = []byte("abcd efgh ijkl mnop qrst uvwx yz123")
  1857  	numChars  = len(randChars)
  1858  )
  1859  
  1860  func randStr(minLen, maxLen int) string {
  1861  	strLen := rand.Intn(maxLen-minLen) + minLen
  1862  	b := make([]byte, 0, strLen)
  1863  	for i := 0; i < strLen; i++ {
  1864  		b = append(b, randChars[rand.Intn(numChars)])
  1865  	}
  1866  	return strings.Trim(string(b), " ")
  1867  }
  1868  
  1869  func (c *TCore) runRandomPokes() {
  1870  	nextWait := func() time.Duration {
  1871  		return time.Duration(float64(time.Second)*rand.Float64()) * 10
  1872  	}
  1873  	for {
  1874  		select {
  1875  		case <-time.NewTimer(nextWait()).C:
  1876  			note := db.NewNotification(randStr(5, 30), core.Topic(randStr(5, 30)), titler.String(randStr(5, 30)), randStr(5, 100), db.Poke)
  1877  			c.noteFeed <- &note
  1878  		case <-tCtx.Done():
  1879  			return
  1880  		}
  1881  	}
  1882  }
  1883  
  1884  func (c *TCore) runRandomNotes() {
  1885  	nextWait := func() time.Duration {
  1886  		return time.Duration(float64(time.Second)*rand.Float64()) * 5
  1887  	}
  1888  	for {
  1889  		select {
  1890  		case <-time.NewTimer(nextWait()).C:
  1891  			roll := rand.Float32()
  1892  			severity := db.Success
  1893  			if roll < 0.05 {
  1894  				severity = db.ErrorLevel
  1895  			} else if roll < 0.10 {
  1896  				severity = db.WarningLevel
  1897  			}
  1898  
  1899  			note := db.NewNotification(randStr(5, 30), core.Topic(randStr(5, 30)), titler.String(randStr(5, 30)), randStr(5, 100), severity)
  1900  			c.noteFeed <- &note
  1901  		case <-tCtx.Done():
  1902  			return
  1903  		}
  1904  	}
  1905  }
  1906  
  1907  func (c *TCore) ExportSeed(pw []byte) (string, error) {
  1908  	return "copper life simple hello fit manage dune curve argue gadget erosion fork theme chase broccoli", nil
  1909  }
  1910  func (c *TCore) WalletLogFilePath(uint32) (string, error) {
  1911  	return "", nil
  1912  }
  1913  func (c *TCore) RecoverWallet(uint32, []byte, bool) error {
  1914  	return nil
  1915  }
  1916  func (c *TCore) UpdateCert(string, []byte) error {
  1917  	return nil
  1918  }
  1919  func (c *TCore) UpdateDEXHost(string, string, []byte, any) (*core.Exchange, error) {
  1920  	return nil, nil
  1921  }
  1922  func (c *TCore) WalletRestorationInfo(pw []byte, assetID uint32) ([]*asset.WalletRestoration, error) {
  1923  	return nil, nil
  1924  }
  1925  func (c *TCore) ToggleRateSourceStatus(src string, disable bool) error {
  1926  	c.fiatSources[src] = !disable
  1927  	return nil
  1928  }
  1929  func (c *TCore) FiatRateSources() map[string]bool {
  1930  	return c.fiatSources
  1931  }
  1932  func (c *TCore) DeleteArchivedRecordsWithBackup(olderThan *time.Time, saveMatchesToFile, saveOrdersToFile bool) (string, int, error) {
  1933  	return "/path/to/records", 10, nil
  1934  }
  1935  func (c *TCore) WalletPeers(assetID uint32) ([]*asset.WalletPeer, error) {
  1936  	return nil, nil
  1937  }
  1938  func (c *TCore) AddWalletPeer(assetID uint32, address string) error {
  1939  	return nil
  1940  }
  1941  func (c *TCore) RemoveWalletPeer(assetID uint32, address string) error {
  1942  	return nil
  1943  }
  1944  func (c *TCore) ApproveToken(appPW []byte, assetID uint32, dexAddr string, onConfirm func()) (string, error) {
  1945  	return "", nil
  1946  }
  1947  func (c *TCore) UnapproveToken(appPW []byte, assetID uint32, version uint32) (string, error) {
  1948  	return "", nil
  1949  }
  1950  func (c *TCore) ApproveTokenFee(assetID uint32, version uint32, approval bool) (uint64, error) {
  1951  	return 0, nil
  1952  }
  1953  
  1954  func (c *TCore) StakeStatus(assetID uint32) (*asset.TicketStakingStatus, error) {
  1955  	res := asset.TicketStakingStatus{
  1956  		TicketPrice:   24000000000,
  1957  		VotingSubsidy: 1200000,
  1958  		VSP:           "",
  1959  		IsRPC:         false,
  1960  		Tickets:       []*asset.Ticket{},
  1961  		Stances: asset.Stances{
  1962  			Agendas:        []*asset.TBAgenda{},
  1963  			TreasurySpends: []*asset.TBTreasurySpend{},
  1964  		},
  1965  		Stats: asset.TicketStats{},
  1966  	}
  1967  	return &res, nil
  1968  }
  1969  
  1970  func (c *TCore) SetVSP(assetID uint32, addr string) error {
  1971  	return nil
  1972  }
  1973  
  1974  func (c *TCore) PurchaseTickets(assetID uint32, pw []byte, n int) error {
  1975  	return nil
  1976  }
  1977  
  1978  func (c *TCore) SetVotingPreferences(assetID uint32, choices, tSpendPolicy, treasuryPolicy map[string]string) error {
  1979  	return nil
  1980  }
  1981  
  1982  func (c *TCore) ListVSPs(assetID uint32) ([]*asset.VotingServiceProvider, error) {
  1983  	vsps := []*asset.VotingServiceProvider{
  1984  		{
  1985  			URL:           "https://example.com",
  1986  			FeePercentage: 0.1,
  1987  			Voting:        12345,
  1988  		},
  1989  	}
  1990  	return vsps, nil
  1991  }
  1992  
  1993  func (c *TCore) TicketPage(assetID uint32, scanStart int32, n, skipN int) ([]*asset.Ticket, error) {
  1994  	return nil, nil
  1995  }
  1996  
  1997  func (c *TCore) FundsMixingStats(assetID uint32) (*asset.FundsMixingStats, error) {
  1998  	return nil, nil
  1999  }
  2000  
  2001  func (c *TCore) ConfigureFundsMixer(appPW []byte, assetID uint32, enabled bool) error {
  2002  	return nil
  2003  }
  2004  
  2005  func (c *TCore) SetLanguage(lang string) error {
  2006  	c.lang = lang
  2007  	return nil
  2008  }
  2009  
  2010  func (c *TCore) Language() string {
  2011  	return c.lang
  2012  }
  2013  
  2014  func (c *TCore) TakeAction(assetID uint32, actionID string, actionB json.RawMessage) error {
  2015  	if rand.Float32() < 0.25 {
  2016  		return fmt.Errorf("it didn't work")
  2017  	}
  2018  	for i, req := range actions {
  2019  		if req.ActionID == actionID && req.AssetID == assetID {
  2020  			copy(actions[i:], actions[i+1:])
  2021  			actions = actions[:len(actions)-1]
  2022  			c.noteFeed <- &core.WalletNote{
  2023  				Notification: db.NewNotification(core.NoteTypeWalletNote, core.TopicWalletNotification, "", "", db.Data),
  2024  				Payload:      makeActionResolved(assetID, req.UniqueID),
  2025  			}
  2026  			break
  2027  		}
  2028  	}
  2029  	return nil
  2030  }
  2031  
  2032  func (c *TCore) RedeemGeocode(appPW, code []byte, msg string) (dex.Bytes, uint64, error) {
  2033  	coinID, _ := hex.DecodeString("308e9a3675fc3ea3862b7863eeead08c621dcc37ff59de597dd3cdab41450ad900000001")
  2034  	return coinID, 100e8, nil
  2035  }
  2036  
  2037  func (*TCore) ExtensionModeConfig() *core.ExtensionModeConfig {
  2038  	return nil
  2039  }
  2040  
  2041  func newMarketDay() *libxc.MarketDay {
  2042  	avgPrice := tenToThe(7)
  2043  	return &libxc.MarketDay{
  2044  		Vol:            tenToThe(7),
  2045  		QuoteVol:       tenToThe(7),
  2046  		PriceChange:    tenToThe(7) - 2*tenToThe(7),
  2047  		PriceChangePct: 0.15 - rand.Float64()*0.3,
  2048  		AvgPrice:       avgPrice,
  2049  		LastPrice:      avgPrice * (1 + (0.05 - 0.1*rand.Float64())),
  2050  		OpenPrice:      avgPrice * (1 + (0.05 - 0.1*rand.Float64())),
  2051  		HighPrice:      avgPrice * (1 + 0.15 + (0.1 - 0.2*rand.Float64())),
  2052  		LowPrice:       avgPrice * (1 - 0.15 + (0.1 - 0.2*rand.Float64())),
  2053  	}
  2054  }
  2055  
  2056  var binanceMarkets = map[string]*libxc.Market{
  2057  	"dcr_btc": {
  2058  		BaseID:  42,
  2059  		QuoteID: 0,
  2060  		Day:     newMarketDay(),
  2061  	},
  2062  	"eth_dcr": {
  2063  		BaseID:  60,
  2064  		QuoteID: 42,
  2065  		Day:     newMarketDay(),
  2066  	},
  2067  	"zec_usdc.polygon": {
  2068  		BaseID:  133,
  2069  		QuoteID: 966001,
  2070  		Day:     newMarketDay(),
  2071  	},
  2072  	"eth_usdc.eth": {
  2073  		BaseID:  60,
  2074  		QuoteID: 60001,
  2075  		Day:     newMarketDay(),
  2076  	},
  2077  }
  2078  
  2079  type TMarketMaker struct {
  2080  	core *TCore
  2081  	cfg  *mm.MarketMakingConfig
  2082  
  2083  	runningBotsMtx sync.RWMutex
  2084  	runningBots    map[mm.MarketWithHost]int64 // mkt -> startTime
  2085  }
  2086  
  2087  func tLotFees() *mm.LotFees {
  2088  	return &mm.LotFees{
  2089  		Swap:   randomBalance() / 100,
  2090  		Redeem: randomBalance() / 100,
  2091  		Refund: randomBalance() / 100,
  2092  	}
  2093  }
  2094  
  2095  func randomProfitLoss(baseID, quoteID uint32) *mm.ProfitLoss {
  2096  	return &mm.ProfitLoss{
  2097  		Initial: map[uint32]*mm.Amount{
  2098  			baseID:  mm.NewAmount(baseID, int64(randomBalance()), tenToThe(5)),
  2099  			quoteID: mm.NewAmount(quoteID, int64(randomBalance()), tenToThe(5)),
  2100  		},
  2101  		InitialUSD: tenToThe(5),
  2102  		Mods: map[uint32]*mm.Amount{
  2103  			baseID:  mm.NewAmount(baseID, int64(randomBalance()), tenToThe(5)),
  2104  			quoteID: mm.NewAmount(quoteID, int64(randomBalance()), tenToThe(5)),
  2105  		},
  2106  		ModsUSD: tenToThe(5),
  2107  		Final: map[uint32]*mm.Amount{
  2108  			baseID:  mm.NewAmount(baseID, int64(randomBalance()), tenToThe(5)),
  2109  			quoteID: mm.NewAmount(quoteID, int64(randomBalance()), tenToThe(5)),
  2110  		},
  2111  		FinalUSD:    tenToThe(5),
  2112  		Profit:      tenToThe(5),
  2113  		ProfitRatio: 0.2 - rand.Float64()*0.4,
  2114  	}
  2115  }
  2116  
  2117  func (m *TMarketMaker) MarketReport(host string, baseID, quoteID uint32) (*mm.MarketReport, error) {
  2118  	baseFiatRate := math.Pow10(3 - rand.Intn(6))
  2119  	quoteFiatRate := math.Pow10(3 - rand.Intn(6))
  2120  	price := baseFiatRate / quoteFiatRate
  2121  	mktID := dex.BipIDSymbol(baseID) + "_" + dex.BipIDSymbol(quoteID)
  2122  	midGap, _ := getMarketStats(mktID)
  2123  	return &mm.MarketReport{
  2124  		BaseFiatRate:  baseFiatRate,
  2125  		QuoteFiatRate: quoteFiatRate,
  2126  		Price:         price,
  2127  		Oracles: []*mm.OracleReport{
  2128  			{
  2129  				Host:     "bittrex.com",
  2130  				USDVol:   tenToThe(7),
  2131  				BestBuy:  midGap * 99 / 100,
  2132  				BestSell: midGap * 101 / 100,
  2133  			},
  2134  			{
  2135  				Host:     "binance.com",
  2136  				USDVol:   tenToThe(7),
  2137  				BestBuy:  midGap * 98 / 100,
  2138  				BestSell: midGap * 102 / 100,
  2139  			},
  2140  		},
  2141  		BaseFees: &mm.LotFeeRange{
  2142  			Max:       tLotFees(),
  2143  			Estimated: tLotFees(),
  2144  		},
  2145  		QuoteFees: &mm.LotFeeRange{
  2146  			Max:       tLotFees(),
  2147  			Estimated: tLotFees(),
  2148  		},
  2149  	}, nil
  2150  }
  2151  
  2152  func (m *TMarketMaker) StartBot(startCfg *mm.StartConfig, alternateConfigPath *string, appPW []byte, overrideLotSizeUpdate bool) (err error) {
  2153  	m.runningBotsMtx.Lock()
  2154  	defer m.runningBotsMtx.Unlock()
  2155  
  2156  	mkt := startCfg.MarketWithHost
  2157  	_, running := m.runningBots[mkt]
  2158  	if running {
  2159  		return fmt.Errorf("bot already running for %s", mkt)
  2160  	}
  2161  	startTime := time.Now().Unix()
  2162  	m.runningBots[mkt] = startTime
  2163  
  2164  	m.core.noteFeed <- &struct {
  2165  		db.Notification
  2166  		Host      string       `json:"host"`
  2167  		Base      uint32       `json:"baseID"`
  2168  		Quote     uint32       `json:"quoteID"`
  2169  		StartTime int64        `json:"startTime"`
  2170  		Stats     *mm.RunStats `json:"stats"`
  2171  	}{
  2172  		Notification: db.NewNotification("runstats", "", "", "", db.Data),
  2173  		Host:         mkt.Host,
  2174  		Base:         mkt.BaseID,
  2175  		Quote:        mkt.QuoteID,
  2176  		StartTime:    startTime,
  2177  		Stats: &mm.RunStats{
  2178  			InitialBalances: map[uint32]uint64{
  2179  				mkt.BaseID: randomBalance(),
  2180  				mkt.BaseID: randomBalance(),
  2181  			},
  2182  			DEXBalances: map[uint32]*mm.BotBalance{
  2183  				mkt.BaseID: {
  2184  					Available: randomBalance(),
  2185  					Locked:    randomBalance(),
  2186  					Pending:   randomBalance(),
  2187  					Reserved:  randomBalance(),
  2188  				},
  2189  				mkt.BaseID: {
  2190  					Available: randomBalance(),
  2191  					Locked:    randomBalance(),
  2192  					Pending:   randomBalance(),
  2193  					Reserved:  randomBalance(),
  2194  				},
  2195  			},
  2196  			CEXBalances: map[uint32]*mm.BotBalance{
  2197  				mkt.BaseID: {
  2198  					Available: randomBalance(),
  2199  					Locked:    randomBalance(),
  2200  					Pending:   randomBalance(),
  2201  					Reserved:  randomBalance(),
  2202  				},
  2203  				mkt.BaseID: {
  2204  					Available: randomBalance(),
  2205  					Locked:    randomBalance(),
  2206  					Pending:   randomBalance(),
  2207  					Reserved:  randomBalance(),
  2208  				},
  2209  			},
  2210  			ProfitLoss:         randomProfitLoss(mkt.BaseID, mkt.QuoteID),
  2211  			StartTime:          startTime,
  2212  			PendingDeposits:    rand.Intn(3),
  2213  			PendingWithdrawals: rand.Intn(3),
  2214  			CompletedMatches:   uint32(math.Pow(10, 3*rand.Float64())),
  2215  			TradedUSD:          math.Pow(10, 3*rand.Float64()),
  2216  			FeeGap:             randomFeeGapStats(),
  2217  		},
  2218  	}
  2219  	return nil
  2220  }
  2221  
  2222  func (m *TMarketMaker) StopBot(mkt *mm.MarketWithHost) error {
  2223  	m.runningBotsMtx.Lock()
  2224  	startTime, running := m.runningBots[*mkt]
  2225  	if !running {
  2226  		m.runningBotsMtx.Unlock()
  2227  		return fmt.Errorf("bot not running for %s", mkt.String())
  2228  	}
  2229  	delete(m.runningBots, *mkt)
  2230  	m.runningBotsMtx.Unlock()
  2231  
  2232  	m.core.noteFeed <- &struct {
  2233  		db.Notification
  2234  		Host      string       `json:"host"`
  2235  		Base      uint32       `json:"baseID"`
  2236  		Quote     uint32       `json:"quoteID"`
  2237  		StartTime int64        `json:"startTime"`
  2238  		Stats     *mm.RunStats `json:"stats"`
  2239  	}{
  2240  		Notification: db.NewNotification("runstats", "", "", "", db.Data),
  2241  		Host:         mkt.Host,
  2242  		Base:         mkt.BaseID,
  2243  		Quote:        mkt.QuoteID,
  2244  		StartTime:    startTime,
  2245  		Stats:        nil,
  2246  	}
  2247  	return nil
  2248  }
  2249  
  2250  func (m *TMarketMaker) UpdateCEXConfig(updatedCfg *mm.CEXConfig) error {
  2251  	for i := 0; i < len(m.cfg.CexConfigs); i++ {
  2252  		cfg := m.cfg.CexConfigs[i]
  2253  		if cfg.Name == updatedCfg.Name {
  2254  			m.cfg.CexConfigs[i] = updatedCfg
  2255  			return nil
  2256  		}
  2257  	}
  2258  	m.cfg.CexConfigs = append(m.cfg.CexConfigs, updatedCfg)
  2259  	return nil
  2260  }
  2261  
  2262  func (m *TMarketMaker) UpdateBotConfig(updatedCfg *mm.BotConfig) error {
  2263  	for i := 0; i < len(m.cfg.BotConfigs); i++ {
  2264  		botCfg := m.cfg.BotConfigs[i]
  2265  		if botCfg.Host == updatedCfg.Host && botCfg.BaseID == updatedCfg.BaseID && botCfg.QuoteID == updatedCfg.QuoteID {
  2266  			m.cfg.BotConfigs[i] = updatedCfg
  2267  			return nil
  2268  		}
  2269  	}
  2270  	m.cfg.BotConfigs = append(m.cfg.BotConfigs, updatedCfg)
  2271  	return nil
  2272  }
  2273  
  2274  func (m *TMarketMaker) UpdateRunningBot(updatedCfg *mm.BotConfig, balanceDiffs *mm.BotInventoryDiffs, saveUpdate bool) error {
  2275  	return m.UpdateBotConfig(updatedCfg)
  2276  }
  2277  
  2278  func (m *TMarketMaker) RemoveBotConfig(host string, baseID, quoteID uint32) error {
  2279  	for i := 0; i < len(m.cfg.BotConfigs); i++ {
  2280  		botCfg := m.cfg.BotConfigs[i]
  2281  		if botCfg.Host == host && botCfg.BaseID == baseID && botCfg.QuoteID == quoteID {
  2282  			copy(m.cfg.BotConfigs[i:], m.cfg.BotConfigs[i+1:])
  2283  			m.cfg.BotConfigs = m.cfg.BotConfigs[:len(m.cfg.BotConfigs)-1]
  2284  		}
  2285  	}
  2286  	return nil
  2287  }
  2288  
  2289  func (m *TMarketMaker) CEXBalance(cexName string, assetID uint32) (*libxc.ExchangeBalance, error) {
  2290  	bal := randomWalletBalance(assetID)
  2291  	return &libxc.ExchangeBalance{
  2292  		Available: bal.Available,
  2293  		Locked:    bal.Locked,
  2294  	}, nil
  2295  }
  2296  
  2297  func randomFeeGapStats() *mm.FeeGapStats {
  2298  	return &mm.FeeGapStats{
  2299  		BasisPrice:    uint64(tenToThe(8) * 1e6),
  2300  		RemoteGap:     uint64(tenToThe(8) * 1e6),
  2301  		FeeGap:        uint64(tenToThe(8) * 1e6),
  2302  		RoundTripFees: uint64(tenToThe(8) * 1e6),
  2303  	}
  2304  }
  2305  
  2306  func (m *TMarketMaker) Status() *mm.Status {
  2307  	status := &mm.Status{
  2308  		CEXes: make(map[string]*mm.CEXStatus, len(m.cfg.CexConfigs)),
  2309  		Bots:  make([]*mm.BotStatus, 0, len(m.cfg.BotConfigs)),
  2310  	}
  2311  	for _, botCfg := range m.cfg.BotConfigs {
  2312  		m.runningBotsMtx.RLock()
  2313  		_, running := m.runningBots[mm.MarketWithHost{Host: botCfg.Host, BaseID: botCfg.BaseID, QuoteID: botCfg.QuoteID}]
  2314  		m.runningBotsMtx.RUnlock()
  2315  		var stats *mm.RunStats
  2316  		if running {
  2317  			stats = &mm.RunStats{
  2318  				InitialBalances: make(map[uint32]uint64),
  2319  				DEXBalances: map[uint32]*mm.BotBalance{
  2320  					botCfg.BaseID:  {Available: randomBalance()},
  2321  					botCfg.QuoteID: {Available: randomBalance()},
  2322  				},
  2323  				CEXBalances: map[uint32]*mm.BotBalance{
  2324  					botCfg.BaseID:  {Available: randomBalance()},
  2325  					botCfg.QuoteID: {Available: randomBalance()},
  2326  				},
  2327  				ProfitLoss:         randomProfitLoss(botCfg.BaseID, botCfg.QuoteID),
  2328  				StartTime:          time.Now().Add(-time.Duration(float64(time.Hour*10) * rand.Float64())).Unix(),
  2329  				PendingDeposits:    rand.Intn(3),
  2330  				PendingWithdrawals: rand.Intn(3),
  2331  				CompletedMatches:   uint32(rand.Intn(200)),
  2332  				TradedUSD:          rand.Float64() * 10_000,
  2333  				FeeGap:             randomFeeGapStats(),
  2334  			}
  2335  		}
  2336  		status.Bots = append(status.Bots, &mm.BotStatus{
  2337  			Config:   botCfg,
  2338  			Running:  stats != nil,
  2339  			RunStats: stats,
  2340  		})
  2341  	}
  2342  	bals := make(map[uint32]*libxc.ExchangeBalance)
  2343  	for _, mkt := range binanceMarkets {
  2344  		for _, assetID := range []uint32{mkt.BaseID, mkt.QuoteID} {
  2345  			if _, found := bals[assetID]; !found {
  2346  				bals[assetID] = &libxc.ExchangeBalance{
  2347  					Available: randomBalance(),
  2348  					Locked:    randomBalance(),
  2349  				}
  2350  			}
  2351  		}
  2352  	}
  2353  	for _, cexCfg := range m.cfg.CexConfigs {
  2354  		status.CEXes[cexCfg.Name] = &mm.CEXStatus{
  2355  			Config:    cexCfg,
  2356  			Connected: rand.Float32() < 0.5,
  2357  			// ConnectionError: "test connection error",
  2358  			Markets:  binanceMarkets,
  2359  			Balances: bals,
  2360  		}
  2361  	}
  2362  	return status
  2363  }
  2364  
  2365  var gapStrategies = []mm.GapStrategy{
  2366  	mm.GapStrategyMultiplier,
  2367  	mm.GapStrategyAbsolute,
  2368  	mm.GapStrategyAbsolutePlus,
  2369  	mm.GapStrategyPercent,
  2370  	mm.GapStrategyPercentPlus,
  2371  }
  2372  
  2373  func randomBotConfig(mkt *mm.MarketWithHost) *mm.BotConfig {
  2374  	cfg := &mm.BotConfig{
  2375  		Host:    mkt.Host,
  2376  		BaseID:  mkt.BaseID,
  2377  		QuoteID: mkt.QuoteID,
  2378  	}
  2379  	newPlacements := func(gapStategy mm.GapStrategy) (lots []uint64, gapFactors []float64) {
  2380  		n := rand.Intn(3)
  2381  		lots, gapFactors = make([]uint64, 0, n), make([]float64, 0, n)
  2382  		maxQty := math.Pow(10, 6+rand.Float64()*6)
  2383  		for i := 0; i < n; i++ {
  2384  			var gapFactor float64
  2385  			switch gapStategy {
  2386  			case mm.GapStrategyAbsolute, mm.GapStrategyAbsolutePlus:
  2387  				gapFactor = math.Exp(-rand.Float64()*5) * maxQty
  2388  			case mm.GapStrategyPercent, mm.GapStrategyPercentPlus:
  2389  				gapFactor = 0.01 + rand.Float64()*0.09
  2390  			default: // multiplier
  2391  				gapFactor = 1 + rand.Float64()
  2392  			}
  2393  			lots = append(lots, uint64(rand.Intn(100)))
  2394  			gapFactors = append(gapFactors, gapFactor)
  2395  		}
  2396  		return
  2397  	}
  2398  
  2399  	typeRoll := rand.Float32()
  2400  	switch {
  2401  	case typeRoll < 0.33: // basic MM
  2402  		gapStrategy := gapStrategies[rand.Intn(len(gapStrategies))]
  2403  		basicCfg := &mm.BasicMarketMakingConfig{
  2404  			GapStrategy:    gapStrategies[rand.Intn(len(gapStrategies))],
  2405  			DriftTolerance: rand.Float64() * 0.01,
  2406  		}
  2407  		cfg.BasicMMConfig = basicCfg
  2408  		lots, gapFactors := newPlacements(gapStrategy)
  2409  		for i := 0; i < len(lots); i++ {
  2410  			p := &mm.OrderPlacement{Lots: lots[i], GapFactor: gapFactors[i]}
  2411  			basicCfg.BuyPlacements = append(basicCfg.BuyPlacements, p)
  2412  			basicCfg.SellPlacements = append(basicCfg.SellPlacements, p)
  2413  		}
  2414  	case typeRoll < 0.67: // arb-mm
  2415  		arbMMCfg := &mm.ArbMarketMakerConfig{
  2416  			Profit:             rand.Float64()*0.03 + 0.005,
  2417  			DriftTolerance:     rand.Float64() * 0.01,
  2418  			NumEpochsLeaveOpen: uint64(rand.Intn(100)),
  2419  		}
  2420  		cfg.ArbMarketMakerConfig = arbMMCfg
  2421  		lots, gapFactors := newPlacements(mm.GapStrategyMultiplier)
  2422  		for i := 0; i < len(lots); i++ {
  2423  			p := &mm.ArbMarketMakingPlacement{Lots: lots[i], Multiplier: gapFactors[i]}
  2424  			arbMMCfg.BuyPlacements = append(arbMMCfg.BuyPlacements, p)
  2425  			arbMMCfg.SellPlacements = append(arbMMCfg.SellPlacements, p)
  2426  		}
  2427  	default: // simple-arb
  2428  		cfg.SimpleArbConfig = &mm.SimpleArbConfig{
  2429  			ProfitTrigger:      rand.Float64()*0.03 + 0.005,
  2430  			MaxActiveArbs:      1 + uint32(rand.Intn(100)),
  2431  			NumEpochsLeaveOpen: uint32(rand.Intn(100)),
  2432  		}
  2433  	}
  2434  	return cfg
  2435  }
  2436  
  2437  func (m *TMarketMaker) RunOverview(startTime int64, mkt *mm.MarketWithHost) (*mm.MarketMakingRunOverview, error) {
  2438  	endTime := time.Unix(startTime, 0).Add(time.Hour * 5).Unix()
  2439  	run := &mm.MarketMakingRunOverview{
  2440  		EndTime: &endTime,
  2441  		Cfgs: []*mm.CfgUpdate{
  2442  			{
  2443  				Cfg:       randomBotConfig(mkt),
  2444  				Timestamp: startTime,
  2445  			},
  2446  		},
  2447  		InitialBalances: make(map[uint32]uint64),
  2448  		ProfitLoss:      randomProfitLoss(mkt.BaseID, mkt.QuoteID),
  2449  	}
  2450  
  2451  	for _, assetID := range []uint32{mkt.BaseID, mkt.QuoteID} {
  2452  		run.InitialBalances[assetID] = randomBalance()
  2453  		if tkn := asset.TokenInfo(assetID); tkn != nil {
  2454  			run.InitialBalances[tkn.ParentID] = randomBalance()
  2455  		}
  2456  	}
  2457  
  2458  	return run, nil
  2459  }
  2460  
  2461  func (m *TMarketMaker) ArchivedRuns() ([]*mm.MarketMakingRun, error) {
  2462  	n := rand.Intn(25)
  2463  	supportedAssets := m.core.SupportedAssets()
  2464  	runs := make([]*mm.MarketMakingRun, 0, n)
  2465  	for i := 0; i < n; i++ {
  2466  		host := firstDEX
  2467  		if rand.Float32() < 0.5 {
  2468  			host = secondDEX
  2469  		}
  2470  		xc := tExchanges[host]
  2471  		mkts := make([]*core.Market, 0, len(xc.Markets))
  2472  		for _, mkt := range xc.Markets {
  2473  			mkts = append(mkts, mkt)
  2474  		}
  2475  		mkt := mkts[rand.Intn(len(mkts))]
  2476  		if supportedAssets[mkt.BaseID] == nil || supportedAssets[mkt.QuoteID] == nil {
  2477  			continue
  2478  		}
  2479  		marketWithHost := &mm.MarketWithHost{
  2480  			Host:    host,
  2481  			BaseID:  mkt.BaseID,
  2482  			QuoteID: mkt.QuoteID,
  2483  		}
  2484  		runs = append(runs, &mm.MarketMakingRun{
  2485  			StartTime: time.Now().Add(-time.Hour * 5 * time.Duration(i)).Unix(),
  2486  			Market:    marketWithHost,
  2487  		})
  2488  	}
  2489  	return runs, nil
  2490  }
  2491  
  2492  func randomWalletTransaction(txType asset.TransactionType, qty uint64) *asset.WalletTransaction {
  2493  	tx := &asset.WalletTransaction{
  2494  		Type:      txType,
  2495  		ID:        ordertest.RandomOrderID().String(),
  2496  		Amount:    qty,
  2497  		Fees:      uint64(float64(qty) * 0.01 * rand.Float64()),
  2498  		Confirmed: rand.Float32() < 0.05,
  2499  	}
  2500  	switch txType {
  2501  	case asset.Redeem, asset.Receive, asset.SelfSend:
  2502  		addr := ordertest.RandomAddress()
  2503  		tx.Recipient = &addr
  2504  	}
  2505  	return tx
  2506  }
  2507  
  2508  func (m *TMarketMaker) RunLogs(startTime int64, mkt *mm.MarketWithHost, n uint64, refID *uint64, filters *mm.RunLogFilters) ([]*mm.MarketMakingEvent, []*mm.MarketMakingEvent, *mm.MarketMakingRunOverview, error) {
  2509  	if n == 0 {
  2510  		n = uint64(rand.Intn(100))
  2511  	}
  2512  	events := make([]*mm.MarketMakingEvent, 0, n)
  2513  	endTime := time.Now().Add(-time.Hour * time.Duration(rand.Intn(1000)))
  2514  	mktID := dex.BipIDSymbol(mkt.BaseID) + "_" + dex.BipIDSymbol(mkt.QuoteID)
  2515  	midGap, maxQty := getMarketStats(mktID)
  2516  	for i := uint64(0); i < n; i++ {
  2517  		ev := &mm.MarketMakingEvent{
  2518  			ID:        i,
  2519  			TimeStamp: endTime.Add(-time.Hour * time.Duration(i)).Unix(),
  2520  			BalanceEffects: &mm.BalanceEffects{
  2521  				Settled: map[uint32]int64{
  2522  					mkt.BaseID:  int64(maxQty * (-0.5 + rand.Float64())),
  2523  					mkt.QuoteID: int64(maxQty * (0.5 + rand.Float64())),
  2524  				},
  2525  				Pending: map[uint32]uint64{
  2526  					mkt.BaseID:  uint64(maxQty * (-0.5 + rand.Float64())),
  2527  					mkt.QuoteID: uint64(maxQty * (0.5 + rand.Float64())),
  2528  				},
  2529  				Locked: map[uint32]uint64{
  2530  					mkt.BaseID:  uint64(maxQty * (-0.5 + rand.Float64())),
  2531  					mkt.QuoteID: uint64(maxQty * (0.5 + rand.Float64())),
  2532  				},
  2533  				Reserved: map[uint32]uint64{},
  2534  			},
  2535  			Pending: i < 10 && rand.Float32() < 0.3,
  2536  			// DEXOrderEvent   *DEXOrderEvent   `json:"dexOrderEvent,omitempty"`
  2537  			// CEXOrderEvent   *CEXOrderEvent   `json:"cexOrderEvent,omitempty"`
  2538  			// DepositEvent    *DepositEvent    `json:"depositEvent,omitempty"`
  2539  			// WithdrawalEvent *WithdrawalEvent `json:"withdrawalEvent,omitempty"`
  2540  		}
  2541  		typeRoll := rand.Float32()
  2542  		switch {
  2543  		case typeRoll < 0.25: // dex order
  2544  			sell := rand.Intn(2) > 0
  2545  			ord := randomOrder(sell, maxQty, midGap, gapWidthFactor*midGap, false)
  2546  			orderEvent := &mm.DEXOrderEvent{
  2547  				ID:   ordertest.RandomOrderID().String(),
  2548  				Rate: ord.MsgRate,
  2549  				Qty:  ord.QtyAtomic,
  2550  				Sell: sell,
  2551  			}
  2552  			ev.DEXOrderEvent = orderEvent
  2553  			if rand.Float32() < 0.7 {
  2554  				orderEvent.Transactions = append(orderEvent.Transactions, randomWalletTransaction(asset.Swap, ord.QtyAtomic))
  2555  				if rand.Float32() < 0.9 {
  2556  					orderEvent.Transactions = append(orderEvent.Transactions, randomWalletTransaction(asset.Redeem, ord.QtyAtomic))
  2557  				} else {
  2558  					orderEvent.Transactions = append(orderEvent.Transactions, randomWalletTransaction(asset.Refund, ord.QtyAtomic))
  2559  				}
  2560  			}
  2561  		case typeRoll < 0.5: // cex order
  2562  			sell := rand.Intn(2) > 0
  2563  			ord := randomOrder(sell, maxQty, midGap, gapWidthFactor*midGap, false)
  2564  			ev.CEXOrderEvent = &mm.CEXOrderEvent{
  2565  				ID:   ordertest.RandomOrderID().String(),
  2566  				Rate: ord.MsgRate,
  2567  				Qty:  ord.QtyAtomic,
  2568  				Sell: sell,
  2569  			}
  2570  		case typeRoll < 0.75: // deposit
  2571  			assetID := mkt.BaseID
  2572  			if rand.Float32() < 0.5 {
  2573  				assetID = mkt.QuoteID
  2574  			}
  2575  			amt := uint64(maxQty * 0.2 * rand.Float64())
  2576  			ev.DepositEvent = &mm.DepositEvent{
  2577  				Transaction: randomWalletTransaction(asset.Send, amt),
  2578  				AssetID:     assetID,
  2579  				CEXCredit:   amt,
  2580  			}
  2581  		default: // withdrawal
  2582  			assetID := mkt.BaseID
  2583  			if rand.Float32() < 0.5 {
  2584  				assetID = mkt.QuoteID
  2585  			}
  2586  			amt := uint64(maxQty * 0.2 * rand.Float64())
  2587  			ev.WithdrawalEvent = &mm.WithdrawalEvent{
  2588  				Transaction: randomWalletTransaction(asset.Receive, amt),
  2589  				AssetID:     assetID,
  2590  				CEXDebit:    amt,
  2591  			}
  2592  		}
  2593  		events = append(events, ev)
  2594  	}
  2595  
  2596  	overview, err := m.RunOverview(startTime, mkt)
  2597  	if err != nil {
  2598  		return nil, nil, nil, err
  2599  	}
  2600  
  2601  	return events, nil, overview, nil
  2602  }
  2603  
  2604  func (m *TMarketMaker) CEXBook(host string, baseID, quoteID uint32) (buys, sells []*core.MiniOrder, _ error) {
  2605  	mktID := dex.BipIDSymbol(baseID) + "_" + dex.BipIDSymbol(quoteID)
  2606  	book := m.core.book(host, mktID)
  2607  	return book.Buys, book.Sells, nil
  2608  }
  2609  
  2610  func makeRequiredAction(assetID uint32, actionID string) *asset.ActionRequiredNote {
  2611  	txID := dex.Bytes(encode.RandomBytes(32)).String()
  2612  	var payload any
  2613  	if actionID == core.ActionIDRedeemRejected {
  2614  		payload = core.RejectedRedemptionData{
  2615  			AssetID: assetID,
  2616  			CoinID:  encode.RandomBytes(32),
  2617  			CoinFmt: "0x8909ec4aa707df569e62e2f8e2040094e2c88fe192b3b3e2dadfa383a41aa645",
  2618  		}
  2619  	} else {
  2620  		payload = &eth.TransactionActionNote{
  2621  			Tx:      randomWalletTransaction(asset.TransactionType(1+rand.Intn(15)), randomBalance()/10), // 1 to 15
  2622  			Nonce:   uint64(rand.Float64() * 500),
  2623  			NewFees: uint64(rand.Float64() * math.Pow10(rand.Intn(8))),
  2624  		}
  2625  	}
  2626  	n := &asset.ActionRequiredNote{
  2627  		ActionID: actionID,
  2628  		UniqueID: txID,
  2629  		Payload:  payload,
  2630  	}
  2631  	n.AssetID = assetID
  2632  	n.Route = "actionRequired"
  2633  	return n
  2634  }
  2635  
  2636  func makeActionResolved(assetID uint32, uniqueID string) *asset.ActionResolvedNote {
  2637  	n := &asset.ActionResolvedNote{
  2638  		UniqueID: uniqueID,
  2639  	}
  2640  	n.AssetID = assetID
  2641  	n.Route = "actionResolved"
  2642  	return n
  2643  }
  2644  
  2645  func TestServer(t *testing.T) {
  2646  	// Register dummy drivers for unimplemented assets.
  2647  	asset.Register(22, &TDriver{})                 // mona
  2648  	asset.Register(28, &TDriver{})                 // vtc
  2649  	asset.Register(unsupportedAssetID, &TDriver{}) // kmd
  2650  	asset.Register(3, &TDriver{})                  // doge
  2651  
  2652  	tinfos = map[uint32]*asset.Token{
  2653  		60001:  asset.TokenInfo(60001),
  2654  		966001: asset.TokenInfo(966001),
  2655  	}
  2656  
  2657  	numBuys = 10
  2658  	numSells = 10
  2659  	feedPeriod = 5000 * time.Millisecond
  2660  	initialize := false
  2661  	register := false
  2662  	forceDisconnectWallet = true
  2663  	gapWidthFactor = 0.2
  2664  	randomPokes = false
  2665  	randomNotes = false
  2666  	numUserOrders = 40
  2667  	delayBalance = true
  2668  	doubleCreateAsyncErr = false
  2669  	randomizeOrdersCount = true
  2670  
  2671  	if enableActions {
  2672  		actions = []*asset.ActionRequiredNote{
  2673  			makeRequiredAction(0, "missingNonces"),
  2674  			makeRequiredAction(42, "lostNonce"),
  2675  			makeRequiredAction(60, "tooCheap"),
  2676  			makeRequiredAction(60, "redeemRejected"),
  2677  		}
  2678  	}
  2679  
  2680  	var shutdown context.CancelFunc
  2681  	tCtx, shutdown = context.WithCancel(context.Background())
  2682  	time.AfterFunc(time.Minute*59, func() { shutdown() })
  2683  	logger := dex.StdOutLogger("TEST", dex.LevelTrace)
  2684  	tCore := newTCore()
  2685  
  2686  	if initialize {
  2687  		tCore.InitializeClient([]byte(""), nil)
  2688  	}
  2689  
  2690  	if register {
  2691  		// initialize is implied and forced if register = true.
  2692  		if !initialize {
  2693  			tCore.InitializeClient([]byte(""), nil)
  2694  		}
  2695  		var assetID uint32 = 42
  2696  		tCore.PostBond(&core.PostBondForm{Addr: firstDEX, Bond: 1, Asset: &assetID})
  2697  	}
  2698  
  2699  	s, err := New(&Config{
  2700  		Core: tCore,
  2701  		MarketMaker: &TMarketMaker{
  2702  			core:        tCore,
  2703  			cfg:         &mm.MarketMakingConfig{},
  2704  			runningBots: make(map[mm.MarketWithHost]int64),
  2705  		},
  2706  		Addr:     "127.0.0.3:54321",
  2707  		Logger:   logger,
  2708  		NoEmbed:  true, // use files on disk, and reload on each page load
  2709  		HttpProf: true,
  2710  	})
  2711  	if err != nil {
  2712  		t.Fatalf("error creating server: %v", err)
  2713  	}
  2714  	cm := dex.NewConnectionMaster(s)
  2715  	err = cm.Connect(tCtx)
  2716  	if err != nil {
  2717  		t.Fatalf("Connect error: %v", err)
  2718  	}
  2719  	go tCore.runEpochs()
  2720  	if randomPokes {
  2721  		go tCore.runRandomPokes()
  2722  	}
  2723  
  2724  	if randomNotes {
  2725  		go tCore.runRandomNotes()
  2726  	}
  2727  
  2728  	cm.Wait()
  2729  }