decred.org/dcrdex@v1.0.5/client/core/simnet_trade.go (about)

     1  //go:build harness && !nolgpl
     2  
     3  package core
     4  
     5  // The asset and dcrdex harnesses should be running before executing this test.
     6  //
     7  // The ./run script rebuilds the dcrdex binary with dex.testLockTimeTaker=1m
     8  // and dex.testLockTimeMaker=2m before running the binary, making it possible
     9  // for this test to wait for swap locktimes to expire and ensure that refundable
    10  // swaps are actually refunded when the swap locktimes expire.
    11  //
    12  // Some errors you might encounter (especially after running this test
    13  // multiple times):
    14  // - error placing order rpc error: 36: coin locked
    15  //   likely that the DEX has not revoked a previously failed match that locked
    16  //   the coin that was about to be reused, waiting a couple seconds before retrying
    17  //   should eliminate the error. Otherwise, clear the dcrdex db and restart the
    18  //   dcrdex harness
    19  // - error placing order not enough to cover requested funds
    20  //   use the affected asset harness to send funds to the affected wallet
    21  // - occasional issue with fee payment confirmation
    22  //   restart dcr-harness and dcrdex-harness. stop dcrdex before dcr harness
    23  
    24  import (
    25  	"context"
    26  	"errors"
    27  	"fmt"
    28  	"os"
    29  	"os/exec"
    30  	"path/filepath"
    31  	"strconv"
    32  	"strings"
    33  	"sync"
    34  	"sync/atomic"
    35  	"time"
    36  
    37  	"decred.org/dcrdex/client/asset"
    38  	"decred.org/dcrdex/client/asset/bch"
    39  	"decred.org/dcrdex/client/asset/btc"
    40  	"decred.org/dcrdex/client/asset/dash"
    41  	"decred.org/dcrdex/client/asset/dcr"
    42  	"decred.org/dcrdex/client/asset/dgb"
    43  	"decred.org/dcrdex/client/asset/doge"
    44  	"decred.org/dcrdex/client/asset/eth"
    45  	"decred.org/dcrdex/client/asset/firo"
    46  	"decred.org/dcrdex/client/asset/ltc"
    47  	"decred.org/dcrdex/client/asset/polygon"
    48  	"decred.org/dcrdex/client/asset/zcl"
    49  	"decred.org/dcrdex/client/asset/zec"
    50  	"decred.org/dcrdex/client/comms"
    51  	"decred.org/dcrdex/dex"
    52  	"decred.org/dcrdex/dex/calc"
    53  	"decred.org/dcrdex/dex/config"
    54  	"decred.org/dcrdex/dex/msgjson"
    55  	dexbtc "decred.org/dcrdex/dex/networks/btc"
    56  	dexdgb "decred.org/dcrdex/dex/networks/dgb"
    57  	dexdoge "decred.org/dcrdex/dex/networks/doge"
    58  	dexeth "decred.org/dcrdex/dex/networks/eth"
    59  	"decred.org/dcrdex/dex/order"
    60  	"github.com/btcsuite/btcd/chaincfg/chainhash"
    61  	"github.com/btcsuite/btcd/wire"
    62  	"golang.org/x/sync/errgroup"
    63  )
    64  
    65  var (
    66  	dexHost = "127.0.0.1:17273"
    67  
    68  	homeDir             = os.Getenv("HOME")
    69  	dextestDir          = filepath.Join(homeDir, "dextest")
    70  	ethAlphaIPCFile     = filepath.Join(dextestDir, "eth", "alpha", "node", "geth.ipc")
    71  	polygonAlphaIPCFile = filepath.Join(dextestDir, "polygon", "alpha", "bor", "bor.ipc")
    72  
    73  	ethUsdcID, _     = dex.BipSymbolID("usdc.eth")
    74  	polygonUsdcID, _ = dex.BipSymbolID("usdc.polygon")
    75  )
    76  
    77  type SimWalletType int
    78  
    79  const (
    80  	WTCoreClone SimWalletType = iota + 1
    81  	WTSPVNative
    82  	WTElectrum
    83  )
    84  
    85  // SimClient is the configuration for the client's wallets.
    86  type SimClient struct {
    87  	BaseWalletType  SimWalletType
    88  	QuoteWalletType SimWalletType
    89  	BaseNode        string
    90  	QuoteNode       string
    91  }
    92  
    93  type assetConfig struct {
    94  	id               uint32
    95  	symbol           string
    96  	conversionFactor uint64
    97  	valFmt           func(any) string
    98  	isToken          bool
    99  }
   100  
   101  var testLookup = map[string]func(s *simulationTest) error{
   102  	"success":       testTradeSuccess,
   103  	"nomakerswap":   testNoMakerSwap,
   104  	"notakerswap":   testNoTakerSwap,
   105  	"nomakerredeem": testNoMakerRedeem,
   106  	"makerghost":    testMakerGhostingAfterTakerRedeem,
   107  	"orderstatus":   testOrderStatusReconciliation,
   108  	"resendpending": testResendPendingRequests,
   109  }
   110  
   111  func SimTests() []string {
   112  	// Use this slice instead of the generating from the testLookup map so that
   113  	// the order doesn't change.
   114  	return []string{
   115  		"success",
   116  		"nomakerswap",
   117  		"notakerswap",
   118  		"nomakerredeem",
   119  		"makerghost",
   120  		"orderstatus",
   121  		"resendpending",
   122  	}
   123  }
   124  
   125  // SimulationConfig is the test configuration.
   126  type SimulationConfig struct {
   127  	BaseSymbol        string
   128  	QuoteSymbol       string
   129  	RegistrationAsset string
   130  	Client1           *SimClient
   131  	Client2           *SimClient
   132  	Tests             []string
   133  	Logger            dex.Logger
   134  	RunOnce           bool
   135  }
   136  
   137  type simulationTest struct {
   138  	ctx            context.Context
   139  	cancel         context.CancelFunc
   140  	log            dex.Logger
   141  	base           *assetConfig
   142  	quote          *assetConfig
   143  	regAsset       uint32
   144  	marketName     string
   145  	lotSize        uint64
   146  	rateStep       uint64
   147  	client1        *simulationClient
   148  	client2        *simulationClient
   149  	clients        []*simulationClient
   150  	client1IsMaker bool
   151  }
   152  
   153  func (s *simulationTest) waitALittleBit() {
   154  	sleep := 4 * time.Second
   155  	if s.client1.BaseWalletType == WTElectrum || s.client1.QuoteWalletType == WTElectrum ||
   156  		s.client2.BaseWalletType == WTElectrum || s.client2.QuoteWalletType == WTElectrum {
   157  		sleep = 7 * time.Second
   158  	}
   159  	s.log.Infof("Waiting a little bit, %s.", sleep)
   160  	time.Sleep(sleep * sleepFactor)
   161  }
   162  
   163  // RunSimulationTest runs one or more simulations tests, based on the provided
   164  // SimulationConfig.
   165  func RunSimulationTest(cfg *SimulationConfig) error {
   166  	if cfg.Client1.BaseWalletType == WTCoreClone && cfg.Client2.BaseWalletType == WTCoreClone &&
   167  		cfg.Client1.BaseNode == cfg.Client2.BaseNode {
   168  		return fmt.Errorf("the %s RPC wallets for both clients are the same", cfg.BaseSymbol)
   169  	}
   170  
   171  	if cfg.Client1.QuoteWalletType == WTCoreClone && cfg.Client2.QuoteWalletType == WTCoreClone &&
   172  		cfg.Client1.QuoteNode == cfg.Client2.QuoteNode {
   173  		return fmt.Errorf("the %s RPC wallets for both clients are the same", cfg.QuoteSymbol)
   174  	}
   175  
   176  	// No alpha wallets allowed until we smarten up the balance checks, I guess.
   177  	if cfg.Client1.BaseNode == "alpha" || cfg.Client1.QuoteNode == "alpha" ||
   178  		cfg.Client2.BaseNode == "alpha" || cfg.Client2.QuoteNode == "alpha" {
   179  		return fmt.Errorf("no alpha nodes allowed")
   180  	}
   181  
   182  	baseID, ok := dex.BipSymbolID(cfg.BaseSymbol)
   183  	if !ok {
   184  		return fmt.Errorf("base asset %q not known", cfg.BaseSymbol)
   185  	}
   186  	baseUnitInfo, err := asset.UnitInfo(baseID)
   187  	if err != nil {
   188  		return fmt.Errorf("no unit info for %q", cfg.BaseSymbol)
   189  	}
   190  	quoteID, ok := dex.BipSymbolID(cfg.QuoteSymbol)
   191  	if !ok {
   192  		return fmt.Errorf("base asset %q not known", cfg.BaseSymbol)
   193  	}
   194  	quoteUnitInfo, err := asset.UnitInfo(quoteID)
   195  	if err != nil {
   196  		return fmt.Errorf("no unit info for %q", cfg.QuoteSymbol)
   197  	}
   198  	regAsset := baseID
   199  	if cfg.RegistrationAsset == cfg.QuoteSymbol {
   200  		regAsset = quoteID
   201  	}
   202  	valFormatter := func(valFmt func(uint64) string) func(any) string {
   203  		return func(vi any) string {
   204  			var vu uint64
   205  			var negative bool
   206  			switch vt := vi.(type) {
   207  			case uint64:
   208  				vu = vt
   209  			case int64:
   210  				if negative = vt < 0; negative {
   211  					vu = uint64(-vt)
   212  				} else {
   213  					vu = uint64(vt)
   214  				}
   215  			}
   216  			if negative {
   217  				return "-" + valFmt(vu)
   218  			}
   219  			return valFmt(vu)
   220  		}
   221  	}
   222  
   223  	s := &simulationTest{
   224  		log: cfg.Logger,
   225  		base: &assetConfig{
   226  			id:               baseID,
   227  			symbol:           cfg.BaseSymbol,
   228  			conversionFactor: baseUnitInfo.Conventional.ConversionFactor,
   229  			valFmt:           valFormatter(baseUnitInfo.ConventionalString),
   230  			isToken:          asset.TokenInfo(baseID) != nil,
   231  		},
   232  		quote: &assetConfig{
   233  			id:               quoteID,
   234  			symbol:           cfg.QuoteSymbol,
   235  			conversionFactor: quoteUnitInfo.Conventional.ConversionFactor,
   236  			valFmt:           valFormatter(quoteUnitInfo.ConventionalString),
   237  			isToken:          asset.TokenInfo(quoteID) != nil,
   238  		},
   239  		regAsset:   regAsset,
   240  		marketName: marketName(baseID, quoteID),
   241  	}
   242  
   243  	if err := s.setup(cfg.Client1, cfg.Client2); err != nil {
   244  		return fmt.Errorf("setup error: %w", err)
   245  	}
   246  	spacer := `
   247  
   248  *******************************************************************************
   249  `
   250  	for _, testName := range cfg.Tests {
   251  		s.log.Infof("%s\nRunning test %q with client 1 as maker on market %q.%s", spacer, testName, s.marketName, spacer)
   252  		f, ok := testLookup[testName]
   253  		if !ok {
   254  			return fmt.Errorf("no test named %q", testName)
   255  		}
   256  		s.client1IsMaker = true
   257  		if err := f(s); err != nil {
   258  			return fmt.Errorf("test %q failed with client 1 as maker: %w", testName, err)
   259  		}
   260  		s.log.Infof("%s\nSUCCESS!! Test %q with client 1 as maker on market %q PASSED!%s", spacer, testName, s.marketName, spacer)
   261  		s.client1IsMaker = false
   262  		if !cfg.RunOnce && testName != "orderstatus" {
   263  			s.log.Infof("%s\nRunning test %q with client 2 as maker on market %q.%s", spacer, testName, s.marketName, spacer)
   264  			if err := f(s); err != nil {
   265  				return fmt.Errorf("test %q failed with client 2 as maker: %w", testName, err)
   266  			}
   267  			s.log.Infof("%s\nSUCCESS!! Test %q with client 2 as maker on market %q PASSED!%s", spacer, testName, s.marketName, spacer)
   268  		}
   269  	}
   270  
   271  	s.cancel()
   272  	for _, c := range s.clients {
   273  		c.wg.Wait()
   274  		c.log.Infof("Client %s done.", c.name)
   275  	}
   276  
   277  	return nil
   278  }
   279  
   280  func (s *simulationTest) startClients() error {
   281  	for _, c := range s.clients {
   282  		err := c.init(s.ctx)
   283  		if err != nil {
   284  			return err
   285  		}
   286  		walletNames := [2]string{}
   287  		var i int
   288  		for bipID := range c.wallets {
   289  			walletNames[i] = unbip(bipID)
   290  			i++
   291  		}
   292  		c.log.Infof("Setting up client %s with wallets %s and %s.", c.name, walletNames[0], walletNames[1])
   293  
   294  		c.wg.Add(1)
   295  		go func() {
   296  			defer c.wg.Done()
   297  			c.core.Run(s.ctx)
   298  		}()
   299  		<-c.core.Ready()
   300  
   301  		// init app
   302  		_, err = c.core.InitializeClient(c.appPass, nil)
   303  		if err != nil {
   304  			return err
   305  		}
   306  		c.log.Infof("Core initialized.")
   307  
   308  		c.core.Login(c.appPass)
   309  
   310  		createWallet := func(pass []byte, fund bool, form *WalletForm) error {
   311  			err = c.core.CreateWallet(c.appPass, pass, form)
   312  			if err != nil {
   313  				return err
   314  			}
   315  			c.log.Infof("Connected %s wallet (fund = %v).", unbip(form.AssetID), fund)
   316  			hctrl := newHarnessCtrl(form.AssetID)
   317  			// eth needs the headers to be new in order to
   318  			// count itself synced, so mining a few blocks here.
   319  			hctrl.mineBlocks(s.ctx, 1)
   320  			c.log.Infof("Waiting for %s wallet to sync.", unbip(form.AssetID))
   321  			synced := make(chan error)
   322  			go func() {
   323  				tStart := time.Now()
   324  				for time.Since(tStart) < time.Second*30 {
   325  					if c.core.WalletState(form.AssetID).Synced {
   326  						synced <- nil
   327  						return
   328  					}
   329  					time.Sleep(time.Second)
   330  				}
   331  				synced <- fmt.Errorf("wallet never synced")
   332  			}()
   333  			if err := <-synced; err != nil {
   334  				return fmt.Errorf("wallet never synced")
   335  			}
   336  			c.log.Infof("Client %s %s wallet.", c.name, unbip(form.AssetID))
   337  			if fund {
   338  				// Fund new wallet.
   339  				c.log.Infof("Client %s funding synced %s wallet.", c.name, unbip(form.AssetID))
   340  				address := c.core.WalletState(form.AssetID).Address
   341  				amts := []int{10, 18, 5, 7, 1, 15, 3, 25}
   342  				if err := hctrl.fund(s.ctx, address, amts); err != nil {
   343  					return fmt.Errorf("fund error: %w", err)
   344  				}
   345  			mined:
   346  				for {
   347  					hctrl.mineBlocks(s.ctx, 2)
   348  					bal, err := c.core.AssetBalance(form.AssetID)
   349  					if err != nil {
   350  						return fmt.Errorf("error getting balance for %s: %w", unbip(form.AssetID), err)
   351  					}
   352  					if bal.Available > 0 {
   353  						break mined
   354  					}
   355  					s.log.Infof("Waiting for %s funding tx to be mined", unbip(form.AssetID))
   356  					select {
   357  					case <-time.After(time.Second * 2):
   358  					case <-s.ctx.Done():
   359  						return s.ctx.Err()
   360  					}
   361  				}
   362  
   363  				// Tip change after block filtering scan takes the wallet time.
   364  				time.Sleep(2 * time.Second * sleepFactor)
   365  
   366  				w, _ := c.core.wallet(form.AssetID)
   367  				approved := make(chan struct{})
   368  				if approver, is := w.Wallet.(asset.TokenApprover); is {
   369  					const contractVer = 0
   370  					if _, err := approver.ApproveToken(contractVer, func() {
   371  						close(approved)
   372  					}); err != nil {
   373  						return fmt.Errorf("error approving %s token: %w", unbip(form.AssetID), err)
   374  					}
   375  				out:
   376  					for {
   377  						s.log.Infof("Mining more blocks to get approval tx confirmed")
   378  						hctrl.mineBlocks(s.ctx, 2)
   379  						select {
   380  						case <-approved:
   381  							c.log.Infof("%s approved", unbip(form.AssetID))
   382  							break out
   383  						case <-time.After(time.Second * 5):
   384  						case <-s.ctx.Done():
   385  							return s.ctx.Err()
   386  						}
   387  					}
   388  				}
   389  			}
   390  
   391  			return nil
   392  		}
   393  
   394  		// connect wallets
   395  		for assetID, wallet := range c.wallets {
   396  			os.RemoveAll(c.core.assetDataDirectory(assetID))
   397  
   398  			c.log.Infof("Creating %s %s-type wallet. config = %+v.", dex.BipIDSymbol(assetID), wallet.walletType, wallet.config)
   399  
   400  			pw := wallet.pass
   401  
   402  			if wallet.parent != nil {
   403  				if err := createWallet(pw, wallet.fund, wallet.parent); err != nil {
   404  					return fmt.Errorf("error creating parent %s wallet: %w", dex.BipIDSymbol(wallet.parent.AssetID), err)
   405  				}
   406  				// For degen assets, the pass is only for the parent.
   407  				pw = nil
   408  			}
   409  
   410  			if err := createWallet(pw, wallet.fund, &WalletForm{
   411  				AssetID: assetID,
   412  				Config:  wallet.config,
   413  				Type:    wallet.walletType,
   414  			}); err != nil {
   415  				return err
   416  			}
   417  
   418  		}
   419  
   420  		// Tip change after block filtering scan takes the wallet time, even
   421  		// longer for Electrum.
   422  		sleep := 2 * time.Second
   423  		if c.BaseWalletType == WTElectrum || c.QuoteWalletType == WTElectrum {
   424  			sleep = 6 * time.Second
   425  		}
   426  		time.Sleep(sleep * sleepFactor)
   427  
   428  		err = s.registerDEX(c)
   429  		if err != nil {
   430  			return err
   431  		}
   432  	}
   433  
   434  	dc, _, err := s.client1.core.dex(dexHost)
   435  	if err != nil {
   436  		return err
   437  	}
   438  
   439  	mktCfg := dc.marketConfig(s.marketName)
   440  
   441  	if mktCfg == nil {
   442  		return fmt.Errorf("market %s not found", s.marketName)
   443  	}
   444  
   445  	s.lotSize = mktCfg.LotSize
   446  	s.rateStep = mktCfg.RateStep
   447  
   448  	return nil
   449  }
   450  
   451  func (s *simulationTest) setup(cl1, cl2 *SimClient) (err error) {
   452  	s.client1, err = s.newClient("1", cl1)
   453  	if err != nil {
   454  		return err
   455  	}
   456  	s.client2, err = s.newClient("2", cl2)
   457  	if err != nil {
   458  		return err
   459  	}
   460  	s.clients = []*simulationClient{s.client1, s.client2}
   461  	s.ctx, s.cancel = context.WithCancel(context.Background())
   462  	err = s.startClients()
   463  	if err != nil {
   464  		return fmt.Errorf("error starting clients: %w", err)
   465  	}
   466  	s.client1.replaceConns()
   467  	s.client2.replaceConns()
   468  	return nil
   469  }
   470  
   471  var sleepFactor time.Duration = 1
   472  
   473  // TestTradeSuccess runs a simple trade test and ensures that the resulting
   474  // trades are completed successfully.
   475  func testTradeSuccess(s *simulationTest) error {
   476  	var qty, rate uint64 = 2 * s.lotSize, 150 * s.rateStep
   477  	s.client1.isSeller, s.client2.isSeller = true, false
   478  	return s.simpleTradeTest(qty, rate, order.MatchConfirmed)
   479  }
   480  
   481  // TestNoMakerSwap runs a simple trade test and ensures that the resulting
   482  // trades fail because of the Maker not sending their init swap tx.
   483  func testNoMakerSwap(s *simulationTest) error {
   484  	var qty, rate uint64 = 1 * s.lotSize, 100 * s.rateStep
   485  	s.client1.isSeller, s.client2.isSeller = false, true
   486  	return s.simpleTradeTest(qty, rate, order.NewlyMatched)
   487  }
   488  
   489  // TestNoTakerSwap runs a simple trade test and ensures that the resulting
   490  // trades fail because of the Taker not sending their init swap tx.
   491  // Also ensures that Maker's funds are refunded after locktime expires.
   492  func testNoTakerSwap(s *simulationTest) error {
   493  	var qty, rate uint64 = 3 * s.lotSize, 200 * s.rateStep
   494  	s.client1.isSeller, s.client2.isSeller = true, false
   495  	return s.simpleTradeTest(qty, rate, order.MakerSwapCast)
   496  }
   497  
   498  // TestNoMakerRedeem runs a simple trade test and ensures that the resulting
   499  // trades fail because of Maker not redeeming Taker's swap.
   500  // Also ensures that both Maker and Taker's funds are refunded after their
   501  // respective swap locktime expires.
   502  // A scenario where Maker actually redeemed Taker's swap but did not notify
   503  // Taker is handled in TestMakerGhostingAfterTakerRedeem which ensures that
   504  // Taker auto-finds Maker's redeem and completes the trade by redeeming Maker's
   505  // swap.
   506  func testNoMakerRedeem(s *simulationTest) error {
   507  	var qty, rate uint64 = 1 * s.lotSize, 250 * s.rateStep
   508  	s.client1.isSeller, s.client2.isSeller = true, false
   509  
   510  	enable := func(client *simulationClient) {
   511  		client.enableWallets()
   512  		s.client1.filteredConn.requestFilter.Store(func(string) error { return nil })
   513  		s.client2.filteredConn.requestFilter.Store(func(string) error { return nil })
   514  	}
   515  	var killed uint32
   516  	preFilter1 := func(route string) error {
   517  		if route == msgjson.InitRoute && atomic.CompareAndSwapUint32(&killed, 0, 1) {
   518  			s.client1.disableWallets()
   519  			time.AfterFunc(s.client1.core.lockTimeTaker, func() { enable(s.client1) })
   520  		}
   521  		return nil
   522  	}
   523  	preFilter2 := func(route string) error {
   524  		if route == msgjson.InitRoute && atomic.CompareAndSwapUint32(&killed, 0, 1) {
   525  			s.client2.disableWallets()
   526  			time.AfterFunc(s.client1.core.lockTimeTaker, func() { enable(s.client2) })
   527  		}
   528  		return nil
   529  	}
   530  
   531  	s.client1.filteredConn.requestFilter.Store(preFilter1)
   532  	s.client2.filteredConn.requestFilter.Store(preFilter2)
   533  
   534  	return s.simpleTradeTest(qty, rate, order.TakerSwapCast)
   535  }
   536  
   537  // TestMakerGhostingAfterTakerRedeem places simple orders for clients 1 and 2,
   538  // negotiates the resulting trades smoothly till TakerSwapCast, then Maker goes
   539  // AWOL after redeeming taker's swap without notifying Taker. This test ensures
   540  // that Taker auto-finds Maker's redeem, extracts the secret key and redeems
   541  // Maker's swap to complete the trade.
   542  // A scenario where Maker actually did NOT redeem Taker's swap is handled in
   543  // testNoMakerRedeem which ensures that both parties are able to refund their
   544  // swaps.
   545  // TODO: What happens if FindRedemption encounters a refund instead of a redeem?
   546  func testMakerGhostingAfterTakerRedeem(s *simulationTest) error {
   547  	var qty, rate uint64 = 1 * s.lotSize, 250 * s.rateStep
   548  	s.client1.isSeller, s.client2.isSeller = true, false
   549  
   550  	var bits uint8
   551  	anErr := errors.New("intentional error from test")
   552  	// Prevent the first redeemer, who must be maker, from sending
   553  	// redeem info to the server.
   554  	preFilter1 := func(route string) error {
   555  		if route == msgjson.RedeemRoute {
   556  			if bits == 0 {
   557  				bits = 0b1
   558  			}
   559  			if bits&0b1 != 0 {
   560  				return anErr
   561  			}
   562  		}
   563  		return nil
   564  	}
   565  	preFilter2 := func(route string) error {
   566  		if route == msgjson.RedeemRoute {
   567  			if bits == 0 {
   568  				bits = 0b10
   569  			}
   570  			if bits&0b10 != 0 {
   571  				return anErr
   572  			}
   573  		}
   574  		return nil
   575  	}
   576  
   577  	s.client1.filteredConn.requestFilter.Store(preFilter1)
   578  	s.client2.filteredConn.requestFilter.Store(preFilter2)
   579  	defer s.client1.filteredConn.requestFilter.Store(func(string) error { return nil })
   580  	defer s.client2.filteredConn.requestFilter.Store(func(string) error { return nil })
   581  
   582  	return s.simpleTradeTest(qty, rate, order.MatchConfirmed)
   583  }
   584  
   585  // TestOrderStatusReconciliation simulates a few conditions that could cause a
   586  // client to record a wrong status for an order, especially where the client
   587  // considers an order as active when it no longer is. The expectation is that
   588  // the client can infer the correct order status for such orders and update
   589  // accordingly. The following scenarios are simulated and tested:
   590  //
   591  // Order 1:
   592  //   - Standing order, preimage not revealed, "missed" revoke_order note.
   593  //   - Expect order status to stay at Epoch status before going AWOL and to
   594  //     become Revoked after re-connecting the DEX. Locked coins should be
   595  //     returned.
   596  //
   597  // Order 2:
   598  //   - Non-standing order, preimage revealed, "missed" nomatch or match request
   599  //     (if matched).
   600  //   - Expect order status to stay at Epoch status before going AWOL and to
   601  //     become Executed after re-connecting the DEX, even if the order was
   602  //     matched and the matches got revoked due to client inaction.
   603  //
   604  // Order 3:
   605  //   - Standing order, partially matched, booked, revoked due to inaction on a
   606  //     match.
   607  //   - Expect order status to be Booked before going AWOL and to become Revoked
   608  //     after re-connecting the DEX. Locked coins should be returned.
   609  func testOrderStatusReconciliation(s *simulationTest) error {
   610  	for _, client := range s.clients {
   611  		if err := s.updateBalances(client); err != nil {
   612  			return err
   613  		}
   614  		client.expectBalanceDiffs = nil // not interested in balance checks for this test case
   615  	}
   616  
   617  	waiter, ctx := errgroup.WithContext(context.Background())
   618  
   619  	s.client1.isSeller, s.client2.isSeller = false, true
   620  
   621  	// Record client 2's locked balance before placing trades
   622  	// to determine the amount locked for the placed trades.
   623  	c2Balance, err := s.client2.core.AssetBalance(s.base.id) // client 2 is seller
   624  	if err != nil {
   625  		return fmt.Errorf("client 2 pre-trade balance error %w", err)
   626  	}
   627  	preTradeLockedBalance := c2Balance.Locked
   628  	s.log.Infof("Client 2 %s available balance is %v.", s.base.symbol, s.base.valFmt(c2Balance.Available))
   629  
   630  	rate := 100 * s.rateStep
   631  
   632  	s.log.Infof("%s\n", `
   633  Placing an order for client 1, qty=3*lotSize, rate=100*rateStep
   634  This order should get matched to either or both of these client 2
   635  sell orders:
   636  - Order 2: immediate limit order, qty=2*lotSize, rate=100*rateStep,
   637             may not get matched if Order 3 below is matched first.
   638  - Order 3: standing limit order, qty=4*lotSize, rate=100*rateStep,
   639             will always be partially matched (3*lotSize matched or
   640             1*lotSize matched, if Order 2 is matched first).`)
   641  	waiter.Go(func() error {
   642  		_, err := s.placeOrder(s.client1, 3*s.lotSize, rate, false)
   643  		if err != nil {
   644  			return fmt.Errorf("client 1 place order error: %v", err)
   645  		}
   646  		return nil
   647  	})
   648  
   649  	// forgetClient2Order deletes the passed order id from client 2's
   650  	// dc.trade map, ensuring that all requests and notifications for
   651  	// the order are not processed.
   652  	c2dc := s.client2.dc()
   653  	c2ForgottenOrders := make(map[order.OrderID]*trackedTrade)
   654  	forgetClient2Order := func(oid order.OrderID) {
   655  		s.log.Infof("Forcing client 2 to forget order %s.", oid)
   656  		c2dc.tradeMtx.Lock()
   657  		defer c2dc.tradeMtx.Unlock()
   658  		tracker, found := c2dc.trades[oid]
   659  		if !found {
   660  			return
   661  		}
   662  		delete(c2dc.trades, oid)
   663  		c2ForgottenOrders[oid] = tracker
   664  	}
   665  
   666  	// Expected order statuses before and after client 2 goes AWOL.
   667  	c2OrdersBefore := make(map[order.OrderID]order.OrderStatus)
   668  	c2OrdersAfter := make(map[order.OrderID]order.OrderStatus)
   669  	var statusMtx sync.Mutex
   670  	recordBeforeAfterStatuses := func(oid order.OrderID, beforeStatus, afterStatus order.OrderStatus) {
   671  		statusMtx.Lock()
   672  		defer statusMtx.Unlock()
   673  		c2OrdersBefore[oid] = beforeStatus
   674  		c2OrdersAfter[oid] = afterStatus
   675  	}
   676  
   677  	s.log.Infof("%s\n", `
   678  Placing Client 2 Order 1:
   679   - Standing order, preimage not revealed, "missed" revoke_order note.
   680   - Expect order status to stay at Epoch status before going AWOL and
   681     to become Revoked after re-connecting the DEX. Locked coins should
   682     be returned.`)
   683  
   684  	waiter.Go(func() error {
   685  		// standing limit order, qty and rate doesn't matter, preimage
   686  		// miss prevents this order from getting matched.
   687  		orderID, err := s.placeOrder(s.client2, 1*s.lotSize, rate, false)
   688  		if err != nil {
   689  			return fmt.Errorf("client 2 place order error: %v", err)
   690  		}
   691  		oid, err := order.IDFromHex(orderID)
   692  		if err != nil {
   693  			return fmt.Errorf("client 2 place order error: %v", err)
   694  		}
   695  		// Foil preimage reveal by "forgetting" this order.
   696  		// Also prevents processing revoke_order notes for this order.
   697  		forgetClient2Order(oid)
   698  		recordBeforeAfterStatuses(oid, order.OrderStatusEpoch, order.OrderStatusRevoked)
   699  		return nil
   700  	})
   701  
   702  	s.log.Infof("%s\n", `
   703  Placing Client 2 Order 2:
   704  - Non-standing order, preimage revealed, "missed" nomatch or match
   705    request (if matched).
   706  - Expect order status to stay at Epoch status before going AWOL and
   707    to become Executed after re-connecting the DEX, even if the order
   708    was matched and the matches got revoked due to client inaction. No
   709    attempt is made to cause match revocation anyways.`)
   710  
   711  	waiter.Go(func() error {
   712  		notes := s.client2.startNotificationReader(ctx)
   713  		// immediate limit order, use qty=2*lotSize, rate=300*rateStep to be
   714  		// potentially matched by client 1's order above.
   715  		orderID, err := s.placeOrder(s.client2, 2*s.lotSize, rate*3, true)
   716  		if err != nil {
   717  			return fmt.Errorf("client 2 place order error: %v", err)
   718  		}
   719  		tracker, err := s.client2.findOrder(orderID)
   720  		if err != nil {
   721  			return fmt.Errorf("client 2 place order error: %v", err)
   722  		}
   723  		oid := tracker.ID()
   724  		// Wait a max of 2 epochs for preimage to be sent for this order.
   725  		twoEpochs := 2 * time.Duration(tracker.epochLen()) * time.Millisecond
   726  		s.client2.log.Infof("Client 2 waiting %v for preimage reveal, order %s", twoEpochs, tracker.token())
   727  		preimageRevealed := notes.find(ctx, twoEpochs, func(n Notification) bool {
   728  			orderNote, isOrderNote := n.(*OrderNote)
   729  			if isOrderNote && n.Topic() == TopicPreimageSent && orderNote.Order.ID.String() == orderID {
   730  				forgetClient2Order(oid)
   731  				return true
   732  			}
   733  			return false
   734  		})
   735  		if !preimageRevealed {
   736  			return fmt.Errorf("preimage not revealed for order %s after %s", tracker.token(), twoEpochs)
   737  		}
   738  		recordBeforeAfterStatuses(oid, order.OrderStatusEpoch, order.OrderStatusExecuted)
   739  		return nil
   740  	})
   741  
   742  	s.log.Infof("%s\n", `
   743  Client 2 placing Order 3:
   744   - Standing order, partially matched, booked, revoked due to inaction on
   745     a match.
   746   - Expect order status to be Booked before going AWOL and to become
   747     Revoked after re-connecting the DEX. Locked coins should be returned.`)
   748  
   749  	waiter.Go(func() error {
   750  		notes := s.client2.startNotificationReader(ctx)
   751  		// standing limit order, use qty=4*lotSize, rate=100*rateStep to be
   752  		// partially matched by client 1's order above.
   753  		orderID, err := s.placeOrder(s.client2, 4*s.lotSize, rate, false)
   754  		if err != nil {
   755  			return fmt.Errorf("client 2 place order error: %v", err)
   756  		}
   757  		tracker, err := s.client2.findOrder(orderID)
   758  		if err != nil {
   759  			return fmt.Errorf("client 2 place order error: %v", err)
   760  		}
   761  		// Wait a max of 2 epochs for preimage to be sent for this order.
   762  		twoEpochs := 2 * time.Duration(tracker.epochLen()) * time.Millisecond
   763  		s.client2.log.Infof("Client 2 waiting %v for preimage reveal, order %s", twoEpochs, tracker.token())
   764  		preimageRevealed := notes.find(ctx, twoEpochs, func(n Notification) bool {
   765  			orderNote, isOrderNote := n.(*OrderNote)
   766  			return isOrderNote && n.Topic() == TopicPreimageSent && orderNote.Order.ID.String() == orderID
   767  		})
   768  		if !preimageRevealed {
   769  			return fmt.Errorf("preimage not revealed for order %s after %s", tracker.token(), twoEpochs)
   770  		}
   771  		// Preimage sent, matches will be made soon. Lock wallets to prevent
   772  		// client from sending swap when this order is matched. Particularly
   773  		// important if we're matched as maker.
   774  		s.client2.disableWallets()
   775  		oid := tracker.ID()
   776  		// Wait 1 minute for order to receive match request.
   777  		maxMatchDuration := time.Minute
   778  		s.client2.log.Infof("Client 2 waiting %v for order %s to be partially matched", maxMatchDuration, tracker.token())
   779  		matched := notes.find(ctx, maxMatchDuration, func(n Notification) bool {
   780  			orderNote, isOrderNote := n.(*OrderNote)
   781  			isMatchedTopic := n.Topic() == TopicBuyMatchesMade || n.Topic() == TopicSellMatchesMade
   782  			return isOrderNote && isMatchedTopic && orderNote.Order.ID.String() == orderID
   783  		})
   784  		if !matched {
   785  			return fmt.Errorf("order %s not matched after %s", tracker.token(), maxMatchDuration)
   786  		}
   787  		if tracker.Trade().Remaining() == 0 {
   788  			return fmt.Errorf("order %s fully matched instead of partially", tracker.token())
   789  		}
   790  		if ctx.Err() != nil {
   791  			return nil // return here if some other goroutine errored
   792  		}
   793  		tracker.mtx.RLock()
   794  		// Partially matched, let's ditch the first match to trigger order
   795  		// revocation due to match inaction.
   796  		var isTaker bool
   797  		for _, match := range tracker.matches {
   798  			match.swapErr = fmt.Errorf("ditch match")
   799  			isTaker = match.Side == order.Taker
   800  			break // only interested in first match
   801  		}
   802  		tracker.mtx.RUnlock()
   803  		if isTaker {
   804  			// Monitor the match till MakerSwapCast, mine a couple blocks for
   805  			// maker's swap and ditch the match just when we're required to send
   806  			// counter-swap.
   807  			// Keep the order active to enable receiving audit request when Maker
   808  			// sends swap.
   809  			err = s.monitorTrackedTrade(s.client2, tracker, order.MakerSwapCast)
   810  			if err != nil {
   811  				return err
   812  			}
   813  		}
   814  		// Match will get revoked after lastEvent+bTimeout.
   815  		forgetClient2Order(oid) // ensure revoke_match request is "missed"
   816  		recordBeforeAfterStatuses(oid, order.OrderStatusBooked, order.OrderStatusRevoked)
   817  		return nil
   818  	})
   819  
   820  	// Wait for orders to be placed and forgotten or partly negotiated.
   821  	if err := waiter.Wait(); err != nil {
   822  		return err
   823  	}
   824  
   825  	s.log.Info("Orders placed and monitored to desired states.")
   826  
   827  	// Confirm that the order statuses are what we expect before triggering
   828  	// a authDEX->connect status recovery.
   829  	c2dc.tradeMtx.RLock()
   830  	for oid, expectStatus := range c2OrdersBefore {
   831  		tracker, found := c2ForgottenOrders[oid]
   832  		if !found {
   833  			tracker, found = c2dc.trades[oid]
   834  		}
   835  		if !found {
   836  			return fmt.Errorf("missing client 2 order %v", oid)
   837  		}
   838  		if tracker.metaData.Status != expectStatus {
   839  			return fmt.Errorf("expected pre-recovery status %v for client 2 order %v, got %v",
   840  				expectStatus, oid, tracker.metaData.Status)
   841  		}
   842  		s.client2.log.Infof("Client 2 order %v in expected pre-recovery status %v.", oid, expectStatus)
   843  	}
   844  	c2dc.tradeMtx.RUnlock()
   845  
   846  	// Check trade-locked amount before disconnecting.
   847  	c2Balance, err = s.client2.core.AssetBalance(s.base.id) // client 2 is seller
   848  	if err != nil {
   849  		return fmt.Errorf("client 2 pre-disconnect balance error %w", err)
   850  	}
   851  	s.client2.log.Infof("Client 2 %s available balance before disconnecting is %v.", s.base.symbol, s.base.valFmt(c2Balance.Available))
   852  	totalLockedByTrades := c2Balance.Locked - preTradeLockedBalance
   853  	preDisconnectLockedBalance := c2Balance.Locked   // should reduce after funds are returned
   854  	preDisconnectAvailableBal := c2Balance.Available // should increase after funds are returned
   855  
   856  	// Disconnect the DEX and allow some time for DEX to update order statuses.
   857  	s.client2.log.Info("Disconnecting client 2 from the DEX server.")
   858  	c2dc.connMaster.Disconnect()
   859  	// Disconnection is asynchronous, wait for confirmation of DEX disconnection.
   860  	disconnectTimeout := 10 * sleepFactor * time.Second
   861  	disconnected := s.client2.notes.find(context.Background(), disconnectTimeout, func(n Notification) bool {
   862  		connNote, ok := n.(*ConnEventNote)
   863  		return ok && connNote.Host == dexHost && connNote.ConnectionStatus != comms.Connected
   864  	})
   865  	if !disconnected {
   866  		return fmt.Errorf("client 2 dex not disconnected after %v", disconnectTimeout)
   867  	}
   868  
   869  	s.client2.enableWallets()
   870  	// Disconnect the wallets, they'll be reconnected when Login is called below.
   871  	// Login->connectWallets will error for btc spv wallets if the wallet is not
   872  	// first disconnected.
   873  	s.client2.disconnectWallets()
   874  
   875  	// Allow some time for orders to be revoked due to inaction, and
   876  	// for requests pending on the server to expire (usually bTimeout).
   877  	bTimeout := time.Millisecond * time.Duration(c2dc.cfg.BroadcastTimeout)
   878  	disconnectPeriod := 2 * bTimeout
   879  	s.client2.log.Infof("Waiting %v before reconnecting client 2 to DEX.", disconnectPeriod)
   880  	time.Sleep(disconnectPeriod)
   881  
   882  	s.client2.log.Info("Reconnecting client 2 to DEX to trigger order status reconciliation.")
   883  
   884  	// Use core.initialize to restore client 2 orders from db, and login
   885  	// to trigger dex authentication.
   886  	// TODO: cannot do this anymore with built-in wallets
   887  	err = s.client2.core.initialize()
   888  	if err != nil {
   889  		return fmt.Errorf("client 2 login error: %w", err)
   890  	}
   891  	err = s.client2.core.Login(s.client2.appPass)
   892  	if err != nil {
   893  		return fmt.Errorf("client 2 login error: %w", err)
   894  	}
   895  
   896  	c2dc = s.client2.dc()
   897  	c2dc.tradeMtx.RLock()
   898  	for oid, expectStatus := range c2OrdersAfter {
   899  		tracker, found := c2dc.trades[oid]
   900  		if !found {
   901  			return fmt.Errorf("client 2 order %v not found after re-initializing core", oid)
   902  		}
   903  		if tracker.metaData.Status != expectStatus {
   904  			return fmt.Errorf("status not updated for client 2 order %v, expected %v, got %v",
   905  				oid, expectStatus, tracker.metaData.Status)
   906  		}
   907  		s.client2.log.Infof("Client 2 order %v in expected post-recovery status %v.", oid, expectStatus)
   908  	}
   909  	c2dc.tradeMtx.RUnlock()
   910  
   911  	// Wait a bit for tick cycle to trigger inactive trade retirement and funds unlocking.
   912  	halfBTimeout := time.Millisecond * time.Duration(c2dc.cfg.BroadcastTimeout/2)
   913  	time.Sleep(halfBTimeout)
   914  
   915  	c2Balance, err = s.client2.core.AssetBalance(s.base.id) // client 2 is seller
   916  	if err != nil {
   917  		return fmt.Errorf("client 2 post-reconnect balance error %w", err)
   918  	}
   919  	s.client2.log.Infof("Client 2 %s available balance after reconnecting to DEX is %v.", s.base.symbol, s.base.valFmt(c2Balance.Available))
   920  	if c2Balance.Available != preDisconnectAvailableBal+totalLockedByTrades {
   921  		return fmt.Errorf("client 2 locked funds not returned: locked before trading %v, locked after trading %v, "+
   922  			"locked after reconnect %v", preTradeLockedBalance, preDisconnectLockedBalance, c2Balance.Locked)
   923  	}
   924  	if c2Balance.Locked != preDisconnectLockedBalance-totalLockedByTrades {
   925  		return fmt.Errorf("client 2 locked funds not returned: locked before trading %v, locked after trading %v, "+
   926  			"locked after reconnect %v", preTradeLockedBalance, preDisconnectLockedBalance, c2Balance.Locked)
   927  	}
   928  	s.client2.enableWallets()
   929  
   930  	for _, c := range s.clients {
   931  		if err := c.mineMedian(context.TODO(), s.quote.id); err != nil {
   932  			return err
   933  		}
   934  		if err := c.mineMedian(context.TODO(), s.base.id); err != nil {
   935  			return err
   936  		}
   937  	}
   938  
   939  	s.waitALittleBit()
   940  
   941  	return nil
   942  }
   943  
   944  // TestResendPendingRequests runs a simple trade test, simulates init/redeem
   945  // request errors during trade negotiation and ensures that failed requests
   946  // are retried and the trades complete successfully.
   947  func testResendPendingRequests(s *simulationTest) error {
   948  	var qty, rate uint64 = 1 * s.lotSize, 250 * s.rateStep
   949  	s.client1.isSeller, s.client2.isSeller = true, false
   950  
   951  	anErr := errors.New("intentional error from test")
   952  	var bits uint8
   953  	// Fail every first try of init and redeem. Second try will be
   954  	// passed on to the real comms.
   955  	preFilter1 := func(route string) error {
   956  		if route == msgjson.InitRoute {
   957  			if bits&0b1 == 0 {
   958  				bits |= 0b1
   959  				return anErr
   960  			}
   961  		}
   962  		if route == msgjson.RedeemRoute {
   963  			if bits&0b10 == 0 {
   964  				bits |= 0b10
   965  				return anErr
   966  			}
   967  		}
   968  		return nil
   969  	}
   970  
   971  	preFilter2 := func(route string) error {
   972  		if route == msgjson.InitRoute {
   973  			if bits&0b100 == 0 {
   974  				bits |= 0b100
   975  				return anErr
   976  			}
   977  		}
   978  		if route == msgjson.RedeemRoute {
   979  			if bits&0b1000 == 0 {
   980  				bits |= 0b1000
   981  				return anErr
   982  			}
   983  		}
   984  		return nil
   985  	}
   986  
   987  	s.client1.filteredConn.requestFilter.Store(preFilter1)
   988  	s.client2.filteredConn.requestFilter.Store(preFilter2)
   989  	defer s.client1.filteredConn.requestFilter.Store(func(string) error { return nil })
   990  	defer s.client2.filteredConn.requestFilter.Store(func(string) error { return nil })
   991  
   992  	return s.simpleTradeTest(qty, rate, order.MatchConfirmed)
   993  }
   994  
   995  // simpleTradeTest uses client1 and client2 to place similar orders but on
   996  // either sides that get matched and monitors the resulting trades up till the
   997  // specified final status.
   998  // Also checks that the changes to the clients wallets balances are within
   999  // expected range.
  1000  func (s *simulationTest) simpleTradeTest(qty, rate uint64, finalStatus order.MatchStatus) error {
  1001  	if s.client1.isSeller && s.client2.isSeller {
  1002  		return fmt.Errorf("Both client 1 and 2 cannot be sellers")
  1003  	}
  1004  
  1005  	c1OrderID, c2OrderID, err := s.placeTestOrders(qty, rate)
  1006  	if err != nil {
  1007  		return err
  1008  	}
  1009  
  1010  	if finalStatus == order.NewlyMatched {
  1011  		// Kill the wallets to prevent Maker from sending swap as soon as the
  1012  		// orders are matched.
  1013  		for _, client := range s.clients {
  1014  			client.disableWallets()
  1015  			defer client.enableWallets()
  1016  		}
  1017  
  1018  		s.log.Info("Disabled wallets to prevent order status from moving past order.NewlyMatched")
  1019  	}
  1020  
  1021  	if finalStatus == order.MakerSwapCast {
  1022  		// Kill the wallets to prevent Taker from sending swap as soon
  1023  		// as the orders are matched.
  1024  		name := s.client1.name
  1025  		if s.client1IsMaker {
  1026  			name = s.client2.name
  1027  			s.client2.disableWallets()
  1028  			defer s.client2.enableWallets()
  1029  		} else {
  1030  			s.client1.disableWallets()
  1031  			defer s.client1.enableWallets()
  1032  		}
  1033  		s.log.Infof("Disabled client %s wallets to prevent order status from moving past order.MakerSwapCast.", name)
  1034  	}
  1035  
  1036  	// NOTE: Client 1 is always maker the first time this test is run and
  1037  	// taker the second when running the same test twice.
  1038  	monitorTrades, ctx := errgroup.WithContext(context.Background())
  1039  	monitorTrades.Go(func() error {
  1040  		return s.monitorOrderMatchingAndTradeNeg(ctx, s.client1, c1OrderID, finalStatus)
  1041  	})
  1042  	monitorTrades.Go(func() error {
  1043  		return s.monitorOrderMatchingAndTradeNeg(ctx, s.client2, c2OrderID, finalStatus)
  1044  	})
  1045  	if err = monitorTrades.Wait(); err != nil {
  1046  		return err
  1047  	}
  1048  
  1049  	// Allow some time for balance changes to be properly reported.
  1050  	// There is usually a split-second window where a locked output
  1051  	// has been spent but the spending tx is still in mempool. This
  1052  	// will cause the txout to be included in the wallets locked
  1053  	// balance, causing a higher than actual balance report.
  1054  	s.waitALittleBit()
  1055  
  1056  	for _, client := range s.clients {
  1057  		if err = s.assertBalanceChanges(client, false); err != nil {
  1058  			return err
  1059  		}
  1060  	}
  1061  
  1062  	// Check if any refunds are necessary and wait to ensure the refunds
  1063  	// are completed.
  1064  	if finalStatus < order.MatchConfirmed {
  1065  		refundsWaiter, ctx := errgroup.WithContext(context.Background())
  1066  		refundsWaiter.Go(func() error {
  1067  			return s.checkAndWaitForRefunds(ctx, s.client1, c1OrderID)
  1068  		})
  1069  		refundsWaiter.Go(func() error {
  1070  			return s.checkAndWaitForRefunds(ctx, s.client2, c2OrderID)
  1071  		})
  1072  		if err = refundsWaiter.Wait(); err != nil {
  1073  			return err
  1074  		}
  1075  	}
  1076  
  1077  	s.log.Infof("Trades ended at %s.", finalStatus)
  1078  	return nil
  1079  }
  1080  
  1081  func (s *simulationTest) placeTestOrders(qty, rate uint64) (string, string, error) {
  1082  	for _, client := range s.clients {
  1083  		if err := s.updateBalances(client); err != nil {
  1084  			return "", "", fmt.Errorf("client %s balance update error: %v", client.name, err)
  1085  		}
  1086  		// Reset the expected balance changes for this client, to be updated
  1087  		// later in the monitorTrackedTrade function as swaps and redeems are
  1088  		// executed.
  1089  		client.expectBalanceDiffs = map[uint32]int64{s.base.id: 0, s.quote.id: 0}
  1090  	}
  1091  
  1092  	var (
  1093  		c1OrderID, c2OrderID string
  1094  		err                  error
  1095  	)
  1096  	placeOrderC1 := func() error {
  1097  		c1OrderID, err = s.placeOrder(s.client1, qty, rate, false)
  1098  		if err != nil {
  1099  			return fmt.Errorf("client1 place %s order error: %v", sellString(s.client1.isSeller), err)
  1100  		}
  1101  		return nil
  1102  	}
  1103  
  1104  	placeOrderC2 := func() error {
  1105  		c2OrderID, err = s.placeOrder(s.client2, qty, rate, false)
  1106  		if err != nil {
  1107  			return fmt.Errorf("client2 place %s order error: %v", sellString(s.client2.isSeller), err)
  1108  		}
  1109  		return nil
  1110  	}
  1111  
  1112  	// The client to have their order booked first becomes the maker of a
  1113  	// trade. Here the second time a trade is run for the same
  1114  	// simulationTest make and taker will be swapped allowing testing for
  1115  	// both sides at different stages of failure to act and their resolution.
  1116  	var (
  1117  		client  *simulationClient
  1118  		orderID string
  1119  	)
  1120  	if s.client1IsMaker {
  1121  		if err = placeOrderC1(); err != nil {
  1122  			return "", "", err
  1123  		}
  1124  		orderID = c1OrderID
  1125  		client = s.client1
  1126  	} else {
  1127  		if err = placeOrderC2(); err != nil {
  1128  			return "", "", err
  1129  		}
  1130  		orderID = c2OrderID
  1131  		client = s.client2
  1132  	}
  1133  
  1134  	tracker, err := client.findOrder(orderID)
  1135  	if err != nil {
  1136  		return "", "", err
  1137  	}
  1138  	// Wait the epoch duration for this order to get booked.
  1139  	epochDur := time.Duration(tracker.epochLen()) * time.Millisecond
  1140  	time.Sleep(epochDur)
  1141  
  1142  	if s.client1IsMaker {
  1143  		if err = placeOrderC2(); err != nil {
  1144  			return "", "", err
  1145  		}
  1146  	} else {
  1147  		if err = placeOrderC1(); err != nil {
  1148  			return "", "", err
  1149  		}
  1150  	}
  1151  
  1152  	return c1OrderID, c2OrderID, nil
  1153  }
  1154  
  1155  func (s *simulationTest) monitorOrderMatchingAndTradeNeg(ctx context.Context, client *simulationClient, orderID string, finalStatus order.MatchStatus) error {
  1156  	errs := newErrorSet("[client %s] ", client.name)
  1157  
  1158  	client.log.Infof("Client %s starting to monitor order %s.", client.name, orderID)
  1159  
  1160  	tracker, err := client.findOrder(orderID)
  1161  	if err != nil {
  1162  		return errs.addErr(err)
  1163  	}
  1164  
  1165  	// Wait up to 2 times the epoch duration for this order to get matched.
  1166  	maxMatchDuration := 2 * time.Duration(tracker.epochLen()) * time.Millisecond
  1167  	client.log.Infof("Client %s Waiting up to %v for matches on order %s.", client.name, maxMatchDuration, tracker.token())
  1168  	matched := client.notes.find(ctx, maxMatchDuration, func(n Notification) bool {
  1169  		orderNote, isOrderNote := n.(*OrderNote)
  1170  		isMatchedTopic := n.Topic() == TopicBuyMatchesMade || n.Topic() == TopicSellMatchesMade
  1171  		return isOrderNote && isMatchedTopic && orderNote.Order.ID.String() == orderID
  1172  	})
  1173  	if ctx.Err() != nil { // context canceled
  1174  		return nil
  1175  	}
  1176  	if !matched {
  1177  		return errs.add("order %s not matched after %s", tracker.token(), maxMatchDuration)
  1178  	}
  1179  
  1180  	recordBalanceChanges := func(isSwap bool, qty, rate uint64) {
  1181  		amt := int64(qty)
  1182  		a := s.base
  1183  		if client.isSeller != isSwap {
  1184  			// use quote amt for seller redeem and buyer swap
  1185  			amt = int64(calc.BaseToQuote(rate, qty))
  1186  			a = s.quote
  1187  		}
  1188  		if isSwap {
  1189  			amt *= -1
  1190  		}
  1191  		client.log.Infof("Updated %s balance diff with %s.", a.symbol, a.valFmt(amt))
  1192  		client.expectBalanceDiffs[a.id] += amt
  1193  	}
  1194  
  1195  	tracker.mtx.RLock()
  1196  	client.log.Infof("Client %s %d match(es) received for order %s.", client.name, len(tracker.matches), tracker.token())
  1197  	for _, match := range tracker.matches {
  1198  		client.log.Infof("Client %s is %s on match %s, amount %s %s.", client.name, match.Side.String(),
  1199  			token(match.MatchID.Bytes()), s.base.valFmt(match.Quantity), s.base.symbol)
  1200  		if match.Side == order.Taker {
  1201  			if finalStatus >= order.TakerSwapCast {
  1202  				recordBalanceChanges(true, match.Quantity, match.Rate)
  1203  			}
  1204  			if finalStatus >= order.MatchComplete {
  1205  				recordBalanceChanges(false, match.Quantity, match.Rate)
  1206  			}
  1207  		} else { // maker
  1208  			if finalStatus >= order.MakerSwapCast {
  1209  				recordBalanceChanges(true, match.Quantity, match.Rate)
  1210  			}
  1211  			if finalStatus >= order.MakerRedeemed {
  1212  				recordBalanceChanges(false, match.Quantity, match.Rate)
  1213  			}
  1214  		}
  1215  	}
  1216  	tracker.mtx.RUnlock()
  1217  
  1218  	return s.monitorTrackedTrade(client, tracker, finalStatus)
  1219  }
  1220  
  1221  func (s *simulationTest) monitorTrackedTrade(client *simulationClient, tracker *trackedTrade, finalStatus order.MatchStatus) error {
  1222  	// run a repeated check for match status changes to mine blocks as necessary.
  1223  	maxTradeDuration := 4 * time.Minute
  1224  	var waitedForOtherSideMakerInit, waitedForOtherSideTakerInit bool
  1225  
  1226  	tryUntil(s.ctx, maxTradeDuration, func() bool {
  1227  
  1228  		var completedTrades int
  1229  		mineAssets := make(map[uint32]uint32)
  1230  		var waitForOtherSideMakerInit, waitForOtherSideTakerInit bool
  1231  		var thisSide uint32
  1232  		// Don't spam.
  1233  		time.Sleep(time.Second * 2 * sleepFactor)
  1234  		tracker.mtx.Lock()
  1235  		for _, match := range tracker.matches {
  1236  			side, status := match.Side, match.Status
  1237  			client.psMTX.Lock()
  1238  			lastStatus := client.processedStatus[match.MatchID]
  1239  			client.psMTX.Unlock()
  1240  			if status >= finalStatus && lastStatus >= finalStatus {
  1241  				match.swapErr = fmt.Errorf("take no further action")
  1242  				completedTrades++
  1243  				continue
  1244  			}
  1245  			if status != lastStatus {
  1246  				client.log.Infof("Match %s: NOW =====> %s.", match.MatchID, status)
  1247  				client.psMTX.Lock()
  1248  				client.processedStatus[match.MatchID] = status
  1249  				client.psMTX.Unlock()
  1250  			}
  1251  
  1252  			logIt := func(swapOrRedeem string, assetID, nBlocks uint32) {
  1253  				var actor order.MatchSide
  1254  				if swapOrRedeem == "redeem" {
  1255  					actor = side // this client
  1256  				} else if side == order.Maker {
  1257  					actor = order.Taker // counter-party
  1258  				} else {
  1259  					actor = order.Maker
  1260  				}
  1261  				client.log.Infof("Mining %d %s blocks for %s's %s, match %s.", nBlocks, unbip(assetID),
  1262  					actor, swapOrRedeem, token(match.MatchID.Bytes()))
  1263  			}
  1264  
  1265  			if (side == order.Maker && status <= order.MakerSwapCast && finalStatus >= order.MakerSwapCast) ||
  1266  				(side == order.Taker && status <= order.TakerSwapCast && finalStatus >= order.TakerSwapCast) {
  1267  				// If the other side is geth, we need to give it
  1268  				// time to confirm the swap in order to populate
  1269  				// swap fees.
  1270  				if !waitedForOtherSideMakerInit && accountBIPs[tracker.wallets.toWallet.AssetID] {
  1271  					thisSide = tracker.wallets.fromWallet.AssetID
  1272  					waitForOtherSideMakerInit = true
  1273  				}
  1274  				if !waitedForOtherSideTakerInit && status > order.MakerSwapCast &&
  1275  					accountBIPs[tracker.wallets.toWallet.AssetID] {
  1276  					thisSide = tracker.wallets.fromWallet.AssetID
  1277  					waitForOtherSideTakerInit = true
  1278  				}
  1279  				// Progress from asset.
  1280  				nBlocks := tracker.metaData.FromSwapConf
  1281  				if accountBIPs[tracker.wallets.fromWallet.AssetID] {
  1282  					nBlocks = 8
  1283  				}
  1284  				assetID := tracker.wallets.fromWallet.AssetID
  1285  				mineAssets[assetID] = nBlocks
  1286  				logIt("swap", assetID, nBlocks)
  1287  			}
  1288  			if (side == order.Maker && status > order.TakerSwapCast && finalStatus > order.TakerSwapCast) ||
  1289  				(side == order.Taker && status > order.MakerRedeemed && finalStatus > order.MakerRedeemed) {
  1290  				if !waitedForOtherSideMakerInit && accountBIPs[tracker.wallets.fromWallet.AssetID] {
  1291  					thisSide = tracker.wallets.toWallet.AssetID
  1292  					waitForOtherSideMakerInit = true
  1293  				}
  1294  				if !waitedForOtherSideTakerInit && status > order.MakerSwapCast &&
  1295  					accountBIPs[tracker.wallets.fromWallet.AssetID] {
  1296  					thisSide = tracker.wallets.toWallet.AssetID
  1297  					waitForOtherSideTakerInit = true
  1298  				}
  1299  				// Progress to asset.
  1300  				nBlocks := tracker.metaData.ToSwapConf
  1301  				if accountBIPs[tracker.wallets.toWallet.AssetID] {
  1302  					nBlocks = 8
  1303  				}
  1304  				assetID := tracker.wallets.toWallet.AssetID
  1305  				mineAssets[assetID] = nBlocks
  1306  				logIt("redeem", assetID, nBlocks)
  1307  			}
  1308  		}
  1309  
  1310  		finish := completedTrades == len(tracker.matches)
  1311  		// Do not hold the lock while mining as this hinders trades.
  1312  		tracker.mtx.Unlock()
  1313  
  1314  		// Geth light client takes time for the best block to be updated
  1315  		// after mining. Sleeping to get confs registered for the init
  1316  		// before the match can be confirmed if the utxo side completes
  1317  		// too fast.
  1318  		if waitForOtherSideMakerInit || waitForOtherSideTakerInit {
  1319  			client.log.Infof("Not mining asset %d so geth can find init confirms.", thisSide)
  1320  			delete(mineAssets, thisSide)
  1321  		}
  1322  		mine := func(assetID, nBlocks uint32) {
  1323  			err := newHarnessCtrl(assetID).mineBlocks(s.ctx, nBlocks)
  1324  			if err != nil {
  1325  				client.log.Infof("%s mine error %v.", unbip(assetID), err) // return err???
  1326  			}
  1327  		}
  1328  		for assetID, swapConf := range mineAssets {
  1329  			mine(assetID, swapConf)
  1330  		}
  1331  		if waitForOtherSideMakerInit || waitForOtherSideTakerInit {
  1332  			client.log.Info("Sleeping 8 seconds for geth to update other side's init best block.")
  1333  			time.Sleep(time.Second * 8)
  1334  			if waitForOtherSideMakerInit {
  1335  				waitedForOtherSideMakerInit = true
  1336  			}
  1337  			if waitForOtherSideTakerInit {
  1338  				waitedForOtherSideTakerInit = true
  1339  			}
  1340  		}
  1341  		return finish
  1342  	})
  1343  	if s.ctx.Err() != nil { // context canceled
  1344  		return nil
  1345  	}
  1346  
  1347  	var incompleteTrades int
  1348  	tracker.mtx.RLock()
  1349  	for _, match := range tracker.matches {
  1350  		if match.Status < finalStatus {
  1351  			incompleteTrades++
  1352  			client.log.Infof("Incomplete trade: order %s, match %s, status %s, side %s.", tracker.token(),
  1353  				token(match.MatchID[:]), match.Status, match.Side)
  1354  		} else {
  1355  			client.log.Infof("Trade for order %s, match %s monitored successfully till %s, side %s.", tracker.token(),
  1356  				token(match.MatchID[:]), match.Status, match.Side)
  1357  		}
  1358  	}
  1359  	tracker.mtx.RUnlock()
  1360  	if incompleteTrades > 0 {
  1361  		return fmt.Errorf("client %s reported %d incomplete trades for order %s after %s",
  1362  			client.name, incompleteTrades, tracker.token(), maxTradeDuration)
  1363  	}
  1364  
  1365  	return nil
  1366  }
  1367  
  1368  // swaps cannot be refunded until the MedianTimePast is greater than
  1369  // the swap locktime. The MedianTimePast is calculated by taking the
  1370  // timestamps of the last 11 blocks and finding the median. Mining 6
  1371  // blocks on the chain a second from now will ensure that the
  1372  // MedianTimePast will be greater than the furthest swap locktime,
  1373  // thereby lifting the time lock on all these swaps.
  1374  func (client *simulationClient) mineMedian(ctx context.Context, assetID uint32) error {
  1375  	time.Sleep(sleepFactor * time.Second)
  1376  	if err := newHarnessCtrl(assetID).mineBlocks(ctx, 6); err != nil {
  1377  		return fmt.Errorf("client %s: error mining 6 %s blocks for swap refunds: %v",
  1378  			client.name, unbip(assetID), err)
  1379  	}
  1380  	client.log.Infof("Mined 6 blocks for assetID %d to expire swap locktimes.", assetID)
  1381  	return nil
  1382  }
  1383  
  1384  func (s *simulationTest) checkAndWaitForRefunds(ctx context.Context, client *simulationClient, orderID string) error {
  1385  	// check if client has pending refunds
  1386  	client.log.Infof("Checking if refunds are necessary for client %s.", client.name)
  1387  	refundAmts := map[uint32]int64{s.base.id: 0, s.quote.id: 0}
  1388  	var furthestLockTime time.Time
  1389  
  1390  	hasRefundableSwap := func(match *matchTracker) bool {
  1391  		sentSwap := match.MetaData.Proof.ContractData != nil
  1392  		noRedeems := match.Status < order.MakerRedeemed
  1393  		return sentSwap && noRedeems
  1394  	}
  1395  
  1396  	tracker, err := client.findOrder(orderID)
  1397  	if err != nil {
  1398  		return err
  1399  	}
  1400  
  1401  	if tracker == nil {
  1402  		return nil
  1403  	}
  1404  	tracker.mtx.RLock()
  1405  	for _, match := range tracker.matches {
  1406  		if !hasRefundableSwap(match) {
  1407  			continue
  1408  		}
  1409  
  1410  		swapAmt := match.Quantity
  1411  		if !client.isSeller {
  1412  			swapAmt = calc.BaseToQuote(match.Rate, match.Quantity)
  1413  		}
  1414  		refundAmts[tracker.wallets.fromWallet.AssetID] += int64(swapAmt)
  1415  
  1416  		matchTime := match.matchTime()
  1417  		swapLockTime := matchTime.Add(tracker.lockTimeTaker)
  1418  		if match.Side == order.Maker {
  1419  			swapLockTime = matchTime.Add(tracker.lockTimeMaker)
  1420  		}
  1421  		if swapLockTime.After(furthestLockTime) {
  1422  			furthestLockTime = swapLockTime
  1423  		}
  1424  	}
  1425  	tracker.mtx.RUnlock()
  1426  
  1427  	if ctx.Err() != nil { // context canceled
  1428  		return nil
  1429  	}
  1430  	if furthestLockTime.IsZero() {
  1431  		client.log.Infof("No refunds necessary for client %s.", client.name)
  1432  		return nil
  1433  	}
  1434  
  1435  	client.log.Infof("Found refundable swaps worth %s %s and %s %s.", s.base.valFmt(refundAmts[s.base.id]),
  1436  		s.base.symbol, s.quote.valFmt(refundAmts[s.quote.id]), s.quote.symbol)
  1437  
  1438  	// wait for refunds to be executed
  1439  	now := time.Now()
  1440  	if furthestLockTime.After(now) {
  1441  		wait := furthestLockTime.Sub(now)
  1442  		client.log.Infof("Waiting until the longest timelock expires at %v before checking "+
  1443  			"client %s wallet balances for expected refunds.", wait, client.name)
  1444  		select {
  1445  		case <-ctx.Done():
  1446  			return nil
  1447  		case <-time.After(wait):
  1448  		}
  1449  	}
  1450  
  1451  	if refundAmts[s.quote.id] > 0 {
  1452  		if err := client.mineMedian(ctx, s.quote.id); err != nil {
  1453  			return err
  1454  		}
  1455  	}
  1456  	if refundAmts[s.base.id] > 0 {
  1457  		if err := client.mineMedian(ctx, s.base.id); err != nil {
  1458  			return err
  1459  		}
  1460  	}
  1461  
  1462  	// allow up to 30 seconds for core to get around to refunding the swaps
  1463  	var notRefundedSwaps int
  1464  	refundWaitTimeout := 60 * time.Second
  1465  	refundedSwaps := tryUntil(ctx, refundWaitTimeout, func() bool {
  1466  		tracker.mtx.RLock()
  1467  		defer tracker.mtx.RUnlock()
  1468  		notRefundedSwaps = 0
  1469  		for _, match := range tracker.matches {
  1470  			if hasRefundableSwap(match) && match.MetaData.Proof.RefundCoin == nil {
  1471  				notRefundedSwaps++
  1472  			}
  1473  		}
  1474  		return notRefundedSwaps == 0
  1475  	})
  1476  	if ctx.Err() != nil { // context canceled
  1477  		return nil
  1478  	}
  1479  	if !refundedSwaps {
  1480  		return fmt.Errorf("client %s reported %d unrefunded swaps after %s",
  1481  			client.name, notRefundedSwaps, refundWaitTimeout)
  1482  	}
  1483  
  1484  	// swaps refunded, mine some blocks to get the refund txs confirmed and
  1485  	// confirm that balance changes are as expected.
  1486  	for assetID, expectedBalanceDiff := range refundAmts {
  1487  		if expectedBalanceDiff > 0 {
  1488  			if err := newHarnessCtrl(assetID).mineBlocks(ctx, 1); err != nil {
  1489  				return fmt.Errorf("%s mine error %v", unbip(assetID), err)
  1490  			}
  1491  		}
  1492  	}
  1493  	s.waitALittleBit()
  1494  
  1495  	client.expectBalanceDiffs = refundAmts
  1496  	err = s.assertBalanceChanges(client, true)
  1497  	if err == nil {
  1498  		client.log.Infof("Successfully refunded swaps worth %s %s and %s %s.", s.base.valFmt(refundAmts[s.base.id]),
  1499  			s.base.symbol, s.quote.valFmt(refundAmts[s.quote.id]), s.quote.symbol)
  1500  	}
  1501  	return err
  1502  }
  1503  
  1504  func tryUntil(ctx context.Context, tryDuration time.Duration, tryFn func() bool) bool {
  1505  	expire := time.NewTimer(tryDuration)
  1506  	tick := time.NewTicker(250 * time.Millisecond)
  1507  	defer func() {
  1508  		expire.Stop()
  1509  		tick.Stop()
  1510  	}()
  1511  	for {
  1512  		select {
  1513  		case <-ctx.Done():
  1514  			return false
  1515  		case <-expire.C:
  1516  			return false
  1517  		case <-tick.C:
  1518  			if tryFn() {
  1519  				return true
  1520  			}
  1521  		}
  1522  	}
  1523  }
  1524  
  1525  /************************************
  1526  HELPER TYPES, FUNCTIONS AND METHODS
  1527  ************************************/
  1528  
  1529  type harnessCtrl struct {
  1530  	dir, fundCmd, fundStr string
  1531  }
  1532  
  1533  func (hc *harnessCtrl) run(ctx context.Context, cmd string, args ...string) error {
  1534  	command := exec.CommandContext(ctx, cmd, args...)
  1535  	command.Dir = hc.dir
  1536  	r, err := command.CombinedOutput()
  1537  	if err != nil {
  1538  		return fmt.Errorf("exec error: running %q from directory %q, err = %q, output = %q",
  1539  			command, command.Dir, err, string(r))
  1540  	}
  1541  	return nil
  1542  }
  1543  
  1544  func (hc *harnessCtrl) mineBlocks(ctx context.Context, n uint32) error {
  1545  	return hc.run(ctx, "./mine-alpha", fmt.Sprintf("%d", n))
  1546  }
  1547  
  1548  func (hc *harnessCtrl) fund(ctx context.Context, address string, amts []int) error {
  1549  	for _, amt := range amts {
  1550  		fs := fmt.Sprintf(hc.fundStr, address, amt)
  1551  		strs := strings.Split(fs, "_")
  1552  		err := hc.run(ctx, hc.fundCmd, strs...)
  1553  		if err != nil {
  1554  			return err
  1555  		}
  1556  	}
  1557  	return nil
  1558  }
  1559  
  1560  func newHarnessCtrl(assetID uint32) *harnessCtrl {
  1561  	symbolParts := strings.Split(dex.BipIDSymbol(assetID), ".")
  1562  	baseChainSymbol := symbolParts[0]
  1563  	if len(symbolParts) == 2 {
  1564  		baseChainSymbol = symbolParts[1]
  1565  	}
  1566  	switch assetID {
  1567  	case dcr.BipID, btc.BipID, ltc.BipID, bch.BipID, doge.BipID, firo.BipID, zec.BipID, zcl.BipID, dgb.BipID, dash.BipID:
  1568  		return &harnessCtrl{
  1569  			dir:     filepath.Join(dextestDir, baseChainSymbol, "harness-ctl"),
  1570  			fundCmd: "./alpha",
  1571  			fundStr: "sendtoaddress_%s_%d",
  1572  		}
  1573  	case eth.BipID, polygon.BipID:
  1574  		// Sending with values of .1 eth.
  1575  		return &harnessCtrl{
  1576  			dir:     filepath.Join(dextestDir, baseChainSymbol, "harness-ctl"),
  1577  			fundCmd: "./sendtoaddress",
  1578  			fundStr: "%s_%d",
  1579  		}
  1580  	case ethUsdcID, polygonUsdcID:
  1581  		return &harnessCtrl{
  1582  			dir:     filepath.Join(dextestDir, baseChainSymbol, "harness-ctl"),
  1583  			fundCmd: "./sendUSDC",
  1584  			fundStr: "%s_%d",
  1585  		}
  1586  	}
  1587  	panic(fmt.Sprintf("unknown asset %d for harness control", assetID))
  1588  }
  1589  
  1590  type tWallet struct {
  1591  	pass       []byte
  1592  	config     map[string]string
  1593  	walletType string // type is a keyword
  1594  	fund       bool
  1595  	hc         *harnessCtrl
  1596  	parent     *WalletForm
  1597  }
  1598  
  1599  var cloneTypes = map[uint32]string{
  1600  	0:   "bitcoindRPC",
  1601  	2:   "litecoindRPC",
  1602  	20:  "digibytedRPC",
  1603  	145: "bitcoindRPC", // yes, same as btc
  1604  	3:   "dogecoindRPC",
  1605  	136: "firodRPC",
  1606  	133: "zcashdRPC",
  1607  	147: "zclassicdRPC",
  1608  	5:   "dashdRPC",
  1609  }
  1610  
  1611  // accountBIPs is a map of account based assets. Used in fee estimation.
  1612  var accountBIPs = map[uint32]bool{
  1613  	eth.BipID:     true,
  1614  	ethUsdcID:     true,
  1615  	polygon.BipID: true,
  1616  	polygonUsdcID: true,
  1617  }
  1618  
  1619  func dcrWallet(wt SimWalletType, node string) (*tWallet, error) {
  1620  	switch wt {
  1621  	case WTSPVNative:
  1622  		return &tWallet{
  1623  			walletType: "SPV",
  1624  			fund:       true,
  1625  		}, nil
  1626  	case WTCoreClone:
  1627  	default:
  1628  		return nil, fmt.Errorf("invalid wallet type: %v", wt)
  1629  	}
  1630  
  1631  	cfg, err := config.Parse(filepath.Join(dextestDir, "dcr", node, fmt.Sprintf("%s.conf", node)))
  1632  	if err != nil {
  1633  		return nil, err
  1634  	}
  1635  	cfg["account"] = "default"
  1636  	return &tWallet{
  1637  		walletType: "dcrwalletRPC",
  1638  		pass:       []byte("abc"),
  1639  		config:     cfg,
  1640  	}, nil
  1641  }
  1642  
  1643  func btcWallet(wt SimWalletType, node string) (*tWallet, error) {
  1644  	return btcCloneWallet(btc.BipID, node, wt)
  1645  }
  1646  
  1647  func ltcWallet(wt SimWalletType, node string) (*tWallet, error) {
  1648  	return btcCloneWallet(ltc.BipID, node, wt)
  1649  }
  1650  
  1651  func bchWallet(wt SimWalletType, node string) (*tWallet, error) {
  1652  	return btcCloneWallet(bch.BipID, node, wt)
  1653  }
  1654  
  1655  func firoWallet(wt SimWalletType, node string) (*tWallet, error) {
  1656  	return btcCloneWallet(firo.BipID, node, wt)
  1657  }
  1658  
  1659  func ethWallet() (*tWallet, error) {
  1660  	return &tWallet{
  1661  		fund:       true,
  1662  		walletType: "rpc",
  1663  		config:     map[string]string{"providers": ethAlphaIPCFile},
  1664  	}, nil
  1665  }
  1666  
  1667  func polygonWallet() (*tWallet, error) {
  1668  	return &tWallet{
  1669  		fund:       true,
  1670  		walletType: "rpc",
  1671  		config:     map[string]string{"providers": polygonAlphaIPCFile},
  1672  	}, nil
  1673  }
  1674  
  1675  func usdcWallet() (*tWallet, error) {
  1676  	return &tWallet{
  1677  		fund:       true,
  1678  		walletType: "token",
  1679  		parent: &WalletForm{
  1680  			Type:    "rpc",
  1681  			AssetID: eth.BipID,
  1682  			Config:  map[string]string{"providers": ethAlphaIPCFile},
  1683  		},
  1684  	}, nil
  1685  }
  1686  
  1687  func polyUsdcWallet() (*tWallet, error) {
  1688  	return &tWallet{
  1689  		fund:       true,
  1690  		walletType: "token",
  1691  		parent: &WalletForm{
  1692  			Type:    "rpc",
  1693  			AssetID: polygon.BipID,
  1694  			Config:  map[string]string{"providers": polygonAlphaIPCFile},
  1695  		},
  1696  	}, nil
  1697  }
  1698  
  1699  func btcCloneWallet(assetID uint32, node string, wt SimWalletType) (*tWallet, error) {
  1700  	switch wt {
  1701  	case WTSPVNative:
  1702  		return &tWallet{
  1703  			walletType: "SPV",
  1704  			fund:       true,
  1705  		}, nil
  1706  	case WTElectrum:
  1707  		// dex/testing/btc/electrum.sh
  1708  		cfg, err := config.Parse(filepath.Join(dextestDir, "electrum", dex.BipIDSymbol(assetID), "client-config.ini"))
  1709  		if err != nil {
  1710  			return nil, err
  1711  		}
  1712  		return &tWallet{
  1713  			walletType: "electrumRPC",
  1714  			pass:       []byte("abc"),
  1715  			config:     cfg,
  1716  			fund:       true,
  1717  		}, nil
  1718  	case WTCoreClone:
  1719  	default:
  1720  		return nil, fmt.Errorf("invalid wallet type: %v", wt)
  1721  	}
  1722  
  1723  	rpcWalletType, ok := cloneTypes[assetID]
  1724  	if !ok {
  1725  		return nil, fmt.Errorf("invalid wallet type %v for asset %v", wt, assetID)
  1726  	}
  1727  
  1728  	parentNode := node
  1729  	pass := []byte("abc")
  1730  	if node == "gamma" || node == "delta" || assetID == zec.BipID || assetID == zcl.BipID {
  1731  		if assetID != dash.BipID {
  1732  			pass = nil
  1733  		}
  1734  	}
  1735  
  1736  	switch assetID {
  1737  	case doge.BipID, zec.BipID, zcl.BipID, firo.BipID:
  1738  	// dogecoind, zcashd and firod don't support > 1 wallet, so gamma and delta
  1739  	// have their own nodes.
  1740  	default:
  1741  		switch node {
  1742  		case "gamma":
  1743  			parentNode = "alpha"
  1744  		case "delta":
  1745  			parentNode = "beta"
  1746  		}
  1747  	}
  1748  
  1749  	cfg, err := config.Parse(filepath.Join(dextestDir, dex.BipIDSymbol(assetID), parentNode, parentNode+".conf"))
  1750  	if err != nil {
  1751  		return nil, err
  1752  	}
  1753  
  1754  	if parentNode != node {
  1755  		cfg["walletname"] = node
  1756  	}
  1757  
  1758  	// doge fees are slightly higher than others. Leaving this as 0 will
  1759  	// apply bitcoin limits.
  1760  	switch assetID {
  1761  	case doge.BipID:
  1762  		cfg["fallbackfee"] = fmt.Sprintf("%f", dexdoge.DefaultFee*1000/1e8)
  1763  		cfg["feeratelimit"] = fmt.Sprintf("%f", dexdoge.DefaultFeeRateLimit*1000/1e8)
  1764  	case dgb.BipID:
  1765  		cfg["fallbackfee"] = fmt.Sprintf("%f", dexdgb.DefaultFee*1000/1e8)
  1766  		cfg["feeratelimit"] = fmt.Sprintf("%f", dexdgb.DefaultFeeRateLimit*1000/1e8)
  1767  	}
  1768  
  1769  	return &tWallet{
  1770  		walletType: rpcWalletType,
  1771  		pass:       pass,
  1772  		config:     cfg,
  1773  	}, nil
  1774  }
  1775  
  1776  func dogeWallet(node string) (*tWallet, error) {
  1777  	return btcCloneWallet(doge.BipID, node, WTCoreClone)
  1778  }
  1779  
  1780  func dashWallet(node string) (*tWallet, error) {
  1781  	return btcCloneWallet(dash.BipID, node, WTCoreClone)
  1782  }
  1783  
  1784  func dgbWallet(node string) (*tWallet, error) {
  1785  	return btcCloneWallet(dgb.BipID, node, WTCoreClone)
  1786  }
  1787  
  1788  func zecWallet(node string) (*tWallet, error) {
  1789  	if node == "alpha" {
  1790  		return nil, errors.New("cannot use alpha wallet on Zcash")
  1791  	}
  1792  	cfg, err := config.Parse(filepath.Join(dextestDir, "zec", node, node+".conf"))
  1793  	if err != nil {
  1794  		return nil, err
  1795  	}
  1796  	return &tWallet{
  1797  		walletType: "zcashdRPC",
  1798  		config:     cfg,
  1799  	}, nil
  1800  }
  1801  
  1802  func zclWallet(node string) (*tWallet, error) {
  1803  	return btcCloneWallet(zcl.BipID, node, WTCoreClone)
  1804  }
  1805  
  1806  func (s *simulationTest) newClient(name string, cl *SimClient) (*simulationClient, error) {
  1807  	wallets := make(map[uint32]*tWallet, 2)
  1808  	addWallet := func(assetID uint32, wt SimWalletType, node string) error {
  1809  		var tw *tWallet
  1810  		var err error
  1811  		switch assetID {
  1812  		case dcr.BipID:
  1813  			tw, err = dcrWallet(wt, node)
  1814  		case btc.BipID:
  1815  			tw, err = btcWallet(wt, node)
  1816  		case eth.BipID:
  1817  			tw, err = ethWallet()
  1818  		case ethUsdcID:
  1819  			tw, err = usdcWallet()
  1820  		case polygon.BipID:
  1821  			tw, err = polygonWallet()
  1822  		case polygonUsdcID:
  1823  			tw, err = polyUsdcWallet()
  1824  		case ltc.BipID:
  1825  			tw, err = ltcWallet(wt, node)
  1826  		case bch.BipID:
  1827  			tw, err = bchWallet(wt, node)
  1828  		case doge.BipID:
  1829  			tw, err = dogeWallet(node)
  1830  		case dgb.BipID:
  1831  			tw, err = dgbWallet(node)
  1832  		case dash.BipID:
  1833  			tw, err = dashWallet(node)
  1834  		case firo.BipID:
  1835  			tw, err = firoWallet(wt, node)
  1836  		case zec.BipID:
  1837  			tw, err = zecWallet(node)
  1838  		case zcl.BipID:
  1839  			tw, err = zclWallet(node)
  1840  		default:
  1841  			return fmt.Errorf("no method to create wallet for asset %d", assetID)
  1842  		}
  1843  		if err != nil {
  1844  			return err
  1845  		}
  1846  		wallets[assetID] = tw
  1847  		return nil
  1848  	}
  1849  	if err := addWallet(s.base.id, cl.BaseWalletType, cl.BaseNode); err != nil {
  1850  		return nil, err
  1851  	}
  1852  	if err := addWallet(s.quote.id, cl.QuoteWalletType, cl.QuoteNode); err != nil {
  1853  		return nil, err
  1854  	}
  1855  	return &simulationClient{
  1856  		SimClient:       cl,
  1857  		name:            name,
  1858  		log:             s.log.SubLogger(name),
  1859  		appPass:         []byte(fmt.Sprintf("client-%s", name)),
  1860  		wallets:         wallets,
  1861  		processedStatus: make(map[order.MatchID]order.MatchStatus),
  1862  	}, nil
  1863  }
  1864  
  1865  type simulationClient struct {
  1866  	*SimClient
  1867  	name  string
  1868  	log   dex.Logger
  1869  	wg    sync.WaitGroup
  1870  	core  *Core
  1871  	notes *notificationReader
  1872  
  1873  	psMTX           sync.Mutex
  1874  	processedStatus map[order.MatchID]order.MatchStatus
  1875  
  1876  	appPass  []byte
  1877  	wallets  map[uint32]*tWallet
  1878  	balances map[uint32]uint64
  1879  	isSeller bool
  1880  	// Update after each test run to perform post-test balance
  1881  	// change validation. Set to nil to NOT perform balance checks.
  1882  	expectBalanceDiffs map[uint32]int64
  1883  	lastOrder          []byte
  1884  	filteredConn       *tConn
  1885  }
  1886  
  1887  var clientCounter uint32
  1888  
  1889  func (client *simulationClient) init(ctx context.Context) error {
  1890  	tmpDir, _ := os.MkdirTemp("", "")
  1891  	cNum := atomic.AddUint32(&clientCounter, 1)
  1892  	var err error
  1893  	client.core, err = New(&Config{
  1894  		DBPath: filepath.Join(tmpDir, "dex.db"),
  1895  		Net:    dex.Regtest,
  1896  		Logger: dex.StdOutLogger("CORE:"+strconv.Itoa(int(cNum)), client.log.Level()),
  1897  	})
  1898  	if err != nil {
  1899  		return err
  1900  	}
  1901  
  1902  	client.notes = client.startNotificationReader(ctx)
  1903  	return nil
  1904  }
  1905  
  1906  func (client *simulationClient) replaceConns() {
  1907  	// Put the real comms in a fake comms we can induce request failures
  1908  	// with.
  1909  	client.core.connMtx.Lock()
  1910  	client.filteredConn = &tConn{
  1911  		WsConn: client.core.conns[dexHost].WsConn,
  1912  	}
  1913  	client.core.conns[dexHost].WsConn = client.filteredConn
  1914  	client.core.connMtx.Unlock()
  1915  }
  1916  
  1917  func (s *simulationTest) registerDEX(client *simulationClient) error {
  1918  	dc := client.dc()
  1919  	if dc != nil {
  1920  		dc.connMaster.Disconnect()
  1921  		client.core.connMtx.Lock()
  1922  		delete(client.core.conns, dc.acct.host)
  1923  		client.core.connMtx.Unlock()
  1924  	}
  1925  
  1926  	dexConf, err := client.core.GetDEXConfig(dexHost, nil)
  1927  	if err != nil {
  1928  		return err
  1929  	}
  1930  
  1931  	feeAssetSymbol := dex.BipIDSymbol(s.regAsset)
  1932  
  1933  	bondAsset := dexConf.BondAssets[feeAssetSymbol]
  1934  	if bondAsset == nil {
  1935  		return fmt.Errorf("%s not supported for fees!", feeAssetSymbol)
  1936  	}
  1937  	postBondRes, err := client.core.PostBond(&PostBondForm{
  1938  		Addr:     dexHost,
  1939  		AppPass:  client.appPass,
  1940  		Asset:    &s.regAsset,
  1941  		Bond:     bondAsset.Amt,
  1942  		LockTime: uint64(time.Now().Add(time.Hour * 24 * 30 * 5).Unix()),
  1943  	})
  1944  	if err != nil {
  1945  		return err
  1946  	}
  1947  	client.log.Infof("Sent registration fee to DEX %s.", dexHost)
  1948  
  1949  	// Mine block(s) to mark fee as paid.
  1950  
  1951  	done := make(chan struct{})
  1952  	go func() {
  1953  		for {
  1954  			select {
  1955  			case <-time.After(time.Second * 5):
  1956  				err = newHarnessCtrl(s.regAsset).mineBlocks(s.ctx, 1)
  1957  				if err != nil {
  1958  					client.log.Errorf("error mining fee blocks: %v", err)
  1959  				}
  1960  			case <-done:
  1961  				return
  1962  			}
  1963  		}
  1964  	}()
  1965  
  1966  	client.log.Infof("Mined %d %s blocks for fee payment confirmation.", postBondRes.ReqConfirms, feeAssetSymbol)
  1967  
  1968  	// Wait up to bTimeout+12 seconds for fee payment. notify_fee times out
  1969  	// after bTimeout+10 seconds.
  1970  	feeTimeout := time.Millisecond*time.Duration(client.dc().cfg.BroadcastTimeout) + 12*time.Second
  1971  	client.log.Infof("Waiting %v for bond confirmation notice.", feeTimeout)
  1972  	feePaid := client.notes.find(s.ctx, feeTimeout, func(n Notification) bool {
  1973  		return n.Type() == NoteTypeBondPost && n.Topic() == TopicBondConfirmed
  1974  	})
  1975  	close(done)
  1976  	if !feePaid {
  1977  		return fmt.Errorf("fee payment not confirmed after %s", feeTimeout)
  1978  	}
  1979  
  1980  	client.log.Infof("Fee payment confirmed for client %s.", client.name)
  1981  	return nil
  1982  }
  1983  
  1984  type notificationReader struct {
  1985  	sync.Mutex
  1986  	feed  <-chan Notification
  1987  	notes []Notification
  1988  }
  1989  
  1990  // startNotificationReader opens a new channel for receiving Core notifications
  1991  // and starts a goroutine to monitor the channel for new notifications to prevent
  1992  // the channel from blocking. Notifications received are added to a notes slice
  1993  // to be read by consumers subsequently.
  1994  // If multiple concurrent processes require access to Core notifications, each
  1995  // should start a notificationReader to ensure that desired notifications are
  1996  // received.
  1997  func (client *simulationClient) startNotificationReader(ctx context.Context) *notificationReader {
  1998  	n := &notificationReader{
  1999  		feed: client.core.NotificationFeed().C,
  2000  	}
  2001  
  2002  	// keep notification channel constantly drained to avoid
  2003  	// 'blocking notification channel' error logs.
  2004  	go func() {
  2005  		for {
  2006  			select {
  2007  			case note := <-n.feed:
  2008  				n.Lock()
  2009  				n.notes = append(n.notes, note)
  2010  				n.Unlock()
  2011  
  2012  			case <-ctx.Done():
  2013  				return
  2014  			}
  2015  		}
  2016  	}()
  2017  
  2018  	return n
  2019  }
  2020  
  2021  // read returns the notifications saved by this notification reader as a slice
  2022  // of Notification objects. The notifications slice is cleared to accept new
  2023  // notifications.
  2024  func (n *notificationReader) readNotifications() []Notification {
  2025  	n.Lock()
  2026  	defer n.Unlock()
  2027  	notifications := n.notes
  2028  	n.notes = nil // mark as "read"
  2029  	return notifications
  2030  }
  2031  
  2032  // find repeatedly checks the client.notifications slice for a particular
  2033  // notification until the notification is found or the specified waitDuration
  2034  // elapses. Clears the notifications slice.
  2035  func (n *notificationReader) find(ctx context.Context, waitDuration time.Duration, check func(Notification) bool) bool {
  2036  	return tryUntil(ctx, waitDuration, func() bool {
  2037  		notifications := n.readNotifications()
  2038  		for _, n := range notifications {
  2039  			if check(n) {
  2040  				return true
  2041  			}
  2042  		}
  2043  		return false
  2044  	})
  2045  }
  2046  
  2047  func (s *simulationTest) placeOrder(client *simulationClient, qty, rate uint64, tifNow bool) (string, error) {
  2048  	dc := client.dc()
  2049  	mkt := dc.marketConfig(s.marketName)
  2050  	if mkt == nil {
  2051  		return "", fmt.Errorf("no %s market found", s.marketName)
  2052  	}
  2053  
  2054  	tradeForm := &TradeForm{
  2055  		Host:    dexHost,
  2056  		Base:    s.base.id,
  2057  		Quote:   s.quote.id,
  2058  		IsLimit: true,
  2059  		Sell:    client.isSeller,
  2060  		Qty:     qty,
  2061  		Rate:    rate,
  2062  		TifNow:  tifNow,
  2063  	}
  2064  
  2065  	ord, err := client.core.Trade(client.appPass, tradeForm)
  2066  	if err != nil {
  2067  		return "", err
  2068  	}
  2069  
  2070  	client.lastOrder = ord.ID
  2071  
  2072  	r := calc.ConventionalRateAlt(rate, s.base.conversionFactor, s.quote.conversionFactor)
  2073  
  2074  	client.log.Infof("Client %s placed order %sing %s %s at %f %s/%s (%s).", client.name, sellString(client.isSeller),
  2075  		s.base.valFmt(qty), s.base.symbol, r, s.quote.symbol, s.base.symbol, ord.ID[:8])
  2076  	return ord.ID.String(), nil
  2077  }
  2078  
  2079  func (s *simulationTest) updateBalances(client *simulationClient) error {
  2080  	client.log.Infof("Updating balances for client %s.", client.name)
  2081  	client.balances = make(map[uint32]uint64, len(client.wallets))
  2082  	setBalance := func(a *assetConfig) error {
  2083  		if parent := client.wallets[a.id].parent; parent != nil {
  2084  			parentSymbol := dex.BipIDSymbol(parent.AssetID)
  2085  			parentBalance, err := client.core.AssetBalance(parent.AssetID)
  2086  			if err != nil {
  2087  				return fmt.Errorf("error getting parent %s balance: %w", parentSymbol, err)
  2088  			}
  2089  			s.log.Infof("Client %s parent %s balance: available %s, immature %s, locked %s.", client.name, parentSymbol,
  2090  				a.valFmt(parentBalance.Available), a.valFmt(parentBalance.Immature), a.valFmt(parentBalance.Locked))
  2091  		}
  2092  
  2093  		balances, err := client.core.AssetBalance(a.id)
  2094  		if err != nil {
  2095  			return err
  2096  		}
  2097  		client.balances[a.id] = balances.Available + balances.Immature + balances.Locked
  2098  		client.log.Infof("Client %s %s balance: available %s, immature %s, locked %s.", client.name, a.symbol,
  2099  			a.valFmt(balances.Available), a.valFmt(balances.Immature), a.valFmt(balances.Locked))
  2100  		return nil
  2101  	}
  2102  	if err := setBalance(s.base); err != nil {
  2103  		return err
  2104  	}
  2105  	return setBalance(s.quote)
  2106  }
  2107  
  2108  func (s *simulationTest) assertBalanceChanges(client *simulationClient, isRefund bool) error {
  2109  	if client.expectBalanceDiffs == nil {
  2110  		return errors.New("balance diff is nil")
  2111  	}
  2112  
  2113  	ord, err := client.core.Order(client.lastOrder)
  2114  	if err != nil {
  2115  		return errors.New("last order not found")
  2116  	}
  2117  
  2118  	var baseFees, quoteFees int64
  2119  	if fees := ord.FeesPaid; fees != nil && !isRefund {
  2120  		if ord.Sell {
  2121  			baseFees = int64(fees.Swap)
  2122  			quoteFees = int64(fees.Redemption)
  2123  		} else {
  2124  			quoteFees = int64(fees.Swap)
  2125  			baseFees = int64(fees.Redemption)
  2126  		}
  2127  	}
  2128  
  2129  	// Account assets require a refund fee in addition to the swap amount.
  2130  	if isRefund {
  2131  		// NOTE: Gas price may be higher if the eth harness has
  2132  		// had a lot of use. The minimum is the gas tip cap.
  2133  		ethRefundFees := int64(dexeth.RefundGas(0 /*version*/)) * dexeth.MinGasTipCap
  2134  
  2135  		msgTx := wire.NewMsgTx(0)
  2136  		prevOut := wire.NewOutPoint(&chainhash.Hash{}, 0)
  2137  		txIn := wire.NewTxIn(prevOut, []byte{}, nil)
  2138  		msgTx.AddTxIn(txIn)
  2139  		size := dexbtc.MsgTxVBytes(msgTx) //wut? btc only
  2140  		// tx fee is 1sat/vByte on simnet. utxoRefundFees is 293 sats.
  2141  		// TODO: even simnet rate on other assets like dgb and doge isn't 1...
  2142  		utxoRefundFees := int64(size + dexbtc.RefundSigScriptSize + dexbtc.P2PKHOutputSize)
  2143  		// TODO: segwit proper fee rate, but need to get segwit flag:
  2144  		// size := btc.calcTxSize(msgTx)
  2145  		// if btc.segwit {
  2146  		// 	witnessVBytes := uint64((dexbtc.RefundSigScriptSize + 2 + 3) / 4)
  2147  		// 	size += witnessVBytes + dexbtc.P2WPKHOutputSize
  2148  		// } else {
  2149  		// 	size += dexbtc.RefundSigScriptSize + dexbtc.P2PKHOutputSize
  2150  		// }
  2151  		if ord.Sell {
  2152  			if accountBIPs[s.base.id] {
  2153  				baseFees = ethRefundFees
  2154  			} else {
  2155  				baseFees = utxoRefundFees
  2156  			}
  2157  		} else {
  2158  			if accountBIPs[s.quote.id] {
  2159  				quoteFees = ethRefundFees
  2160  			} else {
  2161  				quoteFees = utxoRefundFees
  2162  			}
  2163  		}
  2164  	}
  2165  
  2166  	defer func() {
  2167  		// Clear after assertion so that the next assertion is only performed
  2168  		// if the expected balance changes are explicitly set.
  2169  		client.expectBalanceDiffs = nil
  2170  	}()
  2171  	prevBalances := client.balances
  2172  	err = s.updateBalances(client)
  2173  	if err != nil {
  2174  		return err
  2175  	}
  2176  
  2177  	checkDiff := func(a *assetConfig, expDiff, fees int64) error {
  2178  		// actual diff will likely be less than expected because of tx fees
  2179  		// TODO: account for actual fee(s) or use a more realistic fee estimate.
  2180  		expVal := expDiff
  2181  		if !a.isToken {
  2182  			expVal -= fees
  2183  		}
  2184  
  2185  		minExpectedDiff, maxExpectedDiff := int64(float64(expVal)*0.95), int64(float64(expVal)*1.05)
  2186  
  2187  		// diffs can be negative
  2188  		if minExpectedDiff > maxExpectedDiff {
  2189  			minExpectedDiff, maxExpectedDiff = maxExpectedDiff, minExpectedDiff
  2190  		}
  2191  
  2192  		balanceDiff := int64(client.balances[a.id]) - int64(prevBalances[a.id])
  2193  		if balanceDiff < minExpectedDiff || balanceDiff > maxExpectedDiff {
  2194  			return fmt.Errorf("%s balance change not in expected range %s - %s, got %s", a.symbol,
  2195  				a.valFmt(minExpectedDiff), a.valFmt(maxExpectedDiff), a.valFmt(balanceDiff))
  2196  		}
  2197  		client.log.Infof("%s balance change %s is in expected range of %s - %s.", a.symbol,
  2198  			a.valFmt(balanceDiff), a.valFmt(minExpectedDiff), a.valFmt(maxExpectedDiff))
  2199  		return nil
  2200  	}
  2201  
  2202  	if err := checkDiff(s.base, client.expectBalanceDiffs[s.base.id], baseFees); err != nil {
  2203  		return err
  2204  	}
  2205  	return checkDiff(s.quote, client.expectBalanceDiffs[s.quote.id], quoteFees)
  2206  }
  2207  
  2208  func (client *simulationClient) dc() *dexConnection {
  2209  	client.core.connMtx.RLock()
  2210  	defer client.core.connMtx.RUnlock()
  2211  	return client.core.conns[dexHost]
  2212  }
  2213  
  2214  func (client *simulationClient) findOrder(orderID string) (*trackedTrade, error) {
  2215  	oid, err := order.IDFromHex(orderID)
  2216  	if err != nil {
  2217  		return nil, fmt.Errorf("error parsing order id %s -> %v", orderID, err)
  2218  	}
  2219  	tracker, _ := client.dc().findOrder(oid)
  2220  	return tracker, nil
  2221  }
  2222  
  2223  func (client *simulationClient) disableWallets() {
  2224  	client.log.Infof("Disabling wallets for client %s.", client.name)
  2225  	client.core.walletMtx.Lock()
  2226  	for _, wallet := range client.core.wallets {
  2227  		wallet.setDisabled(true)
  2228  	}
  2229  	client.core.walletMtx.Unlock()
  2230  }
  2231  
  2232  func (client *simulationClient) enableWallets() {
  2233  	client.log.Infof("Enabling wallets for client %s.", client.name)
  2234  	client.core.walletMtx.Lock()
  2235  	for _, wallet := range client.core.wallets {
  2236  		wallet.setDisabled(false)
  2237  	}
  2238  	client.core.walletMtx.Unlock()
  2239  }
  2240  
  2241  func (client *simulationClient) disconnectWallets() {
  2242  	client.log.Infof("Disconnecting wallets for client %s.", client.name)
  2243  	client.core.walletMtx.Lock()
  2244  	for _, wallet := range client.core.wallets {
  2245  		wallet.Disconnect()
  2246  	}
  2247  	client.core.walletMtx.Unlock()
  2248  }
  2249  
  2250  var _ comms.WsConn = (*tConn)(nil)
  2251  
  2252  type tConn struct {
  2253  	comms.WsConn
  2254  	requestFilter atomic.Value // func(route string) error
  2255  }
  2256  
  2257  func (tc *tConn) Request(msg *msgjson.Message, respHandler func(*msgjson.Message)) error {
  2258  	return tc.RequestWithTimeout(msg, respHandler, time.Minute, func() {})
  2259  }
  2260  
  2261  func (tc *tConn) RequestWithTimeout(msg *msgjson.Message, respHandler func(*msgjson.Message), expireTime time.Duration, expire func()) error {
  2262  	if fi := tc.requestFilter.Load(); fi != nil {
  2263  		if err := fi.(func(string) error)(msg.Route); err != nil {
  2264  			return err
  2265  		}
  2266  	}
  2267  	return tc.WsConn.RequestWithTimeout(msg, respHandler, expireTime, expire)
  2268  }
  2269  
  2270  func init() {
  2271  	if race {
  2272  		sleepFactor = 3
  2273  	}
  2274  }