decred.org/dcrdex@v1.0.3/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) SetWalletPassword(appPW []byte, assetID uint32, newPW []byte) error { return nil }
  1661  
  1662  func (c *TCore) User() *core.User {
  1663  	user := &core.User{
  1664  		Exchanges:   tExchanges,
  1665  		Initialized: c.inited,
  1666  		Assets:      c.SupportedAssets(),
  1667  		FiatRates: map[uint32]float64{
  1668  			0:      64_551.61, // btc
  1669  			2:      59.08,     // ltc
  1670  			42:     25.46,     // dcr
  1671  			22:     0.5117,    // mona
  1672  			28:     0.1599,    // vtc
  1673  			141:    0.2048,    // kmd
  1674  			3:      0.06769,   // doge
  1675  			145:    114.68,    // bch
  1676  			60:     1_209.51,  // eth
  1677  			60001:  0.999,     // usdc.eth
  1678  			133:    26.75,
  1679  			966:    0.7001,
  1680  			966001: 1.001,
  1681  		},
  1682  		Actions: actions,
  1683  	}
  1684  	return user
  1685  }
  1686  
  1687  func (c *TCore) AutoWalletConfig(assetID uint32, walletType string) (map[string]string, error) {
  1688  	return map[string]string{
  1689  		"username": "tacotime",
  1690  		"password": "abc123",
  1691  	}, nil
  1692  }
  1693  
  1694  func (c *TCore) SupportedAssets() map[uint32]*core.SupportedAsset {
  1695  	c.mtx.RLock()
  1696  	defer c.mtx.RUnlock()
  1697  	return map[uint32]*core.SupportedAsset{
  1698  		0:      mkSupportedAsset("btc", c.walletState(0)),
  1699  		42:     mkSupportedAsset("dcr", c.walletState(42)),
  1700  		2:      mkSupportedAsset("ltc", c.walletState(2)),
  1701  		22:     mkSupportedAsset("mona", c.walletState(22)),
  1702  		3:      mkSupportedAsset("doge", c.walletState(3)),
  1703  		28:     mkSupportedAsset("vtc", c.walletState(28)),
  1704  		60:     mkSupportedAsset("eth", c.walletState(60)),
  1705  		145:    mkSupportedAsset("bch", c.walletState(145)),
  1706  		60001:  mkSupportedAsset("usdc.eth", c.walletState(60001)),
  1707  		966:    mkSupportedAsset("polygon", c.walletState(966)),
  1708  		966001: mkSupportedAsset("usdc.polygon", c.walletState(966001)),
  1709  		133:    mkSupportedAsset("zec", c.walletState(133)),
  1710  	}
  1711  }
  1712  
  1713  func (c *TCore) Send(pw []byte, assetID uint32, value uint64, address string, subtract bool) (asset.Coin, error) {
  1714  	return &tCoin{id: []byte{0xde, 0xc7, 0xed}}, nil
  1715  }
  1716  func (c *TCore) Trade(pw []byte, form *core.TradeForm) (*core.Order, error) {
  1717  	return c.trade(form), nil
  1718  }
  1719  func (c *TCore) TradeAsync(pw []byte, form *core.TradeForm) (*core.InFlightOrder, error) {
  1720  	return &core.InFlightOrder{
  1721  		Order:       c.trade(form),
  1722  		TemporaryID: uint64(rand.Int63()),
  1723  	}, nil
  1724  }
  1725  func (c *TCore) trade(form *core.TradeForm) *core.Order {
  1726  	c.OpenWallet(form.Quote, []byte(""))
  1727  	c.OpenWallet(form.Base, []byte(""))
  1728  	oType := order.LimitOrderType
  1729  	if !form.IsLimit {
  1730  		oType = order.MarketOrderType
  1731  	}
  1732  	return &core.Order{
  1733  		ID:    ordertest.RandomOrderID().Bytes(),
  1734  		Type:  oType,
  1735  		Stamp: uint64(time.Now().UnixMilli()),
  1736  		Rate:  form.Rate,
  1737  		Qty:   form.Qty,
  1738  		Sell:  form.Sell,
  1739  	}
  1740  }
  1741  
  1742  func (c *TCore) Cancel(oid dex.Bytes) error {
  1743  	for _, xc := range tExchanges {
  1744  		for _, mkt := range xc.Markets {
  1745  			for _, ord := range mkt.Orders {
  1746  				if ord.ID.String() == oid.String() {
  1747  					ord.Cancelling = true
  1748  				}
  1749  			}
  1750  		}
  1751  	}
  1752  	return nil
  1753  }
  1754  
  1755  func (c *TCore) NotificationFeed() *core.NoteFeed {
  1756  	return &core.NoteFeed{
  1757  		C: c.noteFeed,
  1758  	}
  1759  }
  1760  
  1761  func (c *TCore) runEpochs() {
  1762  	epochTick := time.NewTimer(time.Second).C
  1763  out:
  1764  	for {
  1765  		select {
  1766  		case <-epochTick:
  1767  			epochTick = time.NewTimer(epochDuration - time.Since(time.Now().Truncate(epochDuration))).C
  1768  			c.mtx.RLock()
  1769  			dexAddr := c.dexAddr
  1770  			mktID := c.marketID
  1771  			baseID := c.base
  1772  			quoteID := c.quote
  1773  			baseConnected := false
  1774  			if w := c.wallets[baseID]; w != nil && w.running {
  1775  				baseConnected = true
  1776  			}
  1777  			quoteConnected := false
  1778  			if w := c.wallets[quoteID]; w != nil && w.running {
  1779  				quoteConnected = true
  1780  			}
  1781  			c.mtx.RUnlock()
  1782  
  1783  			if c.dexAddr == "" {
  1784  				continue
  1785  			}
  1786  
  1787  			c.noteFeed <- &core.EpochNotification{
  1788  				Host:         dexAddr,
  1789  				MarketID:     mktID,
  1790  				Notification: db.NewNotification(core.NoteTypeEpoch, core.TopicEpoch, "", "", db.Data),
  1791  				Epoch:        getEpoch(),
  1792  			}
  1793  
  1794  			rateStep := tExchanges[dexAddr].Markets[mktID].RateStep
  1795  			rate := uint64(rand.Intn(1e3)) * rateStep
  1796  			change24 := rand.Float64()*0.3 - .15
  1797  
  1798  			c.noteFeed <- &core.SpotPriceNote{
  1799  				Host:         dexAddr,
  1800  				Notification: db.NewNotification(core.NoteTypeSpots, core.TopicSpotsUpdate, "", "", db.Data),
  1801  				Spots: map[string]*msgjson.Spot{mktID: {
  1802  					Stamp:   uint64(time.Now().UnixMilli()),
  1803  					BaseID:  baseID,
  1804  					QuoteID: quoteID,
  1805  					Rate:    rate,
  1806  					// BookVolume: ,
  1807  					Change24: change24,
  1808  					// Vol24: ,
  1809  				}},
  1810  			}
  1811  
  1812  			// randomize the balance
  1813  			if baseID != unsupportedAssetID && baseConnected { // komodo unsupported
  1814  				c.noteFeed <- randomBalanceNote(baseID)
  1815  			}
  1816  			if quoteID != unsupportedAssetID && quoteConnected { // komodo unsupported
  1817  				c.noteFeed <- randomBalanceNote(quoteID)
  1818  			}
  1819  
  1820  			c.orderMtx.Lock()
  1821  			// Send limit orders as newly booked.
  1822  			for _, o := range c.epochOrders {
  1823  				miniOrder := o.Payload.(*core.MiniOrder)
  1824  				if miniOrder.Rate > 0 {
  1825  					miniOrder.Epoch = 0
  1826  					o.Action = msgjson.BookOrderRoute
  1827  					c.trySend(o)
  1828  					if miniOrder.Sell {
  1829  						c.sells[miniOrder.Token] = miniOrder
  1830  					} else {
  1831  						c.buys[miniOrder.Token] = miniOrder
  1832  					}
  1833  				}
  1834  			}
  1835  			c.epochOrders = nil
  1836  			c.orderMtx.Unlock()
  1837  
  1838  			// Small chance of randomly generating a required action
  1839  			if enableActions && rand.Float32() < 0.05 {
  1840  				c.noteFeed <- &core.WalletNote{
  1841  					Notification: db.NewNotification(core.NoteTypeWalletNote, core.TopicWalletNotification, "", "", db.Data),
  1842  					Payload:      makeRequiredAction(baseID, "missingNonces"),
  1843  				}
  1844  			}
  1845  		case <-tCtx.Done():
  1846  			break out
  1847  		}
  1848  	}
  1849  }
  1850  
  1851  var (
  1852  	randChars = []byte("abcd efgh ijkl mnop qrst uvwx yz123")
  1853  	numChars  = len(randChars)
  1854  )
  1855  
  1856  func randStr(minLen, maxLen int) string {
  1857  	strLen := rand.Intn(maxLen-minLen) + minLen
  1858  	b := make([]byte, 0, strLen)
  1859  	for i := 0; i < strLen; i++ {
  1860  		b = append(b, randChars[rand.Intn(numChars)])
  1861  	}
  1862  	return strings.Trim(string(b), " ")
  1863  }
  1864  
  1865  func (c *TCore) runRandomPokes() {
  1866  	nextWait := func() time.Duration {
  1867  		return time.Duration(float64(time.Second)*rand.Float64()) * 10
  1868  	}
  1869  	for {
  1870  		select {
  1871  		case <-time.NewTimer(nextWait()).C:
  1872  			note := db.NewNotification(randStr(5, 30), core.Topic(randStr(5, 30)), titler.String(randStr(5, 30)), randStr(5, 100), db.Poke)
  1873  			c.noteFeed <- &note
  1874  		case <-tCtx.Done():
  1875  			return
  1876  		}
  1877  	}
  1878  }
  1879  
  1880  func (c *TCore) runRandomNotes() {
  1881  	nextWait := func() time.Duration {
  1882  		return time.Duration(float64(time.Second)*rand.Float64()) * 5
  1883  	}
  1884  	for {
  1885  		select {
  1886  		case <-time.NewTimer(nextWait()).C:
  1887  			roll := rand.Float32()
  1888  			severity := db.Success
  1889  			if roll < 0.05 {
  1890  				severity = db.ErrorLevel
  1891  			} else if roll < 0.10 {
  1892  				severity = db.WarningLevel
  1893  			}
  1894  
  1895  			note := db.NewNotification(randStr(5, 30), core.Topic(randStr(5, 30)), titler.String(randStr(5, 30)), randStr(5, 100), severity)
  1896  			c.noteFeed <- &note
  1897  		case <-tCtx.Done():
  1898  			return
  1899  		}
  1900  	}
  1901  }
  1902  
  1903  func (c *TCore) ExportSeed(pw []byte) (string, error) {
  1904  	return "copper life simple hello fit manage dune curve argue gadget erosion fork theme chase broccoli", nil
  1905  }
  1906  func (c *TCore) WalletLogFilePath(uint32) (string, error) {
  1907  	return "", nil
  1908  }
  1909  func (c *TCore) RecoverWallet(uint32, []byte, bool) error {
  1910  	return nil
  1911  }
  1912  func (c *TCore) UpdateCert(string, []byte) error {
  1913  	return nil
  1914  }
  1915  func (c *TCore) UpdateDEXHost(string, string, []byte, any) (*core.Exchange, error) {
  1916  	return nil, nil
  1917  }
  1918  func (c *TCore) WalletRestorationInfo(pw []byte, assetID uint32) ([]*asset.WalletRestoration, error) {
  1919  	return nil, nil
  1920  }
  1921  func (c *TCore) ToggleRateSourceStatus(src string, disable bool) error {
  1922  	c.fiatSources[src] = !disable
  1923  	return nil
  1924  }
  1925  func (c *TCore) FiatRateSources() map[string]bool {
  1926  	return c.fiatSources
  1927  }
  1928  func (c *TCore) DeleteArchivedRecordsWithBackup(olderThan *time.Time, saveMatchesToFile, saveOrdersToFile bool) (string, int, error) {
  1929  	return "/path/to/records", 10, nil
  1930  }
  1931  func (c *TCore) WalletPeers(assetID uint32) ([]*asset.WalletPeer, error) {
  1932  	return nil, nil
  1933  }
  1934  func (c *TCore) AddWalletPeer(assetID uint32, address string) error {
  1935  	return nil
  1936  }
  1937  func (c *TCore) RemoveWalletPeer(assetID uint32, address string) error {
  1938  	return nil
  1939  }
  1940  func (c *TCore) ApproveToken(appPW []byte, assetID uint32, dexAddr string, onConfirm func()) (string, error) {
  1941  	return "", nil
  1942  }
  1943  func (c *TCore) UnapproveToken(appPW []byte, assetID uint32, version uint32) (string, error) {
  1944  	return "", nil
  1945  }
  1946  func (c *TCore) ApproveTokenFee(assetID uint32, version uint32, approval bool) (uint64, error) {
  1947  	return 0, nil
  1948  }
  1949  
  1950  func (c *TCore) StakeStatus(assetID uint32) (*asset.TicketStakingStatus, error) {
  1951  	res := asset.TicketStakingStatus{
  1952  		TicketPrice:   24000000000,
  1953  		VotingSubsidy: 1200000,
  1954  		VSP:           "",
  1955  		IsRPC:         false,
  1956  		Tickets:       []*asset.Ticket{},
  1957  		Stances: asset.Stances{
  1958  			Agendas:        []*asset.TBAgenda{},
  1959  			TreasurySpends: []*asset.TBTreasurySpend{},
  1960  		},
  1961  		Stats: asset.TicketStats{},
  1962  	}
  1963  	return &res, nil
  1964  }
  1965  
  1966  func (c *TCore) SetVSP(assetID uint32, addr string) error {
  1967  	return nil
  1968  }
  1969  
  1970  func (c *TCore) PurchaseTickets(assetID uint32, pw []byte, n int) error {
  1971  	return nil
  1972  }
  1973  
  1974  func (c *TCore) SetVotingPreferences(assetID uint32, choices, tSpendPolicy, treasuryPolicy map[string]string) error {
  1975  	return nil
  1976  }
  1977  
  1978  func (c *TCore) ListVSPs(assetID uint32) ([]*asset.VotingServiceProvider, error) {
  1979  	vsps := []*asset.VotingServiceProvider{
  1980  		{
  1981  			URL:           "https://example.com",
  1982  			FeePercentage: 0.1,
  1983  			Voting:        12345,
  1984  		},
  1985  	}
  1986  	return vsps, nil
  1987  }
  1988  
  1989  func (c *TCore) TicketPage(assetID uint32, scanStart int32, n, skipN int) ([]*asset.Ticket, error) {
  1990  	return nil, nil
  1991  }
  1992  
  1993  func (c *TCore) FundsMixingStats(assetID uint32) (*asset.FundsMixingStats, error) {
  1994  	return nil, nil
  1995  }
  1996  
  1997  func (c *TCore) ConfigureFundsMixer(appPW []byte, assetID uint32, enabled bool) error {
  1998  	return nil
  1999  }
  2000  
  2001  func (c *TCore) SetLanguage(lang string) error {
  2002  	c.lang = lang
  2003  	return nil
  2004  }
  2005  
  2006  func (c *TCore) Language() string {
  2007  	return c.lang
  2008  }
  2009  
  2010  func (c *TCore) TakeAction(assetID uint32, actionID string, actionB json.RawMessage) error {
  2011  	if rand.Float32() < 0.25 {
  2012  		return fmt.Errorf("it didn't work")
  2013  	}
  2014  	for i, req := range actions {
  2015  		if req.ActionID == actionID && req.AssetID == assetID {
  2016  			copy(actions[i:], actions[i+1:])
  2017  			actions = actions[:len(actions)-1]
  2018  			c.noteFeed <- &core.WalletNote{
  2019  				Notification: db.NewNotification(core.NoteTypeWalletNote, core.TopicWalletNotification, "", "", db.Data),
  2020  				Payload:      makeActionResolved(assetID, req.UniqueID),
  2021  			}
  2022  			break
  2023  		}
  2024  	}
  2025  	return nil
  2026  }
  2027  
  2028  func (c *TCore) RedeemGeocode(appPW, code []byte, msg string) (dex.Bytes, uint64, error) {
  2029  	coinID, _ := hex.DecodeString("308e9a3675fc3ea3862b7863eeead08c621dcc37ff59de597dd3cdab41450ad900000001")
  2030  	return coinID, 100e8, nil
  2031  }
  2032  
  2033  func (*TCore) ExtensionModeConfig() *core.ExtensionModeConfig {
  2034  	return nil
  2035  }
  2036  
  2037  func newMarketDay() *libxc.MarketDay {
  2038  	avgPrice := tenToThe(7)
  2039  	return &libxc.MarketDay{
  2040  		Vol:            tenToThe(7),
  2041  		QuoteVol:       tenToThe(7),
  2042  		PriceChange:    tenToThe(7) - 2*tenToThe(7),
  2043  		PriceChangePct: 0.15 - rand.Float64()*0.3,
  2044  		AvgPrice:       avgPrice,
  2045  		LastPrice:      avgPrice * (1 + (0.05 - 0.1*rand.Float64())),
  2046  		OpenPrice:      avgPrice * (1 + (0.05 - 0.1*rand.Float64())),
  2047  		HighPrice:      avgPrice * (1 + 0.15 + (0.1 - 0.2*rand.Float64())),
  2048  		LowPrice:       avgPrice * (1 - 0.15 + (0.1 - 0.2*rand.Float64())),
  2049  	}
  2050  }
  2051  
  2052  var binanceMarkets = map[string]*libxc.Market{
  2053  	"dcr_btc": {
  2054  		BaseID:  42,
  2055  		QuoteID: 0,
  2056  		Day:     newMarketDay(),
  2057  	},
  2058  	"eth_dcr": {
  2059  		BaseID:  60,
  2060  		QuoteID: 42,
  2061  		Day:     newMarketDay(),
  2062  	},
  2063  	"zec_usdc.polygon": {
  2064  		BaseID:  133,
  2065  		QuoteID: 966001,
  2066  		Day:     newMarketDay(),
  2067  	},
  2068  	"eth_usdc.eth": {
  2069  		BaseID:  60,
  2070  		QuoteID: 60001,
  2071  		Day:     newMarketDay(),
  2072  	},
  2073  }
  2074  
  2075  type TMarketMaker struct {
  2076  	core *TCore
  2077  	cfg  *mm.MarketMakingConfig
  2078  
  2079  	runningBotsMtx sync.RWMutex
  2080  	runningBots    map[mm.MarketWithHost]int64 // mkt -> startTime
  2081  }
  2082  
  2083  func tLotFees() *mm.LotFees {
  2084  	return &mm.LotFees{
  2085  		Swap:   randomBalance() / 100,
  2086  		Redeem: randomBalance() / 100,
  2087  		Refund: randomBalance() / 100,
  2088  	}
  2089  }
  2090  
  2091  func randomProfitLoss(baseID, quoteID uint32) *mm.ProfitLoss {
  2092  	return &mm.ProfitLoss{
  2093  		Initial: map[uint32]*mm.Amount{
  2094  			baseID:  mm.NewAmount(baseID, int64(randomBalance()), tenToThe(5)),
  2095  			quoteID: mm.NewAmount(quoteID, int64(randomBalance()), tenToThe(5)),
  2096  		},
  2097  		InitialUSD: tenToThe(5),
  2098  		Mods: map[uint32]*mm.Amount{
  2099  			baseID:  mm.NewAmount(baseID, int64(randomBalance()), tenToThe(5)),
  2100  			quoteID: mm.NewAmount(quoteID, int64(randomBalance()), tenToThe(5)),
  2101  		},
  2102  		ModsUSD: tenToThe(5),
  2103  		Final: map[uint32]*mm.Amount{
  2104  			baseID:  mm.NewAmount(baseID, int64(randomBalance()), tenToThe(5)),
  2105  			quoteID: mm.NewAmount(quoteID, int64(randomBalance()), tenToThe(5)),
  2106  		},
  2107  		FinalUSD:    tenToThe(5),
  2108  		Profit:      tenToThe(5),
  2109  		ProfitRatio: 0.2 - rand.Float64()*0.4,
  2110  	}
  2111  }
  2112  
  2113  func (m *TMarketMaker) MarketReport(host string, baseID, quoteID uint32) (*mm.MarketReport, error) {
  2114  	baseFiatRate := math.Pow10(3 - rand.Intn(6))
  2115  	quoteFiatRate := math.Pow10(3 - rand.Intn(6))
  2116  	price := baseFiatRate / quoteFiatRate
  2117  	mktID := dex.BipIDSymbol(baseID) + "_" + dex.BipIDSymbol(quoteID)
  2118  	midGap, _ := getMarketStats(mktID)
  2119  	return &mm.MarketReport{
  2120  		BaseFiatRate:  baseFiatRate,
  2121  		QuoteFiatRate: quoteFiatRate,
  2122  		Price:         price,
  2123  		Oracles: []*mm.OracleReport{
  2124  			{
  2125  				Host:     "bittrex.com",
  2126  				USDVol:   tenToThe(7),
  2127  				BestBuy:  midGap * 99 / 100,
  2128  				BestSell: midGap * 101 / 100,
  2129  			},
  2130  			{
  2131  				Host:     "binance.com",
  2132  				USDVol:   tenToThe(7),
  2133  				BestBuy:  midGap * 98 / 100,
  2134  				BestSell: midGap * 102 / 100,
  2135  			},
  2136  		},
  2137  		BaseFees: &mm.LotFeeRange{
  2138  			Max:       tLotFees(),
  2139  			Estimated: tLotFees(),
  2140  		},
  2141  		QuoteFees: &mm.LotFeeRange{
  2142  			Max:       tLotFees(),
  2143  			Estimated: tLotFees(),
  2144  		},
  2145  	}, nil
  2146  }
  2147  
  2148  func (m *TMarketMaker) StartBot(startCfg *mm.StartConfig, alternateConfigPath *string, appPW []byte) (err error) {
  2149  	m.runningBotsMtx.Lock()
  2150  	defer m.runningBotsMtx.Unlock()
  2151  
  2152  	mkt := startCfg.MarketWithHost
  2153  	_, running := m.runningBots[mkt]
  2154  	if running {
  2155  		return fmt.Errorf("bot already running for %s", mkt)
  2156  	}
  2157  	startTime := time.Now().Unix()
  2158  	m.runningBots[mkt] = startTime
  2159  
  2160  	m.core.noteFeed <- &struct {
  2161  		db.Notification
  2162  		Host      string       `json:"host"`
  2163  		Base      uint32       `json:"baseID"`
  2164  		Quote     uint32       `json:"quoteID"`
  2165  		StartTime int64        `json:"startTime"`
  2166  		Stats     *mm.RunStats `json:"stats"`
  2167  	}{
  2168  		Notification: db.NewNotification("runstats", "", "", "", db.Data),
  2169  		Host:         mkt.Host,
  2170  		Base:         mkt.BaseID,
  2171  		Quote:        mkt.QuoteID,
  2172  		StartTime:    startTime,
  2173  		Stats: &mm.RunStats{
  2174  			InitialBalances: map[uint32]uint64{
  2175  				mkt.BaseID: randomBalance(),
  2176  				mkt.BaseID: randomBalance(),
  2177  			},
  2178  			DEXBalances: map[uint32]*mm.BotBalance{
  2179  				mkt.BaseID: {
  2180  					Available: randomBalance(),
  2181  					Locked:    randomBalance(),
  2182  					Pending:   randomBalance(),
  2183  					Reserved:  randomBalance(),
  2184  				},
  2185  				mkt.BaseID: {
  2186  					Available: randomBalance(),
  2187  					Locked:    randomBalance(),
  2188  					Pending:   randomBalance(),
  2189  					Reserved:  randomBalance(),
  2190  				},
  2191  			},
  2192  			CEXBalances: map[uint32]*mm.BotBalance{
  2193  				mkt.BaseID: {
  2194  					Available: randomBalance(),
  2195  					Locked:    randomBalance(),
  2196  					Pending:   randomBalance(),
  2197  					Reserved:  randomBalance(),
  2198  				},
  2199  				mkt.BaseID: {
  2200  					Available: randomBalance(),
  2201  					Locked:    randomBalance(),
  2202  					Pending:   randomBalance(),
  2203  					Reserved:  randomBalance(),
  2204  				},
  2205  			},
  2206  			ProfitLoss:         randomProfitLoss(mkt.BaseID, mkt.QuoteID),
  2207  			StartTime:          startTime,
  2208  			PendingDeposits:    rand.Intn(3),
  2209  			PendingWithdrawals: rand.Intn(3),
  2210  			CompletedMatches:   uint32(math.Pow(10, 3*rand.Float64())),
  2211  			TradedUSD:          math.Pow(10, 3*rand.Float64()),
  2212  			FeeGap:             randomFeeGapStats(),
  2213  		},
  2214  	}
  2215  	return nil
  2216  }
  2217  
  2218  func (m *TMarketMaker) StopBot(mkt *mm.MarketWithHost) error {
  2219  	m.runningBotsMtx.Lock()
  2220  	startTime, running := m.runningBots[*mkt]
  2221  	if !running {
  2222  		m.runningBotsMtx.Unlock()
  2223  		return fmt.Errorf("bot not running for %s", mkt.String())
  2224  	}
  2225  	delete(m.runningBots, *mkt)
  2226  	m.runningBotsMtx.Unlock()
  2227  
  2228  	m.core.noteFeed <- &struct {
  2229  		db.Notification
  2230  		Host      string       `json:"host"`
  2231  		Base      uint32       `json:"baseID"`
  2232  		Quote     uint32       `json:"quoteID"`
  2233  		StartTime int64        `json:"startTime"`
  2234  		Stats     *mm.RunStats `json:"stats"`
  2235  	}{
  2236  		Notification: db.NewNotification("runstats", "", "", "", db.Data),
  2237  		Host:         mkt.Host,
  2238  		Base:         mkt.BaseID,
  2239  		Quote:        mkt.QuoteID,
  2240  		StartTime:    startTime,
  2241  		Stats:        nil,
  2242  	}
  2243  	return nil
  2244  }
  2245  
  2246  func (m *TMarketMaker) UpdateCEXConfig(updatedCfg *mm.CEXConfig) error {
  2247  	for i := 0; i < len(m.cfg.CexConfigs); i++ {
  2248  		cfg := m.cfg.CexConfigs[i]
  2249  		if cfg.Name == updatedCfg.Name {
  2250  			m.cfg.CexConfigs[i] = updatedCfg
  2251  			return nil
  2252  		}
  2253  	}
  2254  	m.cfg.CexConfigs = append(m.cfg.CexConfigs, updatedCfg)
  2255  	return nil
  2256  }
  2257  
  2258  func (m *TMarketMaker) UpdateBotConfig(updatedCfg *mm.BotConfig) error {
  2259  	for i := 0; i < len(m.cfg.BotConfigs); i++ {
  2260  		botCfg := m.cfg.BotConfigs[i]
  2261  		if botCfg.Host == updatedCfg.Host && botCfg.BaseID == updatedCfg.BaseID && botCfg.QuoteID == updatedCfg.QuoteID {
  2262  			m.cfg.BotConfigs[i] = updatedCfg
  2263  			return nil
  2264  		}
  2265  	}
  2266  	m.cfg.BotConfigs = append(m.cfg.BotConfigs, updatedCfg)
  2267  	return nil
  2268  }
  2269  
  2270  func (m *TMarketMaker) UpdateRunningBot(updatedCfg *mm.BotConfig, balanceDiffs *mm.BotInventoryDiffs, saveUpdate bool) error {
  2271  	return m.UpdateBotConfig(updatedCfg)
  2272  }
  2273  
  2274  func (m *TMarketMaker) RemoveBotConfig(host string, baseID, quoteID uint32) error {
  2275  	for i := 0; i < len(m.cfg.BotConfigs); i++ {
  2276  		botCfg := m.cfg.BotConfigs[i]
  2277  		if botCfg.Host == host && botCfg.BaseID == baseID && botCfg.QuoteID == quoteID {
  2278  			copy(m.cfg.BotConfigs[i:], m.cfg.BotConfigs[i+1:])
  2279  			m.cfg.BotConfigs = m.cfg.BotConfigs[:len(m.cfg.BotConfigs)-1]
  2280  		}
  2281  	}
  2282  	return nil
  2283  }
  2284  
  2285  func (m *TMarketMaker) CEXBalance(cexName string, assetID uint32) (*libxc.ExchangeBalance, error) {
  2286  	bal := randomWalletBalance(assetID)
  2287  	return &libxc.ExchangeBalance{
  2288  		Available: bal.Available,
  2289  		Locked:    bal.Locked,
  2290  	}, nil
  2291  }
  2292  
  2293  func randomFeeGapStats() *mm.FeeGapStats {
  2294  	return &mm.FeeGapStats{
  2295  		BasisPrice:    uint64(tenToThe(8) * 1e6),
  2296  		RemoteGap:     uint64(tenToThe(8) * 1e6),
  2297  		FeeGap:        uint64(tenToThe(8) * 1e6),
  2298  		RoundTripFees: uint64(tenToThe(8) * 1e6),
  2299  	}
  2300  }
  2301  
  2302  func (m *TMarketMaker) Status() *mm.Status {
  2303  	status := &mm.Status{
  2304  		CEXes: make(map[string]*mm.CEXStatus, len(m.cfg.CexConfigs)),
  2305  		Bots:  make([]*mm.BotStatus, 0, len(m.cfg.BotConfigs)),
  2306  	}
  2307  	for _, botCfg := range m.cfg.BotConfigs {
  2308  		m.runningBotsMtx.RLock()
  2309  		_, running := m.runningBots[mm.MarketWithHost{Host: botCfg.Host, BaseID: botCfg.BaseID, QuoteID: botCfg.QuoteID}]
  2310  		m.runningBotsMtx.RUnlock()
  2311  		var stats *mm.RunStats
  2312  		if running {
  2313  			stats = &mm.RunStats{
  2314  				InitialBalances: make(map[uint32]uint64),
  2315  				DEXBalances: map[uint32]*mm.BotBalance{
  2316  					botCfg.BaseID:  {Available: randomBalance()},
  2317  					botCfg.QuoteID: {Available: randomBalance()},
  2318  				},
  2319  				CEXBalances: map[uint32]*mm.BotBalance{
  2320  					botCfg.BaseID:  {Available: randomBalance()},
  2321  					botCfg.QuoteID: {Available: randomBalance()},
  2322  				},
  2323  				ProfitLoss:         randomProfitLoss(botCfg.BaseID, botCfg.QuoteID),
  2324  				StartTime:          time.Now().Add(-time.Duration(float64(time.Hour*10) * rand.Float64())).Unix(),
  2325  				PendingDeposits:    rand.Intn(3),
  2326  				PendingWithdrawals: rand.Intn(3),
  2327  				CompletedMatches:   uint32(rand.Intn(200)),
  2328  				TradedUSD:          rand.Float64() * 10_000,
  2329  				FeeGap:             randomFeeGapStats(),
  2330  			}
  2331  		}
  2332  		status.Bots = append(status.Bots, &mm.BotStatus{
  2333  			Config:   botCfg,
  2334  			Running:  stats != nil,
  2335  			RunStats: stats,
  2336  		})
  2337  	}
  2338  	bals := make(map[uint32]*libxc.ExchangeBalance)
  2339  	for _, mkt := range binanceMarkets {
  2340  		for _, assetID := range []uint32{mkt.BaseID, mkt.QuoteID} {
  2341  			if _, found := bals[assetID]; !found {
  2342  				bals[assetID] = &libxc.ExchangeBalance{
  2343  					Available: randomBalance(),
  2344  					Locked:    randomBalance(),
  2345  				}
  2346  			}
  2347  		}
  2348  	}
  2349  	for _, cexCfg := range m.cfg.CexConfigs {
  2350  		status.CEXes[cexCfg.Name] = &mm.CEXStatus{
  2351  			Config:    cexCfg,
  2352  			Connected: rand.Float32() < 0.5,
  2353  			// ConnectionError: "test connection error",
  2354  			Markets:  binanceMarkets,
  2355  			Balances: bals,
  2356  		}
  2357  	}
  2358  	return status
  2359  }
  2360  
  2361  var gapStrategies = []mm.GapStrategy{
  2362  	mm.GapStrategyMultiplier,
  2363  	mm.GapStrategyAbsolute,
  2364  	mm.GapStrategyAbsolutePlus,
  2365  	mm.GapStrategyPercent,
  2366  	mm.GapStrategyPercentPlus,
  2367  }
  2368  
  2369  func randomBotConfig(mkt *mm.MarketWithHost) *mm.BotConfig {
  2370  	cfg := &mm.BotConfig{
  2371  		Host:    mkt.Host,
  2372  		BaseID:  mkt.BaseID,
  2373  		QuoteID: mkt.QuoteID,
  2374  	}
  2375  	newPlacements := func(gapStategy mm.GapStrategy) (lots []uint64, gapFactors []float64) {
  2376  		n := rand.Intn(3)
  2377  		lots, gapFactors = make([]uint64, 0, n), make([]float64, 0, n)
  2378  		maxQty := math.Pow(10, 6+rand.Float64()*6)
  2379  		for i := 0; i < n; i++ {
  2380  			var gapFactor float64
  2381  			switch gapStategy {
  2382  			case mm.GapStrategyAbsolute, mm.GapStrategyAbsolutePlus:
  2383  				gapFactor = math.Exp(-rand.Float64()*5) * maxQty
  2384  			case mm.GapStrategyPercent, mm.GapStrategyPercentPlus:
  2385  				gapFactor = 0.01 + rand.Float64()*0.09
  2386  			default: // multiplier
  2387  				gapFactor = 1 + rand.Float64()
  2388  			}
  2389  			lots = append(lots, uint64(rand.Intn(100)))
  2390  			gapFactors = append(gapFactors, gapFactor)
  2391  		}
  2392  		return
  2393  	}
  2394  
  2395  	typeRoll := rand.Float32()
  2396  	switch {
  2397  	case typeRoll < 0.33: // basic MM
  2398  		gapStrategy := gapStrategies[rand.Intn(len(gapStrategies))]
  2399  		basicCfg := &mm.BasicMarketMakingConfig{
  2400  			GapStrategy:    gapStrategies[rand.Intn(len(gapStrategies))],
  2401  			DriftTolerance: rand.Float64() * 0.01,
  2402  		}
  2403  		cfg.BasicMMConfig = basicCfg
  2404  		lots, gapFactors := newPlacements(gapStrategy)
  2405  		for i := 0; i < len(lots); i++ {
  2406  			p := &mm.OrderPlacement{Lots: lots[i], GapFactor: gapFactors[i]}
  2407  			basicCfg.BuyPlacements = append(basicCfg.BuyPlacements, p)
  2408  			basicCfg.SellPlacements = append(basicCfg.SellPlacements, p)
  2409  		}
  2410  	case typeRoll < 0.67: // arb-mm
  2411  		arbMMCfg := &mm.ArbMarketMakerConfig{
  2412  			Profit:             rand.Float64()*0.03 + 0.005,
  2413  			DriftTolerance:     rand.Float64() * 0.01,
  2414  			NumEpochsLeaveOpen: uint64(rand.Intn(100)),
  2415  		}
  2416  		cfg.ArbMarketMakerConfig = arbMMCfg
  2417  		lots, gapFactors := newPlacements(mm.GapStrategyMultiplier)
  2418  		for i := 0; i < len(lots); i++ {
  2419  			p := &mm.ArbMarketMakingPlacement{Lots: lots[i], Multiplier: gapFactors[i]}
  2420  			arbMMCfg.BuyPlacements = append(arbMMCfg.BuyPlacements, p)
  2421  			arbMMCfg.SellPlacements = append(arbMMCfg.SellPlacements, p)
  2422  		}
  2423  	default: // simple-arb
  2424  		cfg.SimpleArbConfig = &mm.SimpleArbConfig{
  2425  			ProfitTrigger:      rand.Float64()*0.03 + 0.005,
  2426  			MaxActiveArbs:      1 + uint32(rand.Intn(100)),
  2427  			NumEpochsLeaveOpen: uint32(rand.Intn(100)),
  2428  		}
  2429  	}
  2430  	return cfg
  2431  }
  2432  
  2433  func (m *TMarketMaker) RunOverview(startTime int64, mkt *mm.MarketWithHost) (*mm.MarketMakingRunOverview, error) {
  2434  	endTime := time.Unix(startTime, 0).Add(time.Hour * 5).Unix()
  2435  	run := &mm.MarketMakingRunOverview{
  2436  		EndTime: &endTime,
  2437  		Cfgs: []*mm.CfgUpdate{
  2438  			{
  2439  				Cfg:       randomBotConfig(mkt),
  2440  				Timestamp: startTime,
  2441  			},
  2442  		},
  2443  		InitialBalances: make(map[uint32]uint64),
  2444  		ProfitLoss:      randomProfitLoss(mkt.BaseID, mkt.QuoteID),
  2445  	}
  2446  
  2447  	for _, assetID := range []uint32{mkt.BaseID, mkt.QuoteID} {
  2448  		run.InitialBalances[assetID] = randomBalance()
  2449  		if tkn := asset.TokenInfo(assetID); tkn != nil {
  2450  			run.InitialBalances[tkn.ParentID] = randomBalance()
  2451  		}
  2452  	}
  2453  
  2454  	return run, nil
  2455  }
  2456  
  2457  func (m *TMarketMaker) ArchivedRuns() ([]*mm.MarketMakingRun, error) {
  2458  	n := rand.Intn(25)
  2459  	supportedAssets := m.core.SupportedAssets()
  2460  	runs := make([]*mm.MarketMakingRun, 0, n)
  2461  	for i := 0; i < n; i++ {
  2462  		host := firstDEX
  2463  		if rand.Float32() < 0.5 {
  2464  			host = secondDEX
  2465  		}
  2466  		xc := tExchanges[host]
  2467  		mkts := make([]*core.Market, 0, len(xc.Markets))
  2468  		for _, mkt := range xc.Markets {
  2469  			mkts = append(mkts, mkt)
  2470  		}
  2471  		mkt := mkts[rand.Intn(len(mkts))]
  2472  		if supportedAssets[mkt.BaseID] == nil || supportedAssets[mkt.QuoteID] == nil {
  2473  			continue
  2474  		}
  2475  		marketWithHost := &mm.MarketWithHost{
  2476  			Host:    host,
  2477  			BaseID:  mkt.BaseID,
  2478  			QuoteID: mkt.QuoteID,
  2479  		}
  2480  		runs = append(runs, &mm.MarketMakingRun{
  2481  			StartTime: time.Now().Add(-time.Hour * 5 * time.Duration(i)).Unix(),
  2482  			Market:    marketWithHost,
  2483  		})
  2484  	}
  2485  	return runs, nil
  2486  }
  2487  
  2488  func randomWalletTransaction(txType asset.TransactionType, qty uint64) *asset.WalletTransaction {
  2489  	tx := &asset.WalletTransaction{
  2490  		Type:      txType,
  2491  		ID:        ordertest.RandomOrderID().String(),
  2492  		Amount:    qty,
  2493  		Fees:      uint64(float64(qty) * 0.01 * rand.Float64()),
  2494  		Confirmed: rand.Float32() < 0.05,
  2495  	}
  2496  	switch txType {
  2497  	case asset.Redeem, asset.Receive, asset.SelfSend:
  2498  		addr := ordertest.RandomAddress()
  2499  		tx.Recipient = &addr
  2500  	}
  2501  	return tx
  2502  }
  2503  
  2504  func (m *TMarketMaker) RunLogs(startTime int64, mkt *mm.MarketWithHost, n uint64, refID *uint64, filters *mm.RunLogFilters) ([]*mm.MarketMakingEvent, []*mm.MarketMakingEvent, *mm.MarketMakingRunOverview, error) {
  2505  	if n == 0 {
  2506  		n = uint64(rand.Intn(100))
  2507  	}
  2508  	events := make([]*mm.MarketMakingEvent, 0, n)
  2509  	endTime := time.Now().Add(-time.Hour * time.Duration(rand.Intn(1000)))
  2510  	mktID := dex.BipIDSymbol(mkt.BaseID) + "_" + dex.BipIDSymbol(mkt.QuoteID)
  2511  	midGap, maxQty := getMarketStats(mktID)
  2512  	for i := uint64(0); i < n; i++ {
  2513  		ev := &mm.MarketMakingEvent{
  2514  			ID:        i,
  2515  			TimeStamp: endTime.Add(-time.Hour * time.Duration(i)).Unix(),
  2516  			BalanceEffects: &mm.BalanceEffects{
  2517  				Settled: map[uint32]int64{
  2518  					mkt.BaseID:  int64(maxQty * (-0.5 + rand.Float64())),
  2519  					mkt.QuoteID: int64(maxQty * (0.5 + rand.Float64())),
  2520  				},
  2521  				Pending: map[uint32]uint64{
  2522  					mkt.BaseID:  uint64(maxQty * (-0.5 + rand.Float64())),
  2523  					mkt.QuoteID: uint64(maxQty * (0.5 + rand.Float64())),
  2524  				},
  2525  				Locked: map[uint32]uint64{
  2526  					mkt.BaseID:  uint64(maxQty * (-0.5 + rand.Float64())),
  2527  					mkt.QuoteID: uint64(maxQty * (0.5 + rand.Float64())),
  2528  				},
  2529  				Reserved: map[uint32]uint64{},
  2530  			},
  2531  			Pending: i < 10 && rand.Float32() < 0.3,
  2532  			// DEXOrderEvent   *DEXOrderEvent   `json:"dexOrderEvent,omitempty"`
  2533  			// CEXOrderEvent   *CEXOrderEvent   `json:"cexOrderEvent,omitempty"`
  2534  			// DepositEvent    *DepositEvent    `json:"depositEvent,omitempty"`
  2535  			// WithdrawalEvent *WithdrawalEvent `json:"withdrawalEvent,omitempty"`
  2536  		}
  2537  		typeRoll := rand.Float32()
  2538  		switch {
  2539  		case typeRoll < 0.25: // dex order
  2540  			sell := rand.Intn(2) > 0
  2541  			ord := randomOrder(sell, maxQty, midGap, gapWidthFactor*midGap, false)
  2542  			orderEvent := &mm.DEXOrderEvent{
  2543  				ID:   ordertest.RandomOrderID().String(),
  2544  				Rate: ord.MsgRate,
  2545  				Qty:  ord.QtyAtomic,
  2546  				Sell: sell,
  2547  			}
  2548  			ev.DEXOrderEvent = orderEvent
  2549  			if rand.Float32() < 0.7 {
  2550  				orderEvent.Transactions = append(orderEvent.Transactions, randomWalletTransaction(asset.Swap, ord.QtyAtomic))
  2551  				if rand.Float32() < 0.9 {
  2552  					orderEvent.Transactions = append(orderEvent.Transactions, randomWalletTransaction(asset.Redeem, ord.QtyAtomic))
  2553  				} else {
  2554  					orderEvent.Transactions = append(orderEvent.Transactions, randomWalletTransaction(asset.Refund, ord.QtyAtomic))
  2555  				}
  2556  			}
  2557  		case typeRoll < 0.5: // cex order
  2558  			sell := rand.Intn(2) > 0
  2559  			ord := randomOrder(sell, maxQty, midGap, gapWidthFactor*midGap, false)
  2560  			ev.CEXOrderEvent = &mm.CEXOrderEvent{
  2561  				ID:   ordertest.RandomOrderID().String(),
  2562  				Rate: ord.MsgRate,
  2563  				Qty:  ord.QtyAtomic,
  2564  				Sell: sell,
  2565  			}
  2566  		case typeRoll < 0.75: // deposit
  2567  			assetID := mkt.BaseID
  2568  			if rand.Float32() < 0.5 {
  2569  				assetID = mkt.QuoteID
  2570  			}
  2571  			amt := uint64(maxQty * 0.2 * rand.Float64())
  2572  			ev.DepositEvent = &mm.DepositEvent{
  2573  				Transaction: randomWalletTransaction(asset.Send, amt),
  2574  				AssetID:     assetID,
  2575  				CEXCredit:   amt,
  2576  			}
  2577  		default: // withdrawal
  2578  			assetID := mkt.BaseID
  2579  			if rand.Float32() < 0.5 {
  2580  				assetID = mkt.QuoteID
  2581  			}
  2582  			amt := uint64(maxQty * 0.2 * rand.Float64())
  2583  			ev.WithdrawalEvent = &mm.WithdrawalEvent{
  2584  				Transaction: randomWalletTransaction(asset.Receive, amt),
  2585  				AssetID:     assetID,
  2586  				CEXDebit:    amt,
  2587  			}
  2588  		}
  2589  		events = append(events, ev)
  2590  	}
  2591  
  2592  	overview, err := m.RunOverview(startTime, mkt)
  2593  	if err != nil {
  2594  		return nil, nil, nil, err
  2595  	}
  2596  
  2597  	return events, nil, overview, nil
  2598  }
  2599  
  2600  func (m *TMarketMaker) CEXBook(host string, baseID, quoteID uint32) (buys, sells []*core.MiniOrder, _ error) {
  2601  	mktID := dex.BipIDSymbol(baseID) + "_" + dex.BipIDSymbol(quoteID)
  2602  	book := m.core.book(host, mktID)
  2603  	return book.Buys, book.Sells, nil
  2604  }
  2605  
  2606  func makeRequiredAction(assetID uint32, actionID string) *asset.ActionRequiredNote {
  2607  	txID := dex.Bytes(encode.RandomBytes(32)).String()
  2608  	var payload any
  2609  	if actionID == core.ActionIDRedeemRejected {
  2610  		payload = core.RejectedRedemptionData{
  2611  			AssetID: assetID,
  2612  			CoinID:  encode.RandomBytes(32),
  2613  			CoinFmt: "0x8909ec4aa707df569e62e2f8e2040094e2c88fe192b3b3e2dadfa383a41aa645",
  2614  		}
  2615  	} else {
  2616  		payload = &eth.TransactionActionNote{
  2617  			Tx:      randomWalletTransaction(asset.TransactionType(1+rand.Intn(15)), randomBalance()/10), // 1 to 15
  2618  			Nonce:   uint64(rand.Float64() * 500),
  2619  			NewFees: uint64(rand.Float64() * math.Pow10(rand.Intn(8))),
  2620  		}
  2621  	}
  2622  	n := &asset.ActionRequiredNote{
  2623  		ActionID: actionID,
  2624  		UniqueID: txID,
  2625  		Payload:  payload,
  2626  	}
  2627  	n.AssetID = assetID
  2628  	n.Route = "actionRequired"
  2629  	return n
  2630  }
  2631  
  2632  func makeActionResolved(assetID uint32, uniqueID string) *asset.ActionResolvedNote {
  2633  	n := &asset.ActionResolvedNote{
  2634  		UniqueID: uniqueID,
  2635  	}
  2636  	n.AssetID = assetID
  2637  	n.Route = "actionResolved"
  2638  	return n
  2639  }
  2640  
  2641  func TestServer(t *testing.T) {
  2642  	// Register dummy drivers for unimplemented assets.
  2643  	asset.Register(22, &TDriver{})                 // mona
  2644  	asset.Register(28, &TDriver{})                 // vtc
  2645  	asset.Register(unsupportedAssetID, &TDriver{}) // kmd
  2646  	asset.Register(3, &TDriver{})                  // doge
  2647  
  2648  	tinfos = map[uint32]*asset.Token{
  2649  		60001:  asset.TokenInfo(60001),
  2650  		966001: asset.TokenInfo(966001),
  2651  	}
  2652  
  2653  	numBuys = 10
  2654  	numSells = 10
  2655  	feedPeriod = 5000 * time.Millisecond
  2656  	initialize := false
  2657  	register := false
  2658  	forceDisconnectWallet = true
  2659  	gapWidthFactor = 0.2
  2660  	randomPokes = false
  2661  	randomNotes = false
  2662  	numUserOrders = 40
  2663  	delayBalance = true
  2664  	doubleCreateAsyncErr = false
  2665  	randomizeOrdersCount = true
  2666  
  2667  	if enableActions {
  2668  		actions = []*asset.ActionRequiredNote{
  2669  			makeRequiredAction(0, "missingNonces"),
  2670  			makeRequiredAction(42, "lostNonce"),
  2671  			makeRequiredAction(60, "tooCheap"),
  2672  			makeRequiredAction(60, "redeemRejected"),
  2673  		}
  2674  	}
  2675  
  2676  	var shutdown context.CancelFunc
  2677  	tCtx, shutdown = context.WithCancel(context.Background())
  2678  	time.AfterFunc(time.Minute*59, func() { shutdown() })
  2679  	logger := dex.StdOutLogger("TEST", dex.LevelTrace)
  2680  	tCore := newTCore()
  2681  
  2682  	if initialize {
  2683  		tCore.InitializeClient([]byte(""), nil)
  2684  	}
  2685  
  2686  	if register {
  2687  		// initialize is implied and forced if register = true.
  2688  		if !initialize {
  2689  			tCore.InitializeClient([]byte(""), nil)
  2690  		}
  2691  		var assetID uint32 = 42
  2692  		tCore.PostBond(&core.PostBondForm{Addr: firstDEX, Bond: 1, Asset: &assetID})
  2693  	}
  2694  
  2695  	s, err := New(&Config{
  2696  		Core: tCore,
  2697  		MarketMaker: &TMarketMaker{
  2698  			core:        tCore,
  2699  			cfg:         &mm.MarketMakingConfig{},
  2700  			runningBots: make(map[mm.MarketWithHost]int64),
  2701  		},
  2702  		Addr:     "127.0.0.3:54321",
  2703  		Logger:   logger,
  2704  		NoEmbed:  true, // use files on disk, and reload on each page load
  2705  		HttpProf: true,
  2706  	})
  2707  	if err != nil {
  2708  		t.Fatalf("error creating server: %v", err)
  2709  	}
  2710  	cm := dex.NewConnectionMaster(s)
  2711  	err = cm.Connect(tCtx)
  2712  	if err != nil {
  2713  		t.Fatalf("Connect error: %v", err)
  2714  	}
  2715  	go tCore.runEpochs()
  2716  	if randomPokes {
  2717  		go tCore.runRandomPokes()
  2718  	}
  2719  
  2720  	if randomNotes {
  2721  		go tCore.runRandomNotes()
  2722  	}
  2723  
  2724  	cm.Wait()
  2725  }