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: ð.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 <- ¬e 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 <- ¬e 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 = ð.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 }