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

     1  //go:build !harness && !botlive
     2  
     3  package core
     4  
     5  import (
     6  	"bytes"
     7  	"context"
     8  	crand "crypto/rand"
     9  	"crypto/sha256"
    10  	"encoding/hex"
    11  	"errors"
    12  	"fmt"
    13  	"math"
    14  	"math/rand"
    15  	"os"
    16  	"reflect"
    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/comms"
    27  	"decred.org/dcrdex/client/db"
    28  	dbtest "decred.org/dcrdex/client/db/test"
    29  	"decred.org/dcrdex/dex"
    30  	"decred.org/dcrdex/dex/calc"
    31  	"decred.org/dcrdex/dex/encode"
    32  	"decred.org/dcrdex/dex/encrypt"
    33  	"decred.org/dcrdex/dex/msgjson"
    34  	"decred.org/dcrdex/dex/order"
    35  	ordertest "decred.org/dcrdex/dex/order/test"
    36  	"decred.org/dcrdex/dex/wait"
    37  	"decred.org/dcrdex/server/account"
    38  	serverdex "decred.org/dcrdex/server/dex"
    39  	"github.com/decred/dcrd/crypto/blake256"
    40  	"github.com/decred/dcrd/dcrec/secp256k1/v4"
    41  	"golang.org/x/text/language"
    42  	"golang.org/x/text/message"
    43  )
    44  
    45  func init() {
    46  	asset.Register(tUTXOAssetA.ID, &tDriver{
    47  		decodedCoinID: tUTXOAssetA.Symbol + "-coin",
    48  		winfo:         tWalletInfo,
    49  	})
    50  	asset.Register(tUTXOAssetB.ID, &tCreator{
    51  		tDriver: &tDriver{
    52  			decodedCoinID: tUTXOAssetB.Symbol + "-coin",
    53  			winfo:         tWalletInfo,
    54  		},
    55  	})
    56  	asset.Register(tACCTAsset.ID, &tCreator{
    57  		tDriver: &tDriver{
    58  			decodedCoinID: tACCTAsset.Symbol + "-coin",
    59  			winfo:         tWalletInfo,
    60  		},
    61  	})
    62  	rand.Seed(time.Now().UnixNano())
    63  }
    64  
    65  var (
    66  	tCtx           context.Context
    67  	dcrBtcLotSize  uint64 = 1e7
    68  	dcrBtcRateStep uint64 = 10
    69  	tUTXOAssetA           = &dex.Asset{
    70  		ID:         42,
    71  		Symbol:     "dcr",
    72  		Version:    0, // match the stubbed (*TXCWallet).Info result
    73  		MaxFeeRate: 10,
    74  		SwapConf:   1,
    75  	}
    76  	tSwapSizeA uint64 = 251
    77  
    78  	tUTXOAssetB = &dex.Asset{
    79  		ID:         0,
    80  		Symbol:     "btc",
    81  		Version:    0, // match the stubbed (*TXCWallet).Info result
    82  		MaxFeeRate: 2,
    83  		SwapConf:   1,
    84  	}
    85  	tSwapSizeB uint64 = 225
    86  
    87  	tACCTAsset = &dex.Asset{
    88  		ID:         60,
    89  		Symbol:     "eth",
    90  		Version:    0, // match the stubbed (*TXCWallet).Info result
    91  		MaxFeeRate: 20,
    92  		SwapConf:   1,
    93  	}
    94  	tDexPriv            *secp256k1.PrivateKey
    95  	tDexKey             *secp256k1.PublicKey
    96  	tPW                        = []byte("dexpw")
    97  	wPW                        = []byte("walletpw")
    98  	tDexHost                   = "somedex.tld:7232"
    99  	tDcrBtcMktName             = "dcr_btc"
   100  	tBtcEthMktName             = "btc_eth"
   101  	tErr                       = fmt.Errorf("test error")
   102  	tFee                uint64 = 1e8
   103  	tFeeAsset           uint32 = 42
   104  	tUnparseableHost           = string([]byte{0x7f})
   105  	tSwapFeesPaid       uint64 = 500
   106  	tRedemptionFeesPaid uint64 = 350
   107  	tLogger                    = dex.StdOutLogger("TCORE", dex.LevelInfo)
   108  	tMaxFeeRate         uint64 = 10
   109  	tWalletInfo                = &asset.WalletInfo{
   110  		SupportedVersions: []uint32{0},
   111  		UnitInfo: dex.UnitInfo{
   112  			Conventional: dex.Denomination{
   113  				ConversionFactor: 1e8,
   114  			},
   115  		},
   116  		AvailableWallets: []*asset.WalletDefinition{{
   117  			Type: "type",
   118  		}},
   119  	}
   120  	dcrBondAsset = &msgjson.BondAsset{ID: 42, Amt: tFee, Confs: 1}
   121  )
   122  
   123  type tMsg = *msgjson.Message
   124  type msgFunc = func(*msgjson.Message)
   125  
   126  func uncovertAssetInfo(ai *dex.Asset) *msgjson.Asset {
   127  	return &msgjson.Asset{
   128  		Symbol:     ai.Symbol,
   129  		ID:         ai.ID,
   130  		Version:    ai.Version,
   131  		MaxFeeRate: ai.MaxFeeRate,
   132  		SwapConf:   uint16(ai.SwapConf),
   133  	}
   134  }
   135  
   136  func makeAcker(serializer func(msg *msgjson.Message) msgjson.Signable) func(msg *msgjson.Message, f msgFunc) error {
   137  	return func(msg *msgjson.Message, f msgFunc) error {
   138  		signable := serializer(msg)
   139  		sigMsg := signable.Serialize()
   140  		sig := signMsg(tDexPriv, sigMsg)
   141  		ack := &msgjson.Acknowledgement{
   142  			Sig: sig,
   143  		}
   144  		resp, _ := msgjson.NewResponse(msg.ID, ack, nil)
   145  		f(resp)
   146  		return nil
   147  	}
   148  }
   149  
   150  var (
   151  	invalidAcker = func(msg *msgjson.Message, f msgFunc) error {
   152  		resp, _ := msgjson.NewResponse(msg.ID, msg, nil)
   153  		f(resp)
   154  		return nil
   155  	}
   156  	initAcker = makeAcker(func(msg *msgjson.Message) msgjson.Signable {
   157  		init := new(msgjson.Init)
   158  		msg.Unmarshal(init)
   159  		return init
   160  	})
   161  	redeemAcker = makeAcker(func(msg *msgjson.Message) msgjson.Signable {
   162  		redeem := new(msgjson.Redeem)
   163  		msg.Unmarshal(redeem)
   164  		return redeem
   165  	})
   166  )
   167  
   168  type TWebsocket struct {
   169  	mtx            sync.RWMutex
   170  	id             uint64
   171  	sendErr        error
   172  	sendMsgErrChan chan *msgjson.Error
   173  	reqErr         error
   174  	connectErr     error
   175  	msgs           <-chan *msgjson.Message
   176  	// handlers simulates a peer (server) response for request, and handles the
   177  	// response with the msgFunc.
   178  	handlers       map[string][]func(*msgjson.Message, msgFunc) error
   179  	submittedBond  *msgjson.PostBond
   180  	liveBondExpiry uint64
   181  }
   182  
   183  func newTWebsocket() *TWebsocket {
   184  	return &TWebsocket{
   185  		msgs:     make(<-chan *msgjson.Message),
   186  		handlers: make(map[string][]func(*msgjson.Message, msgFunc) error),
   187  	}
   188  }
   189  
   190  func tNewAccount(crypter *tCrypter) *dexAccount {
   191  	privKey, _ := secp256k1.GeneratePrivateKey()
   192  	encKey, err := crypter.Encrypt(privKey.Serialize())
   193  	if err != nil {
   194  		panic(err)
   195  	}
   196  	return &dexAccount{
   197  		host:      tDexHost,
   198  		encKey:    encKey,
   199  		dexPubKey: tDexKey,
   200  		privKey:   privKey,
   201  		id:        account.NewID(privKey.PubKey().SerializeCompressed()),
   202  		// feeAssetID is 0 (btc)
   203  		// tier, bonds, etc. set on auth
   204  		pendingBondsConfs: make(map[string]uint32),
   205  		rep:               account.Reputation{BondedTier: 1}, // not suspended by default
   206  	}
   207  }
   208  
   209  func testDexConnection(ctx context.Context, crypter *tCrypter) (*dexConnection, *TWebsocket, *dexAccount) {
   210  	conn := newTWebsocket()
   211  	connMaster := dex.NewConnectionMaster(conn)
   212  	connMaster.Connect(ctx)
   213  	acct := tNewAccount(crypter)
   214  	return &dexConnection{
   215  		WsConn:     conn,
   216  		log:        tLogger,
   217  		connMaster: connMaster,
   218  		ticker:     newDexTicker(time.Millisecond * 1000 / 3),
   219  		acct:       acct,
   220  		assets: map[uint32]*dex.Asset{
   221  			tUTXOAssetA.ID: tUTXOAssetA,
   222  			tUTXOAssetB.ID: tUTXOAssetB,
   223  			tACCTAsset.ID:  tACCTAsset,
   224  		},
   225  		books: make(map[string]*bookie),
   226  		cfg: &msgjson.ConfigResult{
   227  			APIVersion:       serverdex.V1APIVersion,
   228  			DEXPubKey:        acct.dexPubKey.SerializeCompressed(),
   229  			CancelMax:        0.8,
   230  			BroadcastTimeout: 1000, // 1000 ms for faster expiration, but ticker fires fast
   231  			Assets: []*msgjson.Asset{
   232  				uncovertAssetInfo(tUTXOAssetA),
   233  				uncovertAssetInfo(tUTXOAssetB),
   234  				uncovertAssetInfo(tACCTAsset),
   235  			},
   236  			Markets: []*msgjson.Market{
   237  				{
   238  					Name:            tDcrBtcMktName,
   239  					Base:            tUTXOAssetA.ID,
   240  					Quote:           tUTXOAssetB.ID,
   241  					LotSize:         dcrBtcLotSize,
   242  					ParcelSize:      1,
   243  					RateStep:        dcrBtcRateStep,
   244  					EpochLen:        60000,
   245  					MarketBuyBuffer: 1.1,
   246  					MarketStatus: msgjson.MarketStatus{
   247  						StartEpoch: 12, // since the stone age
   248  						FinalEpoch: 0,  // no scheduled suspend
   249  						// Persist:   nil,
   250  					},
   251  				},
   252  				{
   253  					Name:            tBtcEthMktName,
   254  					Base:            tUTXOAssetB.ID,
   255  					Quote:           tACCTAsset.ID,
   256  					LotSize:         dcrBtcLotSize,
   257  					RateStep:        dcrBtcRateStep,
   258  					EpochLen:        60000,
   259  					MarketBuyBuffer: 1.1,
   260  					MarketStatus: msgjson.MarketStatus{
   261  						StartEpoch: 12,
   262  						FinalEpoch: 0,
   263  					},
   264  				},
   265  			},
   266  			BondExpiry: 86400, // >0 make client treat as API v1
   267  			BondAssets: map[string]*msgjson.BondAsset{
   268  				"dcr": dcrBondAsset,
   269  			},
   270  			BinSizes: []string{"1h", "24h"},
   271  		},
   272  		notify:            func(Notification) {},
   273  		trades:            make(map[order.OrderID]*trackedTrade),
   274  		cancels:           make(map[order.OrderID]order.OrderID),
   275  		inFlightOrders:    make(map[uint64]*InFlightOrder),
   276  		epoch:             map[string]uint64{tDcrBtcMktName: 0},
   277  		resolvedEpoch:     map[string]uint64{tDcrBtcMktName: 0},
   278  		apiVer:            serverdex.PreAPIVersion,
   279  		connectionStatus:  uint32(comms.Connected),
   280  		reportingConnects: 1,
   281  		spots:             make(map[string]*msgjson.Spot),
   282  	}, conn, acct
   283  }
   284  
   285  func (conn *TWebsocket) queueResponse(route string, handler func(*msgjson.Message, msgFunc) error) {
   286  	conn.mtx.Lock()
   287  	defer conn.mtx.Unlock()
   288  	handlers := conn.handlers[route]
   289  	if handlers == nil {
   290  		handlers = make([]func(*msgjson.Message, msgFunc) error, 0, 1)
   291  	}
   292  	conn.handlers[route] = append(handlers, handler) // NOTE: handler is called by RequestWithTimeout
   293  }
   294  
   295  func (conn *TWebsocket) NextID() uint64 {
   296  	conn.mtx.Lock()
   297  	defer conn.mtx.Unlock()
   298  	conn.id++
   299  	return conn.id
   300  }
   301  func (conn *TWebsocket) Send(msg *msgjson.Message) error {
   302  	if conn.sendMsgErrChan != nil {
   303  		resp, err := msg.Response()
   304  		if err != nil {
   305  			return err
   306  		}
   307  		if resp.Error != nil {
   308  			conn.sendMsgErrChan <- resp.Error
   309  			return nil // the response was sent successfully
   310  		}
   311  	}
   312  
   313  	return conn.sendErr
   314  }
   315  
   316  func (conn *TWebsocket) SendRaw([]byte) error {
   317  	return conn.sendErr
   318  }
   319  func (conn *TWebsocket) Request(msg *msgjson.Message, f msgFunc) error {
   320  	return conn.RequestWithTimeout(msg, f, 0, func() {})
   321  }
   322  func (conn *TWebsocket) RequestRaw(msgID uint64, rawMsg []byte, respHandler func(*msgjson.Message)) error {
   323  	return nil
   324  }
   325  func (conn *TWebsocket) RequestWithTimeout(msg *msgjson.Message, f func(*msgjson.Message), _ time.Duration, _ func()) error {
   326  	if conn.reqErr != nil {
   327  		return conn.reqErr
   328  	}
   329  	conn.mtx.Lock()
   330  	defer conn.mtx.Unlock()
   331  	handlers := conn.handlers[msg.Route]
   332  	if len(handlers) > 0 {
   333  		handler := handlers[0]
   334  		conn.handlers[msg.Route] = handlers[1:]
   335  		return handler(msg, f)
   336  	}
   337  	return fmt.Errorf("no handler for route %q", msg.Route)
   338  }
   339  func (conn *TWebsocket) MessageSource() <-chan *msgjson.Message { return conn.msgs } // use when Core.listen is running
   340  func (conn *TWebsocket) IsDown() bool {
   341  	return false
   342  }
   343  func (conn *TWebsocket) Connect(context.Context) (*sync.WaitGroup, error) {
   344  	// NOTE: tCore's wsConstructor just returns a reused conn, so we can't close
   345  	// conn.msgs on ctx cancel. See the wsConstructor definition in newTestRig.
   346  	// Consider reworking the tests (TODO).
   347  	return &sync.WaitGroup{}, conn.connectErr
   348  }
   349  
   350  func (conn *TWebsocket) UpdateURL(string) {}
   351  
   352  type TDB struct {
   353  	updateWalletErr          error
   354  	acct                     *db.AccountInfo
   355  	acctErr                  error
   356  	createAccountErr         error
   357  	addBondErr               error
   358  	updateOrderErr           error
   359  	activeDEXOrders          []*db.MetaOrder
   360  	matchesForOID            []*db.MetaMatch
   361  	matchesForOIDErr         error
   362  	updateMatchChan          chan order.MatchStatus
   363  	activeMatchOIDs          []order.OrderID
   364  	activeMatchOIDSErr       error
   365  	lastStatusID             order.OrderID
   366  	lastStatus               order.OrderStatus
   367  	wallet                   *db.Wallet
   368  	walletErr                error
   369  	setWalletPwErr           error
   370  	orderOrders              map[order.OrderID]*db.MetaOrder
   371  	orderErr                 error
   372  	linkedFromID             order.OrderID
   373  	linkedToID               order.OrderID
   374  	existValues              map[string]bool
   375  	accountProofErr          error
   376  	verifyCreateAccount      bool
   377  	verifyUpdateAccountInfo  bool
   378  	disabledHost             *string
   379  	disableAccountErr        error
   380  	creds                    *db.PrimaryCredentials
   381  	setCredsErr              error
   382  	legacyKeyErr             error
   383  	recryptErr               error
   384  	deleteInactiveOrdersErr  error
   385  	archivedOrders           int
   386  	deleteInactiveMatchesErr error
   387  	archivedMatches          int
   388  	updateAccountInfoErr     error
   389  }
   390  
   391  func (tdb *TDB) Run(context.Context) {}
   392  
   393  func (tdb *TDB) ListAccounts() ([]string, error) {
   394  	return nil, nil
   395  }
   396  
   397  func (tdb *TDB) Accounts() ([]*db.AccountInfo, error) {
   398  	return []*db.AccountInfo{}, nil
   399  }
   400  
   401  func (tdb *TDB) Account(url string) (*db.AccountInfo, error) {
   402  	return tdb.acct, tdb.acctErr
   403  }
   404  
   405  func (tdb *TDB) CreateAccount(ai *db.AccountInfo) error {
   406  	tdb.verifyCreateAccount = true
   407  	tdb.acct = ai
   408  	return tdb.createAccountErr
   409  }
   410  
   411  func (tdb *TDB) NextBondKeyIndex(assetID uint32) (uint32, error) {
   412  	return 0, nil
   413  }
   414  
   415  func (tdb *TDB) AddBond(host string, bond *db.Bond) error {
   416  	return tdb.addBondErr
   417  }
   418  
   419  func (tdb *TDB) ConfirmBond(host string, assetID uint32, bondCoinID []byte) error {
   420  	return nil
   421  }
   422  func (tdb *TDB) BondRefunded(host string, assetID uint32, bondCoinID []byte) error {
   423  	return nil
   424  }
   425  
   426  func (tdb *TDB) ToggleAccountStatus(host string, disable bool) error {
   427  	if disable {
   428  		tdb.disabledHost = &host
   429  	} else {
   430  		tdb.disabledHost = nil
   431  	}
   432  	return tdb.disableAccountErr
   433  }
   434  
   435  func (tdb *TDB) UpdateAccountInfo(ai *db.AccountInfo) error {
   436  	tdb.verifyUpdateAccountInfo = true
   437  	tdb.acct = ai
   438  	return tdb.updateAccountInfoErr
   439  }
   440  
   441  func (tdb *TDB) UpdateOrder(m *db.MetaOrder) error {
   442  	return tdb.updateOrderErr
   443  }
   444  
   445  func (tdb *TDB) ActiveDEXOrders(dex string) ([]*db.MetaOrder, error) {
   446  	return tdb.activeDEXOrders, nil
   447  }
   448  
   449  func (tdb *TDB) ActiveOrders() ([]*db.MetaOrder, error) {
   450  	return nil, nil
   451  }
   452  
   453  func (tdb *TDB) AccountOrders(dex string, n int, since uint64) ([]*db.MetaOrder, error) {
   454  	return nil, nil
   455  }
   456  
   457  func (tdb *TDB) Order(oid order.OrderID) (*db.MetaOrder, error) {
   458  	if tdb.orderErr != nil {
   459  		return nil, tdb.orderErr
   460  	}
   461  	return tdb.orderOrders[oid], nil
   462  }
   463  
   464  func (tdb *TDB) Orders(*db.OrderFilter) ([]*db.MetaOrder, error) {
   465  	return nil, nil
   466  }
   467  
   468  func (tdb *TDB) MarketOrders(dex string, base, quote uint32, n int, since uint64) ([]*db.MetaOrder, error) {
   469  	return nil, nil
   470  }
   471  
   472  func (tdb *TDB) UpdateOrderMetaData(order.OrderID, *db.OrderMetaData) error {
   473  	return nil
   474  }
   475  
   476  func (tdb *TDB) UpdateOrderStatus(oid order.OrderID, status order.OrderStatus) error {
   477  	tdb.lastStatusID = oid
   478  	tdb.lastStatus = status
   479  	return nil
   480  }
   481  
   482  func (tdb *TDB) LinkOrder(oid, linkedID order.OrderID) error {
   483  	tdb.linkedFromID = oid
   484  	tdb.linkedToID = linkedID
   485  	return nil
   486  }
   487  
   488  func (tdb *TDB) UpdateMatch(m *db.MetaMatch) error {
   489  	if tdb.updateMatchChan != nil {
   490  		tdb.updateMatchChan <- m.Status
   491  	}
   492  	return nil
   493  }
   494  
   495  func (tdb *TDB) ActiveMatches() ([]*db.MetaMatch, error) {
   496  	return nil, nil
   497  }
   498  
   499  func (tdb *TDB) MatchesForOrder(oid order.OrderID, excludeCancels bool) ([]*db.MetaMatch, error) {
   500  	return tdb.matchesForOID, tdb.matchesForOIDErr
   501  }
   502  
   503  func (tdb *TDB) DEXOrdersWithActiveMatches(dex string) ([]order.OrderID, error) {
   504  	return tdb.activeMatchOIDs, tdb.activeMatchOIDSErr
   505  }
   506  
   507  func (tdb *TDB) UpdateWallet(wallet *db.Wallet) error {
   508  	tdb.wallet = wallet
   509  	return tdb.updateWalletErr
   510  }
   511  
   512  func (tdb *TDB) SetWalletPassword(wid []byte, newPW []byte) error {
   513  	return tdb.setWalletPwErr
   514  }
   515  
   516  func (tdb *TDB) UpdateBalance(wid []byte, balance *db.Balance) error {
   517  	return nil
   518  }
   519  
   520  func (tdb *TDB) UpdateWalletStatus(wid []byte, disable bool) error {
   521  	return nil
   522  }
   523  
   524  func (tdb *TDB) Wallets() ([]*db.Wallet, error) {
   525  	return nil, nil
   526  }
   527  
   528  func (tdb *TDB) Wallet([]byte) (*db.Wallet, error) {
   529  	return tdb.wallet, tdb.walletErr
   530  }
   531  
   532  func (tdb *TDB) SaveNotification(*db.Notification) error            { return nil }
   533  func (tdb *TDB) BackupTo(dst string, overwrite, compact bool) error { return nil }
   534  func (tdb *TDB) NotificationsN(int) ([]*db.Notification, error)     { return nil, nil }
   535  func (tdb *TDB) SavePokes([]*db.Notification) error                 { return nil }
   536  func (tdb *TDB) LoadPokes() ([]*db.Notification, error)             { return nil, nil }
   537  
   538  func (tdb *TDB) SetPrimaryCredentials(creds *db.PrimaryCredentials) error {
   539  	if tdb.setCredsErr != nil {
   540  		return tdb.setCredsErr
   541  	}
   542  	tdb.creds = creds
   543  	return nil
   544  }
   545  
   546  func (tdb *TDB) DeleteInactiveOrders(ctx context.Context, olderThan *time.Time, perBatchFn func(ords *db.MetaOrder) error) (int, error) {
   547  	return tdb.archivedOrders, tdb.deleteInactiveOrdersErr
   548  }
   549  
   550  func (tdb *TDB) DeleteInactiveMatches(ctx context.Context, olderThan *time.Time, perBatchFn func(mtchs *db.MetaMatch, isSell bool) error) (int, error) {
   551  	return tdb.archivedMatches, tdb.deleteInactiveMatchesErr
   552  }
   553  
   554  func (tdb *TDB) PrimaryCredentials() (*db.PrimaryCredentials, error) {
   555  	return tdb.creds, nil
   556  }
   557  func (tdb *TDB) SetSeedGenerationTime(time uint64) error {
   558  	return nil
   559  }
   560  func (tdb *TDB) SeedGenerationTime() (uint64, error) {
   561  	return 0, nil
   562  }
   563  func (tdb *TDB) DisabledRateSources() ([]string, error) {
   564  	return nil, nil
   565  }
   566  func (tdb *TDB) SaveDisabledRateSources(disableSources []string) error {
   567  	return nil
   568  }
   569  func (tdb *TDB) Recrypt(creds *db.PrimaryCredentials, oldCrypter, newCrypter encrypt.Crypter) (
   570  	walletUpdates map[uint32][]byte, acctUpdates map[string][]byte, err error) {
   571  
   572  	if tdb.recryptErr != nil {
   573  		return nil, nil, tdb.recryptErr
   574  	}
   575  
   576  	return nil, nil, nil
   577  }
   578  
   579  func (tdb *TDB) Backup() error {
   580  	return nil
   581  }
   582  
   583  func (tdb *TDB) AckNotification(id []byte) error { return nil }
   584  
   585  func (tdb *TDB) SetLanguage(lang string) error {
   586  	return nil
   587  }
   588  func (tdb *TDB) Language() (string, error) {
   589  	return "en-US", nil
   590  }
   591  
   592  type tCoin struct {
   593  	id []byte
   594  
   595  	val uint64
   596  }
   597  
   598  func (c *tCoin) ID() dex.Bytes {
   599  	return c.id
   600  }
   601  
   602  func (c *tCoin) TxID() string {
   603  	return ""
   604  }
   605  
   606  func (c *tCoin) String() string {
   607  	return hex.EncodeToString(c.id)
   608  }
   609  
   610  func (c *tCoin) Value() uint64 {
   611  	return c.val
   612  }
   613  
   614  type tReceipt struct {
   615  	coin       *tCoin
   616  	contract   []byte
   617  	expiration time.Time
   618  }
   619  
   620  func (r *tReceipt) Coin() asset.Coin {
   621  	return r.coin
   622  }
   623  
   624  func (r *tReceipt) Contract() dex.Bytes {
   625  	return r.contract
   626  }
   627  
   628  func (r *tReceipt) Expiration() time.Time {
   629  	return r.expiration
   630  }
   631  
   632  func (r *tReceipt) String() string {
   633  	return r.coin.String()
   634  }
   635  
   636  func (r *tReceipt) SignedRefund() dex.Bytes {
   637  	return nil
   638  }
   639  
   640  type TXCWallet struct {
   641  	swapSize            uint64
   642  	sendFeeSuggestion   uint64
   643  	sendCoin            *tCoin
   644  	sendErr             error
   645  	addrErr             error
   646  	signCoinErr         error
   647  	lastSwaps           []*asset.Swaps
   648  	lastRedeems         []*asset.RedeemForm
   649  	swapReceipts        []asset.Receipt
   650  	swapCounter         int
   651  	swapErr             error
   652  	auditInfo           *asset.AuditInfo
   653  	auditErr            error
   654  	auditChan           chan struct{}
   655  	refundCoin          dex.Bytes
   656  	refundErr           error
   657  	refundFeeSuggestion uint64
   658  	redeemCoins         []dex.Bytes
   659  	redeemCounter       int
   660  	redeemFeeSuggestion uint64
   661  	redeemErr           error
   662  	redeemErrChan       chan error
   663  	badSecret           bool
   664  	fundedVal           uint64
   665  	fundedSwaps         uint64
   666  	connectErr          error
   667  	unlockErr           error
   668  	balErr              error
   669  	bal                 *asset.Balance
   670  	fundingMtx          sync.RWMutex
   671  	fundingCoins        asset.Coins
   672  	fundRedeemScripts   []dex.Bytes
   673  	returnedCoins       asset.Coins
   674  	fundingCoinErr      error
   675  	lockErr             error
   676  	locked              bool
   677  	changeCoin          *tCoin
   678  	syncStatus          func() (bool, float32, error)
   679  	confsMtx            sync.RWMutex
   680  	confs               map[string]uint32
   681  	confsErr            map[string]error
   682  	preSwapForm         *asset.PreSwapForm
   683  	preSwap             *asset.PreSwap
   684  	preRedeemForm       *asset.PreRedeemForm
   685  	preRedeem           *asset.PreRedeem
   686  	ownsAddress         bool
   687  	ownsAddressErr      error
   688  	pubKeys             []dex.Bytes
   689  	sigs                []dex.Bytes
   690  	feeCoin             []byte
   691  	makeRegFeeTxErr     error
   692  	feeCoinSent         []byte
   693  	sendTxnErr          error
   694  	contractExpired     bool
   695  	contractLockTime    time.Time
   696  	accelerationParams  *struct {
   697  		swapCoins                 []dex.Bytes
   698  		accelerationCoins         []dex.Bytes
   699  		changeCoin                dex.Bytes
   700  		feeSuggestion             uint64
   701  		newFeeRate                uint64
   702  		requiredForRemainingSwaps uint64
   703  	}
   704  	newAccelerationTxID         string
   705  	newChangeCoinID             *dex.Bytes
   706  	preAccelerateSwapRate       uint64
   707  	preAccelerateSuggestedRange asset.XYRange
   708  	accelerationEstimate        uint64
   709  	accelerateOrderErr          error
   710  	info                        *asset.WalletInfo
   711  	bondTxCoinID                []byte
   712  	refundBondCoin              asset.Coin
   713  	refundBondErr               error
   714  	makeBondTxErr               error
   715  	reserves                    atomic.Uint64
   716  	findBond                    *asset.BondDetails
   717  	findBondErr                 error
   718  
   719  	confirmRedemptionResult *asset.ConfirmRedemptionStatus
   720  	confirmRedemptionErr    error
   721  	confirmRedemptionCalled bool
   722  
   723  	estFee    uint64
   724  	estFeeErr error
   725  	validAddr bool
   726  
   727  	returnedAddr      string
   728  	returnedContracts [][]byte
   729  }
   730  
   731  var _ asset.Accelerator = (*TXCWallet)(nil)
   732  var _ asset.Withdrawer = (*TXCWallet)(nil)
   733  
   734  func newTWallet(assetID uint32) (*xcWallet, *TXCWallet) {
   735  	w := &TXCWallet{
   736  		changeCoin:       &tCoin{id: encode.RandomBytes(36)},
   737  		syncStatus:       func() (synced bool, progress float32, err error) { return true, 1, nil },
   738  		confs:            make(map[string]uint32),
   739  		confsErr:         make(map[string]error),
   740  		ownsAddress:      true,
   741  		contractLockTime: time.Now().Add(time.Minute),
   742  		lastSwaps:        make([]*asset.Swaps, 0),
   743  		lastRedeems:      make([]*asset.RedeemForm, 0),
   744  		info: &asset.WalletInfo{
   745  			SupportedVersions: []uint32{0},
   746  		},
   747  		bondTxCoinID: encode.RandomBytes(32),
   748  	}
   749  	var broadcasting uint32 = 1
   750  	xcWallet := &xcWallet{
   751  		log:               tLogger,
   752  		supportedVersions: w.info.SupportedVersions,
   753  		Wallet:            w,
   754  		Symbol:            dex.BipIDSymbol(assetID),
   755  		connector:         dex.NewConnectionMaster(w),
   756  		AssetID:           assetID,
   757  		hookedUp:          true,
   758  		dbID:              encode.Uint32Bytes(assetID),
   759  		encPass:           []byte{0x01},
   760  		peerCount:         1,
   761  		syncStatus:        &asset.SyncStatus{Synced: true},
   762  		pw:                tPW,
   763  		traits:            asset.DetermineWalletTraits(w),
   764  		broadcasting:      &broadcasting,
   765  	}
   766  
   767  	return xcWallet, w
   768  }
   769  
   770  func (w *TXCWallet) Info() *asset.WalletInfo {
   771  	return w.info
   772  }
   773  
   774  func (w *TXCWallet) OwnsDepositAddress(address string) (bool, error) {
   775  	return w.ownsAddress, w.ownsAddressErr
   776  }
   777  
   778  func (w *TXCWallet) Connect(ctx context.Context) (*sync.WaitGroup, error) {
   779  	var wg sync.WaitGroup
   780  	wg.Add(1)
   781  	go func() {
   782  		<-ctx.Done()
   783  		wg.Done()
   784  	}()
   785  	return &wg, w.connectErr
   786  }
   787  
   788  func (w *TXCWallet) Balance() (*asset.Balance, error) {
   789  	if w.balErr != nil {
   790  		return nil, w.balErr
   791  	}
   792  	if w.bal == nil {
   793  		w.bal = new(asset.Balance)
   794  	}
   795  	return w.bal, nil
   796  }
   797  
   798  func (w *TXCWallet) ConfirmRedemption(coinID dex.Bytes, redemption *asset.Redemption, feeSuggestion uint64) (*asset.ConfirmRedemptionStatus, error) {
   799  	w.confirmRedemptionCalled = true
   800  	return w.confirmRedemptionResult, w.confirmRedemptionErr
   801  }
   802  
   803  func (w *TXCWallet) FundOrder(ord *asset.Order) (asset.Coins, []dex.Bytes, uint64, error) {
   804  	w.fundedVal = ord.Value
   805  	w.fundedSwaps = ord.MaxSwapCount
   806  	return w.fundingCoins, w.fundRedeemScripts, 0, w.fundingCoinErr
   807  }
   808  
   809  func (w *TXCWallet) MaxOrder(*asset.MaxOrderForm) (*asset.SwapEstimate, error) {
   810  	return nil, nil
   811  }
   812  
   813  func (w *TXCWallet) PreSwap(form *asset.PreSwapForm) (*asset.PreSwap, error) {
   814  	w.preSwapForm = form
   815  	return w.preSwap, nil
   816  }
   817  
   818  func (w *TXCWallet) PreRedeem(form *asset.PreRedeemForm) (*asset.PreRedeem, error) {
   819  	w.preRedeemForm = form
   820  	return w.preRedeem, nil
   821  }
   822  func (w *TXCWallet) RedemptionFees() (uint64, error) { return 0, nil }
   823  
   824  func (w *TXCWallet) ReturnCoins(coins asset.Coins) error {
   825  	w.fundingMtx.Lock()
   826  	defer w.fundingMtx.Unlock()
   827  	w.returnedCoins = coins
   828  	coinInSlice := func(coin asset.Coin) bool {
   829  		for _, c := range coins {
   830  			if bytes.Equal(c.ID(), coin.ID()) {
   831  				return true
   832  			}
   833  		}
   834  		return false
   835  	}
   836  
   837  	for _, c := range w.fundingCoins {
   838  		if coinInSlice(c) {
   839  			continue
   840  		}
   841  		return errors.New("not found")
   842  	}
   843  	return nil
   844  }
   845  
   846  func (w *TXCWallet) FundingCoins([]dex.Bytes) (asset.Coins, error) {
   847  	return w.fundingCoins, w.fundingCoinErr
   848  }
   849  
   850  func (w *TXCWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uint64, error) {
   851  	w.swapCounter++
   852  	w.lastSwaps = append(w.lastSwaps, swaps)
   853  	if w.swapErr != nil {
   854  		return nil, nil, 0, w.swapErr
   855  	}
   856  	return w.swapReceipts, w.changeCoin, tSwapFeesPaid, nil
   857  }
   858  
   859  func (w *TXCWallet) Redeem(form *asset.RedeemForm) ([]dex.Bytes, asset.Coin, uint64, error) {
   860  	w.redeemFeeSuggestion = form.FeeSuggestion
   861  	defer func() {
   862  		if w.redeemErrChan != nil {
   863  			w.redeemErrChan <- w.redeemErr
   864  		}
   865  	}()
   866  	w.lastRedeems = append(w.lastRedeems, form)
   867  	w.redeemCounter++
   868  	if w.redeemErr != nil {
   869  		return nil, nil, 0, w.redeemErr
   870  	}
   871  	return w.redeemCoins, &tCoin{id: []byte{0x0c, 0x0d}}, tRedemptionFeesPaid, nil
   872  }
   873  
   874  func (w *TXCWallet) SignMessage(asset.Coin, dex.Bytes) (pubkeys, sigs []dex.Bytes, err error) {
   875  	return w.pubKeys, w.sigs, w.signCoinErr
   876  }
   877  
   878  func (w *TXCWallet) AuditContract(coinID, contract, txData dex.Bytes, rebroadcast bool) (*asset.AuditInfo, error) {
   879  	defer func() {
   880  		if w.auditChan != nil {
   881  			w.auditChan <- struct{}{}
   882  		}
   883  	}()
   884  	return w.auditInfo, w.auditErr
   885  }
   886  
   887  func (w *TXCWallet) LockTimeExpired(_ context.Context, lockTime time.Time) (bool, error) {
   888  	return w.contractExpired, nil
   889  }
   890  
   891  func (w *TXCWallet) ContractLockTimeExpired(_ context.Context, contract dex.Bytes) (bool, time.Time, error) {
   892  	return w.contractExpired, w.contractLockTime, nil
   893  }
   894  
   895  func (w *TXCWallet) FindRedemption(ctx context.Context, coinID, _ dex.Bytes) (redemptionCoin, secret dex.Bytes, err error) {
   896  	return nil, nil, fmt.Errorf("not mocked")
   897  }
   898  
   899  func (w *TXCWallet) Refund(refundCoin dex.Bytes, refundContract dex.Bytes, feeSuggestion uint64) (dex.Bytes, error) {
   900  	w.refundFeeSuggestion = feeSuggestion
   901  	return w.refundCoin, w.refundErr
   902  }
   903  
   904  func (w *TXCWallet) DepositAddress() (string, error) {
   905  	return "", w.addrErr
   906  }
   907  
   908  func (w *TXCWallet) RedemptionAddress() (string, error) {
   909  	return "", w.addrErr
   910  }
   911  
   912  func (w *TXCWallet) NewAddress() (string, error) {
   913  	return "", w.addrErr
   914  }
   915  
   916  func (w *TXCWallet) AddressUsed(addr string) (bool, error) {
   917  	return false, nil
   918  }
   919  
   920  func (w *TXCWallet) Unlock(pw []byte) error {
   921  	return w.unlockErr
   922  }
   923  
   924  func (w *TXCWallet) Lock() error {
   925  	return w.lockErr
   926  }
   927  
   928  func (w *TXCWallet) Locked() bool {
   929  	return w.locked
   930  }
   931  
   932  func (w *TXCWallet) ConfirmTime(id dex.Bytes, nConfs uint32) (time.Time, error) {
   933  	return time.Time{}, nil
   934  }
   935  
   936  func (w *TXCWallet) Send(address string, value, feeSuggestion uint64) (asset.Coin, error) {
   937  	w.sendFeeSuggestion = feeSuggestion
   938  	w.sendCoin.val = value
   939  	return w.sendCoin, w.sendErr
   940  }
   941  
   942  func (w *TXCWallet) SendTransaction(rawTx []byte) ([]byte, error) {
   943  	return w.feeCoinSent, w.sendTxnErr
   944  }
   945  
   946  func (w *TXCWallet) Withdraw(address string, value, feeSuggestion uint64) (asset.Coin, error) {
   947  	w.sendFeeSuggestion = feeSuggestion
   948  	return w.sendCoin, w.sendErr
   949  }
   950  
   951  func (w *TXCWallet) ValidateAddress(address string) bool {
   952  	return w.validAddr
   953  }
   954  
   955  func (w *TXCWallet) EstimateSendTxFee(address string, value, feeRate uint64, subtract, maxWithdraw bool) (fee uint64, isValidAddress bool, err error) {
   956  	return w.estFee, true, w.estFeeErr
   957  }
   958  
   959  func (w *TXCWallet) ValidateSecret(secret, secretHash []byte) bool {
   960  	return !w.badSecret
   961  }
   962  
   963  func (w *TXCWallet) SyncStatus() (*asset.SyncStatus, error) {
   964  	synced, progress, err := w.syncStatus()
   965  	if err != nil {
   966  		return nil, err
   967  	}
   968  	blocks := uint64(math.Round(float64(progress) * 100))
   969  	return &asset.SyncStatus{Synced: synced, TargetHeight: blocks, Blocks: blocks}, nil
   970  }
   971  
   972  func (w *TXCWallet) setConfs(coinID dex.Bytes, confs uint32, err error) {
   973  	id := coinID.String()
   974  	w.confsMtx.Lock()
   975  	w.confs[id] = confs
   976  	w.confsErr[id] = err
   977  	w.confsMtx.Unlock()
   978  }
   979  
   980  func (w *TXCWallet) tConfirmations(_ context.Context, coinID dex.Bytes) (uint32, error) {
   981  	id := coinID.String()
   982  	w.confsMtx.RLock()
   983  	defer w.confsMtx.RUnlock()
   984  	return w.confs[id], w.confsErr[id]
   985  }
   986  
   987  func (w *TXCWallet) SwapConfirmations(ctx context.Context, coinID dex.Bytes, contract dex.Bytes, matchTime time.Time) (uint32, bool, error) {
   988  	confs, err := w.tConfirmations(ctx, coinID)
   989  	return confs, false, err
   990  }
   991  
   992  func (w *TXCWallet) RegFeeConfirmations(ctx context.Context, coinID dex.Bytes) (uint32, error) {
   993  	return w.tConfirmations(ctx, coinID)
   994  }
   995  
   996  func (w *TXCWallet) FeesForRemainingSwaps(n, feeRate uint64) uint64 {
   997  	return n * feeRate * w.swapSize
   998  }
   999  func (w *TXCWallet) AccelerateOrder(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (asset.Coin, string, error) {
  1000  	if w.accelerateOrderErr != nil {
  1001  		return nil, "", w.accelerateOrderErr
  1002  	}
  1003  
  1004  	w.accelerationParams = &struct {
  1005  		swapCoins                 []dex.Bytes
  1006  		accelerationCoins         []dex.Bytes
  1007  		changeCoin                dex.Bytes
  1008  		feeSuggestion             uint64
  1009  		newFeeRate                uint64
  1010  		requiredForRemainingSwaps uint64
  1011  	}{
  1012  		swapCoins:                 swapCoins,
  1013  		accelerationCoins:         accelerationCoins,
  1014  		changeCoin:                changeCoin,
  1015  		requiredForRemainingSwaps: requiredForRemainingSwaps,
  1016  		newFeeRate:                newFeeRate,
  1017  	}
  1018  	if w.newChangeCoinID != nil {
  1019  		return &tCoin{id: *w.newChangeCoinID}, w.newAccelerationTxID, nil
  1020  	}
  1021  
  1022  	return nil, w.newAccelerationTxID, nil
  1023  }
  1024  
  1025  func (w *TXCWallet) PreAccelerate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, feeSuggestion uint64) (uint64, *asset.XYRange, *asset.EarlyAcceleration, error) {
  1026  	if w.accelerateOrderErr != nil {
  1027  		return 0, nil, nil, w.accelerateOrderErr
  1028  	}
  1029  
  1030  	w.accelerationParams = &struct {
  1031  		swapCoins                 []dex.Bytes
  1032  		accelerationCoins         []dex.Bytes
  1033  		changeCoin                dex.Bytes
  1034  		feeSuggestion             uint64
  1035  		newFeeRate                uint64
  1036  		requiredForRemainingSwaps uint64
  1037  	}{
  1038  		swapCoins:                 swapCoins,
  1039  		accelerationCoins:         accelerationCoins,
  1040  		changeCoin:                changeCoin,
  1041  		requiredForRemainingSwaps: requiredForRemainingSwaps,
  1042  		feeSuggestion:             feeSuggestion,
  1043  	}
  1044  
  1045  	return w.preAccelerateSwapRate, &w.preAccelerateSuggestedRange, nil, nil
  1046  }
  1047  
  1048  func (w *TXCWallet) SingleLotSwapRefundFees(version uint32, feeRate uint64, useSafeTxSize bool) (uint64, uint64, error) {
  1049  	return 0, 0, nil
  1050  }
  1051  
  1052  func (w *TXCWallet) SingleLotRedeemFees(version uint32, feeRate uint64) (uint64, error) {
  1053  	return 0, nil
  1054  }
  1055  
  1056  func (w *TXCWallet) StandardSendFee(uint64) uint64 { return 1 }
  1057  
  1058  func (w *TXCWallet) AccelerationEstimate(swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps, newFeeRate uint64) (uint64, error) {
  1059  	if w.accelerateOrderErr != nil {
  1060  		return 0, w.accelerateOrderErr
  1061  	}
  1062  
  1063  	w.accelerationParams = &struct {
  1064  		swapCoins                 []dex.Bytes
  1065  		accelerationCoins         []dex.Bytes
  1066  		changeCoin                dex.Bytes
  1067  		feeSuggestion             uint64
  1068  		newFeeRate                uint64
  1069  		requiredForRemainingSwaps uint64
  1070  	}{
  1071  		swapCoins:                 swapCoins,
  1072  		accelerationCoins:         accelerationCoins,
  1073  		changeCoin:                changeCoin,
  1074  		requiredForRemainingSwaps: requiredForRemainingSwaps,
  1075  		newFeeRate:                newFeeRate,
  1076  	}
  1077  
  1078  	return w.accelerationEstimate, nil
  1079  }
  1080  
  1081  func (w *TXCWallet) ReturnRedemptionAddress(addr string) {
  1082  	w.returnedAddr = addr
  1083  }
  1084  func (w *TXCWallet) ReturnRefundContracts(contracts [][]byte) {
  1085  	w.returnedContracts = contracts
  1086  }
  1087  func (w *TXCWallet) MaxFundingFees(_ uint32, _ uint64, _ map[string]string) uint64 {
  1088  	return 0
  1089  }
  1090  
  1091  func (*TXCWallet) FundMultiOrder(ord *asset.MultiOrder, maxLock uint64) (coins []asset.Coins, redeemScripts [][]dex.Bytes, fundingFees uint64, err error) {
  1092  	return nil, nil, 0, nil
  1093  }
  1094  
  1095  var _ asset.Bonder = (*TXCWallet)(nil)
  1096  
  1097  func (*TXCWallet) BondsFeeBuffer(feeRate uint64) uint64 {
  1098  	return 4 * 1000 * feeRate * 2
  1099  }
  1100  
  1101  func (w *TXCWallet) SetBondReserves(reserves uint64) {
  1102  	w.reserves.Store(reserves)
  1103  }
  1104  
  1105  func (w *TXCWallet) RefundBond(ctx context.Context, ver uint16, coinID, script []byte, amt uint64, privKey *secp256k1.PrivateKey) (asset.Coin, error) {
  1106  	return w.refundBondCoin, w.refundBondErr
  1107  }
  1108  
  1109  func (w *TXCWallet) FindBond(ctx context.Context, coinID []byte, searchUntil time.Time) (bond *asset.BondDetails, err error) {
  1110  	return w.findBond, w.findBondErr
  1111  }
  1112  
  1113  func (w *TXCWallet) MakeBondTx(ver uint16, amt, feeRate uint64, lockTime time.Time, privKey *secp256k1.PrivateKey, acctID []byte) (*asset.Bond, func(), error) {
  1114  	if w.makeBondTxErr != nil {
  1115  		return nil, nil, w.makeBondTxErr
  1116  	}
  1117  	return &asset.Bond{
  1118  		Version: ver,
  1119  		AssetID: dcrBondAsset.ID,
  1120  		Amount:  amt,
  1121  		CoinID:  w.bondTxCoinID,
  1122  	}, func() {}, nil
  1123  }
  1124  
  1125  func (w *TXCWallet) WalletTransaction(context.Context, dex.Bytes) (*asset.WalletTransaction, error) {
  1126  	return nil, nil
  1127  }
  1128  
  1129  type TAccountLocker struct {
  1130  	*TXCWallet
  1131  	reserveNRedemptions    uint64
  1132  	reserveNRedemptionsErr error
  1133  	reReserveRedemptionErr error
  1134  	redemptionUnlocked     uint64
  1135  	reservedRedemption     uint64
  1136  
  1137  	reserveNRefunds    uint64
  1138  	reserveNRefundsErr error
  1139  	reReserveRefundErr error
  1140  	refundUnlocked     uint64
  1141  	reservedRefund     uint64
  1142  }
  1143  
  1144  var _ asset.AccountLocker = (*TAccountLocker)(nil)
  1145  
  1146  func newTAccountLocker(assetID uint32) (*xcWallet, *TAccountLocker) {
  1147  	xcWallet, tWallet := newTWallet(assetID)
  1148  	accountLocker := &TAccountLocker{TXCWallet: tWallet}
  1149  	xcWallet.Wallet = accountLocker
  1150  	return xcWallet, accountLocker
  1151  }
  1152  
  1153  func (w *TAccountLocker) ReserveNRedemptions(n uint64, ver uint32, maxFeeRate uint64) (uint64, error) {
  1154  	return w.reserveNRedemptions, w.reserveNRedemptionsErr
  1155  }
  1156  
  1157  func (w *TAccountLocker) ReReserveRedemption(v uint64) error {
  1158  	w.fundingMtx.Lock()
  1159  	defer w.fundingMtx.Unlock()
  1160  	w.reservedRedemption += v
  1161  	return w.reReserveRedemptionErr
  1162  }
  1163  
  1164  func (w *TAccountLocker) UnlockRedemptionReserves(v uint64) {
  1165  	w.fundingMtx.Lock()
  1166  	defer w.fundingMtx.Unlock()
  1167  	w.redemptionUnlocked += v
  1168  }
  1169  
  1170  func (w *TAccountLocker) ReserveNRefunds(n uint64, ver uint32, maxFeeRate uint64) (uint64, error) {
  1171  	return w.reserveNRefunds, w.reserveNRefundsErr
  1172  }
  1173  
  1174  func (w *TAccountLocker) UnlockRefundReserves(v uint64) {
  1175  	w.fundingMtx.Lock()
  1176  	defer w.fundingMtx.Unlock()
  1177  	w.refundUnlocked += v
  1178  }
  1179  
  1180  func (w *TAccountLocker) ReReserveRefund(v uint64) error {
  1181  	w.fundingMtx.Lock()
  1182  	defer w.fundingMtx.Unlock()
  1183  	w.reservedRefund += v
  1184  	return w.reReserveRefundErr
  1185  }
  1186  
  1187  type TFeeRater struct {
  1188  	*TXCWallet
  1189  	feeRate uint64
  1190  }
  1191  
  1192  func (w *TFeeRater) FeeRate() uint64 {
  1193  	return w.feeRate
  1194  }
  1195  
  1196  type TLiveReconfigurer struct {
  1197  	*TXCWallet
  1198  	restart     bool
  1199  	reconfigErr error
  1200  }
  1201  
  1202  func (r *TLiveReconfigurer) Reconfigure(ctx context.Context, cfg *asset.WalletConfig, currentAddress string) (restartRequired bool, err error) {
  1203  	return r.restart, r.reconfigErr
  1204  }
  1205  
  1206  type tCrypterSmart struct {
  1207  	params     []byte
  1208  	encryptErr error
  1209  	decryptErr error
  1210  	recryptErr error
  1211  }
  1212  
  1213  func newTCrypterSmart() *tCrypterSmart {
  1214  	return &tCrypterSmart{
  1215  		params: encode.RandomBytes(5),
  1216  	}
  1217  }
  1218  
  1219  // Encrypt appends 8 random bytes to given []byte to mock.
  1220  func (c *tCrypterSmart) Encrypt(b []byte) ([]byte, error) {
  1221  	randSuffix := make([]byte, 8)
  1222  	crand.Read(randSuffix)
  1223  	b = append(b, randSuffix...)
  1224  	return b, c.encryptErr
  1225  }
  1226  
  1227  // Decrypt deletes the last 8 bytes from given []byte.
  1228  func (c *tCrypterSmart) Decrypt(b []byte) ([]byte, error) {
  1229  	return b[:len(b)-8], c.decryptErr
  1230  }
  1231  
  1232  func (c *tCrypterSmart) Serialize() []byte { return c.params }
  1233  
  1234  func (c *tCrypterSmart) Close() {}
  1235  
  1236  type tCrypter struct {
  1237  	encryptErr error
  1238  	decryptErr error
  1239  	recryptErr error
  1240  }
  1241  
  1242  func (c *tCrypter) Encrypt(b []byte) ([]byte, error) {
  1243  	return b, c.encryptErr
  1244  }
  1245  
  1246  func (c *tCrypter) Decrypt(b []byte) ([]byte, error) {
  1247  	return b, c.decryptErr
  1248  }
  1249  
  1250  func (c *tCrypter) Serialize() []byte { return nil }
  1251  
  1252  func (c *tCrypter) Close() {}
  1253  
  1254  var tAssetID uint32
  1255  
  1256  func randomAsset() *msgjson.Asset {
  1257  	tAssetID++
  1258  	return &msgjson.Asset{
  1259  		Symbol:  "BT" + strconv.Itoa(int(tAssetID)),
  1260  		ID:      tAssetID,
  1261  		Version: tAssetID * 2,
  1262  	}
  1263  }
  1264  
  1265  func randomMsgMarket() (baseAsset, quoteAsset *msgjson.Asset) {
  1266  	return randomAsset(), randomAsset()
  1267  }
  1268  
  1269  func tFetcher(_ context.Context, log dex.Logger, _ map[uint32]*SupportedAsset) map[uint32]float64 {
  1270  	return map[uint32]float64{
  1271  		tUTXOAssetA.ID: 45,
  1272  		tUTXOAssetB.ID: 32000,
  1273  	}
  1274  }
  1275  
  1276  type testRig struct {
  1277  	shutdown func()
  1278  	core     *Core
  1279  	db       *TDB
  1280  	queue    *wait.TickerQueue
  1281  	ws       *TWebsocket
  1282  	dc       *dexConnection
  1283  	acct     *dexAccount
  1284  	crypter  encrypt.Crypter
  1285  }
  1286  
  1287  func newTestRig() *testRig {
  1288  	tdb := &TDB{
  1289  		orderOrders:  make(map[order.OrderID]*db.MetaOrder),
  1290  		wallet:       &db.Wallet{},
  1291  		existValues:  map[string]bool{},
  1292  		legacyKeyErr: tErr,
  1293  	}
  1294  
  1295  	// Set the global waiter expiration, and start the waiter.
  1296  	queue := wait.NewTickerQueue(time.Millisecond * 5)
  1297  	ctx, cancel := context.WithCancel(tCtx)
  1298  	var wg sync.WaitGroup
  1299  	wg.Add(1)
  1300  	go func() {
  1301  		defer wg.Done()
  1302  		queue.Run(ctx)
  1303  	}()
  1304  
  1305  	crypter := &tCrypter{}
  1306  	dc, conn, acct := testDexConnection(ctx, crypter) // crypter makes acct.encKey consistent with privKey
  1307  
  1308  	ai := &db.AccountInfo{
  1309  		Host:      "somedex.com",
  1310  		Cert:      acct.cert,
  1311  		DEXPubKey: acct.dexPubKey,
  1312  		EncKeyV2:  acct.encKey,
  1313  	}
  1314  	tdb.acct = ai
  1315  
  1316  	shutdown := func() {
  1317  		cancel()
  1318  		wg.Wait()
  1319  		dc.connMaster.Wait()
  1320  	}
  1321  
  1322  	rig := &testRig{
  1323  		shutdown: shutdown,
  1324  		core: &Core{
  1325  			ctx:      ctx,
  1326  			cfg:      &Config{},
  1327  			db:       tdb,
  1328  			log:      tLogger,
  1329  			latencyQ: queue,
  1330  			conns: map[string]*dexConnection{
  1331  				tDexHost: dc,
  1332  			},
  1333  			lockTimeTaker: dex.LockTimeTaker(dex.Testnet),
  1334  			lockTimeMaker: dex.LockTimeMaker(dex.Testnet),
  1335  			wallets:       make(map[uint32]*xcWallet),
  1336  			blockWaiters:  make(map[string]*blockWaiter),
  1337  			sentCommits:   make(map[order.Commitment]chan struct{}),
  1338  			tickSched:     make(map[order.OrderID]*time.Timer),
  1339  			wsConstructor: func(*comms.WsCfg) (comms.WsConn, error) {
  1340  				// This is not very realistic since it doesn't start a fresh
  1341  				// one, and (*Core).connectDEX always gets the same TWebsocket,
  1342  				// which may have been previously "disconnected".
  1343  				return conn, nil
  1344  			},
  1345  			newCrypter: func([]byte) encrypt.Crypter { return crypter },
  1346  			reCrypter:  func([]byte, []byte) (encrypt.Crypter, error) { return crypter, crypter.recryptErr },
  1347  			noteChans:  make(map[uint64]chan Notification),
  1348  
  1349  			fiatRateSources:  make(map[string]*commonRateSource),
  1350  			notes:            make(chan asset.WalletNotification, 128),
  1351  			pokesCache:       newPokesCache(pokesCapacity),
  1352  			requestedActions: make(map[string]*asset.ActionRequiredNote),
  1353  		},
  1354  		db:      tdb,
  1355  		queue:   queue,
  1356  		ws:      conn,
  1357  		dc:      dc,
  1358  		acct:    acct,
  1359  		crypter: crypter,
  1360  	}
  1361  
  1362  	rig.core.intl.Store(&locale{
  1363  		m:       originLocale,
  1364  		printer: message.NewPrinter(language.AmericanEnglish),
  1365  	})
  1366  
  1367  	rig.core.InitializeClient(tPW, nil)
  1368  
  1369  	// tCrypter doesn't actually use random bytes supplied by InitializeClient,
  1370  	// (the crypter is known ahead of time) but if that changes, we would need
  1371  	// to encrypt the acct.privKey here, after InitializeClient generates a new
  1372  	// random inner key/crypter: rig.resetAcctEncKey(tPW)
  1373  
  1374  	return rig
  1375  }
  1376  
  1377  // Encrypt acct.privKey -> acct.encKey if InitializeClient generates a new
  1378  // random inner key/crypter that is different from the one used on construction.
  1379  // Important if Core's crypters actually use their initialization data (random
  1380  // bytes for inner crypter and the pw for outer).
  1381  func (rig *testRig) resetAcctEncKey(pw []byte) error {
  1382  	innerCrypter, err := rig.core.encryptionKey(pw)
  1383  	if err != nil {
  1384  		return fmt.Errorf("encryptionKey error: %w", err)
  1385  	}
  1386  	encKey, err := innerCrypter.Encrypt(rig.acct.privKey.Serialize())
  1387  	if err != nil {
  1388  		return fmt.Errorf("crypter.Encrypt error: %w", err)
  1389  	}
  1390  	rig.acct.encKey = encKey
  1391  	return nil
  1392  }
  1393  
  1394  func (rig *testRig) queueConfig() {
  1395  	rig.ws.queueResponse(msgjson.ConfigRoute, func(msg *msgjson.Message, f msgFunc) error {
  1396  		resp, _ := msgjson.NewResponse(msg.ID, rig.dc.cfg, nil)
  1397  		f(resp)
  1398  		return nil
  1399  	})
  1400  }
  1401  
  1402  func (rig *testRig) queuePrevalidateBond() {
  1403  	rig.ws.queueResponse(msgjson.PreValidateBondRoute, func(msg *msgjson.Message, f msgFunc) error {
  1404  		preEval := new(msgjson.PreValidateBond)
  1405  		msg.Unmarshal(preEval)
  1406  
  1407  		preEvalResult := &msgjson.PreValidateBondResult{
  1408  			AccountID: rig.dc.acct.id[:],
  1409  			AssetID:   preEval.AssetID,
  1410  			Amount:    dcrBondAsset.Amt,
  1411  			// Expiry: ,
  1412  		}
  1413  		sign(tDexPriv, preEvalResult)
  1414  		resp, _ := msgjson.NewResponse(msg.ID, preEvalResult, nil)
  1415  		f(resp)
  1416  		return nil
  1417  	})
  1418  }
  1419  
  1420  func (rig *testRig) queuePostBond(postBondResult *msgjson.PostBondResult) {
  1421  	rig.ws.queueResponse(msgjson.PostBondRoute, func(msg *msgjson.Message, f msgFunc) error {
  1422  		bond := new(msgjson.PostBond)
  1423  		msg.Unmarshal(bond)
  1424  		rig.ws.submittedBond = bond
  1425  		postBondResult.BondID = bond.CoinID
  1426  		sign(tDexPriv, postBondResult)
  1427  		resp, _ := msgjson.NewResponse(msg.ID, postBondResult, nil)
  1428  		f(resp)
  1429  		return nil
  1430  	})
  1431  }
  1432  
  1433  func (rig *testRig) queueConnect(rpcErr *msgjson.Error, matches []*msgjson.Match, orders []*msgjson.OrderStatus, suspended ...bool) {
  1434  	rig.ws.queueResponse(msgjson.ConnectRoute, func(msg *msgjson.Message, f msgFunc) error {
  1435  		if rpcErr != nil {
  1436  			resp, _ := msgjson.NewResponse(msg.ID, nil, rpcErr)
  1437  			f(resp)
  1438  			return nil
  1439  		}
  1440  
  1441  		connect := new(msgjson.Connect)
  1442  		msg.Unmarshal(connect)
  1443  		sign(tDexPriv, connect)
  1444  
  1445  		activeBonds := make([]*msgjson.Bond, 0, 1)
  1446  		if b := rig.ws.submittedBond; b != nil {
  1447  			activeBonds = append(activeBonds, &msgjson.Bond{
  1448  				Version: b.Version,
  1449  				Amount:  dcrBondAsset.Amt,
  1450  				Expiry:  rig.ws.liveBondExpiry,
  1451  				CoinID:  b.CoinID,
  1452  				AssetID: b.AssetID,
  1453  			})
  1454  		}
  1455  
  1456  		result := &msgjson.ConnectResult{
  1457  			Sig:                 connect.Sig,
  1458  			ActiveMatches:       matches,
  1459  			ActiveOrderStatuses: orders,
  1460  			ActiveBonds:         activeBonds,
  1461  			Score:               10,
  1462  			Reputation:          &account.Reputation{BondedTier: 1},
  1463  		}
  1464  		if len(suspended) > 0 && suspended[0] {
  1465  			result.Reputation.Penalties = 1
  1466  		}
  1467  		resp, _ := msgjson.NewResponse(msg.ID, result, nil)
  1468  		f(resp)
  1469  		return nil
  1470  	})
  1471  }
  1472  
  1473  func (rig *testRig) queueCancel(rpcErr *msgjson.Error) {
  1474  	rig.ws.queueResponse(msgjson.CancelRoute, func(msg *msgjson.Message, f msgFunc) error {
  1475  		var resp *msgjson.Message
  1476  		if rpcErr == nil {
  1477  			// Need to stamp and sign the message with the server's key.
  1478  			msgOrder := new(msgjson.CancelOrder)
  1479  			err := msg.Unmarshal(msgOrder)
  1480  			if err != nil {
  1481  				rpcErr = msgjson.NewError(msgjson.RPCParseError, "unable to unmarshal request")
  1482  			} else {
  1483  				co := convertMsgCancelOrder(msgOrder)
  1484  				resp = orderResponse(msg.ID, msgOrder, co, false, false, false)
  1485  			}
  1486  		}
  1487  		if rpcErr != nil {
  1488  			resp, _ = msgjson.NewResponse(msg.ID, nil, rpcErr)
  1489  		}
  1490  		f(resp)
  1491  		return nil
  1492  	})
  1493  }
  1494  
  1495  func TestMain(m *testing.M) {
  1496  	var shutdown context.CancelFunc
  1497  	tCtx, shutdown = context.WithCancel(context.Background())
  1498  	tDexPriv, _ = secp256k1.GeneratePrivateKey()
  1499  	tDexKey = tDexPriv.PubKey()
  1500  
  1501  	doIt := func() int {
  1502  		// Not counted as coverage, must test Archiver constructor explicitly.
  1503  		defer shutdown()
  1504  		return m.Run()
  1505  	}
  1506  	os.Exit(doIt())
  1507  }
  1508  
  1509  func TestMarkets(t *testing.T) {
  1510  	rig := newTestRig()
  1511  	defer rig.shutdown()
  1512  	// The test rig's dexConnection comes with a market. Clear that for this test.
  1513  	rig.dc.cfgMtx.Lock()
  1514  	rig.dc.cfg.Markets = nil
  1515  	rig.dc.cfgMtx.Unlock()
  1516  	numMarkets := 10
  1517  
  1518  	tCore := rig.core
  1519  	// Simulate 10 markets.
  1520  	marketIDs := make(map[string]struct{})
  1521  	for i := 0; i < numMarkets; i++ {
  1522  		base, quote := randomMsgMarket()
  1523  		marketIDs[marketName(base.ID, quote.ID)] = struct{}{}
  1524  		rig.dc.cfgMtx.RLock()
  1525  		cfg := rig.dc.cfg
  1526  		rig.dc.cfgMtx.RUnlock()
  1527  		cfg.Markets = append(cfg.Markets, &msgjson.Market{
  1528  			Name:            base.Symbol + quote.Symbol,
  1529  			Base:            base.ID,
  1530  			Quote:           quote.ID,
  1531  			EpochLen:        5000,
  1532  			MarketBuyBuffer: 1.4,
  1533  		})
  1534  		rig.dc.assetsMtx.Lock()
  1535  		rig.dc.assets[base.ID] = convertAssetInfo(base)
  1536  		rig.dc.assets[quote.ID] = convertAssetInfo(quote)
  1537  		rig.dc.assetsMtx.Unlock()
  1538  	}
  1539  
  1540  	// Just check that the information is coming through correctly.
  1541  	xcs := tCore.Exchanges()
  1542  	if len(xcs) != 1 {
  1543  		t.Fatalf("expected 1 MarketInfo, got %d", len(xcs))
  1544  	}
  1545  
  1546  	rig.dc.assetsMtx.RLock()
  1547  	defer rig.dc.assetsMtx.RUnlock()
  1548  
  1549  	assets := rig.dc.assets
  1550  	for _, xc := range xcs {
  1551  		for _, market := range xc.Markets {
  1552  			mkt := marketName(market.BaseID, market.QuoteID)
  1553  			_, found := marketIDs[mkt]
  1554  			if !found {
  1555  				t.Fatalf("market %s not found", mkt)
  1556  			}
  1557  			if assets[market.BaseID].Symbol != market.BaseSymbol {
  1558  				t.Fatalf("base symbol mismatch. %s != %s", assets[market.BaseID].Symbol, market.BaseSymbol)
  1559  			}
  1560  			if assets[market.QuoteID].Symbol != market.QuoteSymbol {
  1561  				t.Fatalf("quote symbol mismatch. %s != %s", assets[market.QuoteID].Symbol, market.QuoteSymbol)
  1562  			}
  1563  		}
  1564  	}
  1565  }
  1566  
  1567  func TestBookFeed(t *testing.T) {
  1568  	rig := newTestRig()
  1569  	defer rig.shutdown()
  1570  	tCore := rig.core
  1571  	dc := rig.dc
  1572  
  1573  	checkAction := func(feed BookFeed, action string) {
  1574  		t.Helper()
  1575  		select {
  1576  		case u := <-feed.Next():
  1577  			if u.Action != action {
  1578  				t.Fatalf("expected action = %s, got %s", action, u.Action)
  1579  			}
  1580  		default:
  1581  			t.Fatalf("no %s received", action)
  1582  		}
  1583  	}
  1584  
  1585  	// Ensure handleOrderBookMsg creates an order book as expected.
  1586  	oid1 := ordertest.RandomOrderID()
  1587  	bookMsg, err := msgjson.NewResponse(1, &msgjson.OrderBook{
  1588  		Seq:      1,
  1589  		MarketID: tDcrBtcMktName,
  1590  		Orders: []*msgjson.BookOrderNote{
  1591  			{
  1592  				TradeNote: msgjson.TradeNote{
  1593  					Side:     msgjson.BuyOrderNum,
  1594  					Quantity: 10,
  1595  					Rate:     2,
  1596  				},
  1597  				OrderNote: msgjson.OrderNote{
  1598  					Seq:      1,
  1599  					MarketID: tDcrBtcMktName,
  1600  					OrderID:  oid1[:],
  1601  				},
  1602  			},
  1603  		},
  1604  	}, nil)
  1605  	if err != nil {
  1606  		t.Fatalf("[NewResponse]: unexpected err: %v", err)
  1607  	}
  1608  
  1609  	oid2 := ordertest.RandomOrderID()
  1610  	bookNote, _ := msgjson.NewNotification(msgjson.BookOrderRoute, &msgjson.BookOrderNote{
  1611  		TradeNote: msgjson.TradeNote{
  1612  			Side:     msgjson.BuyOrderNum,
  1613  			Quantity: 10,
  1614  			Rate:     2,
  1615  		},
  1616  		OrderNote: msgjson.OrderNote{
  1617  			Seq:      2,
  1618  			MarketID: tDcrBtcMktName,
  1619  			OrderID:  oid2[:],
  1620  		},
  1621  	})
  1622  
  1623  	err = handleBookOrderMsg(tCore, dc, bookNote)
  1624  	if err == nil {
  1625  		t.Fatalf("no error for missing book")
  1626  	}
  1627  
  1628  	// Sync to unknown dex
  1629  	_, _, err = tCore.SyncBook("unknown dex", tUTXOAssetA.ID, tUTXOAssetB.ID)
  1630  	if err == nil {
  1631  		t.Fatalf("no error for unknown dex")
  1632  	}
  1633  	_, _, err = tCore.SyncBook(tDexHost, tUTXOAssetA.ID, 12345)
  1634  	if err == nil {
  1635  		t.Fatalf("no error for nonsense market")
  1636  	}
  1637  
  1638  	// Success
  1639  	rig.ws.queueResponse(msgjson.OrderBookRoute, func(msg *msgjson.Message, f msgFunc) error {
  1640  		f(bookMsg)
  1641  		return nil
  1642  	})
  1643  	_, feed1, err := tCore.SyncBook(tDexHost, tUTXOAssetA.ID, tUTXOAssetB.ID)
  1644  	if err != nil {
  1645  		t.Fatalf("SyncBook 1 error: %v", err)
  1646  	}
  1647  	_, feed2, err := tCore.SyncBook(tDexHost, tUTXOAssetA.ID, tUTXOAssetB.ID)
  1648  	if err != nil {
  1649  		t.Fatalf("SyncBook 2 error: %v", err)
  1650  	}
  1651  
  1652  	// Should be able to retrieve the book now.
  1653  	book, err := tCore.Book(tDexHost, tUTXOAssetA.ID, tUTXOAssetB.ID)
  1654  	if err != nil {
  1655  		t.Fatalf("Core.Book error: %v", err)
  1656  	}
  1657  	// Should have one buy order
  1658  	if len(book.Buys) != 1 {
  1659  		t.Fatalf("no buy orders found. expected 1")
  1660  	}
  1661  
  1662  	// Both channels should have a full orderbook.
  1663  	checkAction(feed1, FreshBookAction)
  1664  	checkAction(feed2, FreshBookAction)
  1665  
  1666  	err = handleBookOrderMsg(tCore, dc, bookNote)
  1667  	if err != nil {
  1668  		t.Fatalf("[handleBookOrderMsg]: unexpected err: %v", err)
  1669  	}
  1670  
  1671  	// Both channels should have an update.
  1672  	checkAction(feed1, BookOrderAction)
  1673  	checkAction(feed2, BookOrderAction)
  1674  
  1675  	// Close feed 1
  1676  	feed1.Close()
  1677  
  1678  	oid3 := ordertest.RandomOrderID()
  1679  	bookNote, _ = msgjson.NewNotification(msgjson.BookOrderRoute, &msgjson.BookOrderNote{
  1680  		TradeNote: msgjson.TradeNote{
  1681  			Side:     msgjson.SellOrderNum,
  1682  			Quantity: 10,
  1683  			Rate:     3,
  1684  		},
  1685  		OrderNote: msgjson.OrderNote{
  1686  			Seq:      3,
  1687  			MarketID: tDcrBtcMktName,
  1688  			OrderID:  oid3[:],
  1689  		},
  1690  	})
  1691  	err = handleBookOrderMsg(tCore, dc, bookNote)
  1692  	if err != nil {
  1693  		t.Fatalf("[handleBookOrderMsg]: unexpected err: %v", err)
  1694  	}
  1695  
  1696  	// feed1 should have no update
  1697  	select {
  1698  	case <-feed1.Next():
  1699  		t.Fatalf("update for feed 1 after Close")
  1700  	default:
  1701  	}
  1702  	// feed2 should though
  1703  	checkAction(feed2, BookOrderAction)
  1704  
  1705  	// Make sure the book has been updated.
  1706  	book, _ = tCore.Book(tDexHost, tUTXOAssetA.ID, tUTXOAssetB.ID)
  1707  	if len(book.Buys) != 2 {
  1708  		t.Fatalf("expected 2 buys, got %d", len(book.Buys))
  1709  	}
  1710  	if len(book.Sells) != 1 {
  1711  		t.Fatalf("expected 1 sell, got %d", len(book.Sells))
  1712  	}
  1713  
  1714  	// Update the remaining quantity of the just booked order.
  1715  	var remaining uint64 = 5 * 1e8
  1716  	bookNote, _ = msgjson.NewNotification(msgjson.BookOrderRoute, &msgjson.UpdateRemainingNote{
  1717  		OrderNote: msgjson.OrderNote{
  1718  			Seq:      4,
  1719  			MarketID: tDcrBtcMktName,
  1720  			OrderID:  oid3[:],
  1721  		},
  1722  		Remaining: remaining,
  1723  	})
  1724  	err = handleUpdateRemainingMsg(tCore, dc, bookNote)
  1725  	if err != nil {
  1726  		t.Fatalf("[handleBookOrderMsg]: unexpected err: %v", err)
  1727  	}
  1728  
  1729  	// feed2 should have an update
  1730  	checkAction(feed2, UpdateRemainingAction)
  1731  	book, _ = tCore.Book(tDexHost, tUTXOAssetA.ID, tUTXOAssetB.ID)
  1732  	firstSellQty := book.Sells[0].QtyAtomic
  1733  	if firstSellQty != remaining {
  1734  		t.Fatalf("expected remaining quantity of %d after update_remaining. got %d", remaining, firstSellQty)
  1735  	}
  1736  
  1737  	// Ensure handleUnbookOrderMsg removes a book order from an associated
  1738  	// order book as expected.
  1739  	unbookNote, _ := msgjson.NewNotification(msgjson.UnbookOrderRoute, &msgjson.UnbookOrderNote{
  1740  		Seq:      5,
  1741  		MarketID: tDcrBtcMktName,
  1742  		OrderID:  oid1[:],
  1743  	})
  1744  
  1745  	err = handleUnbookOrderMsg(tCore, dc, unbookNote)
  1746  	if err != nil {
  1747  		t.Fatalf("[handleUnbookOrderMsg]: unexpected err: %v", err)
  1748  	}
  1749  	// feed2 should have a notification.
  1750  	checkAction(feed2, UnbookOrderAction)
  1751  	book, _ = tCore.Book(tDexHost, tUTXOAssetA.ID, tUTXOAssetB.ID)
  1752  	if len(book.Buys) != 1 {
  1753  		t.Fatalf("expected 1 buy after unbook_order, got %d", len(book.Buys))
  1754  	}
  1755  
  1756  	// Test candles
  1757  	queueCandles := func() {
  1758  		rig.ws.queueResponse(msgjson.CandlesRoute, func(msg *msgjson.Message, f msgFunc) error {
  1759  			resp, _ := msgjson.NewResponse(msg.ID, &msgjson.WireCandles{
  1760  				StartStamps:  []uint64{1, 2},
  1761  				EndStamps:    []uint64{3, 4},
  1762  				MatchVolumes: []uint64{1, 2},
  1763  				QuoteVolumes: []uint64{1, 2},
  1764  				HighRates:    []uint64{3, 4},
  1765  				LowRates:     []uint64{1, 2},
  1766  				StartRates:   []uint64{1, 2},
  1767  				EndRates:     []uint64{3, 4},
  1768  			}, nil)
  1769  			f(resp)
  1770  			return nil
  1771  		})
  1772  	}
  1773  	queueCandles()
  1774  
  1775  	if err := feed2.Candles("1h"); err != nil {
  1776  		t.Fatalf("Candles error: %v", err)
  1777  	}
  1778  
  1779  	checkAction(feed2, FreshCandlesAction)
  1780  
  1781  	// An epoch report should trigger two candle updates, one for each bin size.
  1782  	epochReport, _ := msgjson.NewNotification(msgjson.EpochReportRoute, &msgjson.EpochReportNote{
  1783  		MarketID:     tDcrBtcMktName,
  1784  		Epoch:        1,
  1785  		BaseFeeRate:  2,
  1786  		QuoteFeeRate: 3,
  1787  		Candle: msgjson.Candle{
  1788  			StartStamp:  1,
  1789  			EndStamp:    2,
  1790  			MatchVolume: 3,
  1791  			QuoteVolume: 3,
  1792  			HighRate:    4,
  1793  			LowRate:     1,
  1794  			StartRate:   1,
  1795  			EndRate:     2,
  1796  		},
  1797  	})
  1798  
  1799  	if err := handleEpochReportMsg(tCore, dc, epochReport); err != nil {
  1800  		t.Fatalf("handleEpochReportMsg error: %v", err)
  1801  	}
  1802  
  1803  	checkAction(feed2, EpochMatchSummary)
  1804  
  1805  	// We'll only receive 1 candle update, since we only synced one set of
  1806  	// candles so far.
  1807  	checkAction(feed2, CandleUpdateAction)
  1808  	checkAction(feed2, EpochResolved)
  1809  
  1810  	// Now subscribe to the 24h candles too.
  1811  	queueCandles()
  1812  	if err := feed2.Candles("24h"); err != nil {
  1813  		t.Fatalf("24h Candles error: %v", err)
  1814  	}
  1815  	checkAction(feed2, FreshCandlesAction)
  1816  
  1817  	// This time, an epoch report should trigger two updates.
  1818  	if err := handleEpochReportMsg(tCore, dc, epochReport); err != nil {
  1819  		t.Fatalf("handleEpochReportMsg error: %v", err)
  1820  	}
  1821  	checkAction(feed2, EpochMatchSummary)
  1822  	checkAction(feed2, CandleUpdateAction)
  1823  	checkAction(feed2, CandleUpdateAction)
  1824  }
  1825  
  1826  type tDriver struct {
  1827  	wallet        asset.Wallet
  1828  	decodedCoinID string
  1829  	winfo         *asset.WalletInfo
  1830  }
  1831  
  1832  func (drv *tDriver) Open(cfg *asset.WalletConfig, logger dex.Logger, net dex.Network) (asset.Wallet, error) {
  1833  	return drv.wallet, nil
  1834  }
  1835  
  1836  func (drv *tDriver) DecodeCoinID(coinID []byte) (string, error) {
  1837  	return drv.decodedCoinID, nil
  1838  }
  1839  
  1840  func (drv *tDriver) Info() *asset.WalletInfo {
  1841  	return drv.winfo
  1842  }
  1843  
  1844  type tCreator struct {
  1845  	*tDriver
  1846  	doesntExist bool
  1847  	existsErr   error
  1848  	createErr   error
  1849  }
  1850  
  1851  func (ctr *tCreator) Exists(walletType, dataDir string, settings map[string]string, net dex.Network) (bool, error) {
  1852  	return !ctr.doesntExist, ctr.existsErr
  1853  }
  1854  
  1855  func (ctr *tCreator) Create(*asset.CreateWalletParams) error {
  1856  	return ctr.createErr
  1857  }
  1858  
  1859  func TestCreateWallet(t *testing.T) {
  1860  	rig := newTestRig()
  1861  	defer rig.shutdown()
  1862  	tCore := rig.core
  1863  
  1864  	// Create a new asset.
  1865  	a := *tUTXOAssetA
  1866  	tILT := &a
  1867  	tILT.Symbol = "ilt"
  1868  	tILT.ID, _ = dex.BipSymbolID(tILT.Symbol)
  1869  
  1870  	// Create registration form.
  1871  	form := &WalletForm{
  1872  		AssetID: tILT.ID,
  1873  		Config: map[string]string{
  1874  			"rpclisten": "localhost",
  1875  		},
  1876  		Type: "type",
  1877  	}
  1878  
  1879  	ensureErr := func(tag string) {
  1880  		t.Helper()
  1881  		err := tCore.CreateWallet(tPW, wPW, form)
  1882  		if err == nil {
  1883  			t.Fatalf("no %s error", tag)
  1884  		}
  1885  	}
  1886  
  1887  	// Try to add an existing wallet.
  1888  	wallet, tWallet := newTWallet(tILT.ID)
  1889  	tCore.wallets[tILT.ID] = wallet
  1890  	ensureErr("existing wallet")
  1891  	delete(tCore.wallets, tILT.ID)
  1892  
  1893  	// Failure to retrieve encryption key params.
  1894  	creds := tCore.credentials
  1895  	tCore.credentials = nil
  1896  	ensureErr("db.Get")
  1897  	tCore.credentials = creds
  1898  
  1899  	// Crypter error.
  1900  	rig.crypter.(*tCrypter).encryptErr = tErr
  1901  	ensureErr("Encrypt")
  1902  	rig.crypter.(*tCrypter).encryptErr = nil
  1903  
  1904  	// Try an unknown wallet (not yet asset.Register'ed).
  1905  	ensureErr("unregistered asset")
  1906  
  1907  	// Register the asset.
  1908  	asset.Register(tILT.ID, &tDriver{
  1909  		wallet:        wallet.Wallet,
  1910  		decodedCoinID: "ilt-coin",
  1911  		winfo:         tWalletInfo,
  1912  	})
  1913  
  1914  	// Connection error.
  1915  	tWallet.connectErr = tErr
  1916  	ensureErr("Connect")
  1917  	tWallet.connectErr = nil
  1918  
  1919  	// Unlock error.
  1920  	tWallet.unlockErr = tErr
  1921  	ensureErr("Unlock")
  1922  	tWallet.unlockErr = nil
  1923  
  1924  	// Address error.
  1925  	tWallet.addrErr = tErr
  1926  	ensureErr("Address")
  1927  	tWallet.addrErr = nil
  1928  
  1929  	// Balance error.
  1930  	tWallet.balErr = tErr
  1931  	ensureErr("Balance")
  1932  	tWallet.balErr = nil
  1933  
  1934  	// Database error.
  1935  	rig.db.updateWalletErr = tErr
  1936  	ensureErr("db.UpdateWallet")
  1937  	rig.db.updateWalletErr = nil
  1938  
  1939  	// Success
  1940  	delete(tCore.wallets, tILT.ID)
  1941  	err := tCore.CreateWallet(tPW, wPW, form)
  1942  	if err != nil {
  1943  		t.Fatalf("error when should be no error: %v", err)
  1944  	}
  1945  }
  1946  
  1947  // TODO: TestGetDEXConfig
  1948  /*
  1949  func TestGetFee(t *testing.T) {
  1950  	rig := newTestRig()
  1951  	defer rig.shutdown()
  1952  	tCore := rig.core
  1953  	cert := []byte{}
  1954  
  1955  	// DEX already registered
  1956  	_, err := tCore.GetFee(tDexHost, cert)
  1957  	if !errorHasCode(err, dupeDEXErr) {
  1958  		t.Fatalf("wrong account exists error: %v", err)
  1959  	}
  1960  
  1961  	// Lose the dexConnection
  1962  	tCore.connMtx.Lock()
  1963  	delete(tCore.conns, tDexHost)
  1964  	tCore.connMtx.Unlock()
  1965  
  1966  	// connectDEX error
  1967  	_, err = tCore.GetFee(tUnparseableHost, cert)
  1968  	if !errorHasCode(err, addressParseErr) {
  1969  		t.Fatalf("wrong connectDEX error: %v", err)
  1970  	}
  1971  
  1972  	// Queue a config response for success
  1973  	rig.queueConfig()
  1974  
  1975  	// Success
  1976  	_, err = tCore.GetFee(tDexHost, cert)
  1977  	if err != nil {
  1978  		t.Fatalf("GetFee error: %v", err)
  1979  	}
  1980  }
  1981  */
  1982  
  1983  func TestPostBond(t *testing.T) {
  1984  	// This test takes a little longer because the key is decrypted every time
  1985  	// Register is called.
  1986  	rig := newTestRig()
  1987  	defer rig.shutdown()
  1988  	tCore := rig.core
  1989  	dc := rig.dc
  1990  	clearConn := func() {
  1991  		tCore.connMtx.Lock()
  1992  		delete(tCore.conns, tDexHost)
  1993  		tCore.connMtx.Unlock()
  1994  	}
  1995  	clearConn()
  1996  
  1997  	wallet, tWallet := newTWallet(tUTXOAssetA.ID)
  1998  	tCore.wallets[tUTXOAssetA.ID] = wallet
  1999  	tWallet.bal = &asset.Balance{
  2000  		Available: 4e9,
  2001  	}
  2002  
  2003  	// When registering, successfully retrieving *db.AccountInfo from the DB is
  2004  	// an error (no dupes). Initial state is to return an error.
  2005  	rig.db.acctErr = tErr
  2006  
  2007  	_ = tCore.Login(tPW)
  2008  
  2009  	// (*Core).Register does setupCryptoV2 to make the dc.acct.privKey etc., so
  2010  	// we don't know the ClientPubKey here. It must be set in the request
  2011  	// handler configured by queueRegister.
  2012  	rig.ws.liveBondExpiry = uint64(time.Now().Add(time.Duration(pendingBuffer(dex.Simnet)) * 2 * time.Second).Unix())
  2013  	postBondResult := &msgjson.PostBondResult{
  2014  		AccountID:  rig.acct.id[:],
  2015  		AssetID:    dcrBondAsset.ID,
  2016  		Amount:     dcrBondAsset.Amt,
  2017  		Expiry:     rig.ws.liveBondExpiry,
  2018  		Reputation: &account.Reputation{BondedTier: 1},
  2019  	}
  2020  
  2021  	var wg sync.WaitGroup
  2022  	defer wg.Wait() // don't allow fail after TestRegister return
  2023  
  2024  	queueTipChange := func() {
  2025  		wg.Add(1)
  2026  		go func() {
  2027  			defer wg.Done()
  2028  			timeout := time.NewTimer(time.Second * 2)
  2029  			defer timeout.Stop()
  2030  			ticker := time.NewTicker(10 * time.Millisecond)
  2031  			defer ticker.Stop()
  2032  			for {
  2033  				select {
  2034  				case <-ticker.C:
  2035  					tCore.waiterMtx.Lock()
  2036  					waiterCount := len(tCore.blockWaiters)
  2037  					tCore.waiterMtx.Unlock()
  2038  
  2039  					// Every tick, increase the bond tx confirmation count.
  2040  					if waiterCount > 0 { // when verifyRegistrationFee adds a waiter, then we can trigger tip change
  2041  						confs, found := tWallet.confs[dex.Bytes(tWallet.bondTxCoinID).String()]
  2042  						if !found {
  2043  							tWallet.setConfs(tWallet.bondTxCoinID, 0, nil)
  2044  						} else {
  2045  							tWallet.setConfs(tWallet.bondTxCoinID, confs+1, nil)
  2046  						}
  2047  
  2048  						tCore.tipChange(tUTXOAssetA.ID)
  2049  						return
  2050  					}
  2051  				case <-timeout.C:
  2052  					t.Errorf("failed to find waiter before timeout")
  2053  					return
  2054  				}
  2055  			}
  2056  		}()
  2057  	}
  2058  
  2059  	accountNotFoundError := msgjson.NewError(msgjson.AccountNotFoundError, "test account not found error")
  2060  
  2061  	queueConfigAndConnectUnknownAcct := func() {
  2062  		rig.ws.submittedBond = nil
  2063  		rig.queueConfig()
  2064  		rig.queueConnect(accountNotFoundError, nil, nil) // for discoverAccount
  2065  		rig.ws.queueResponse(msgjson.FeeRateRoute, func(msg *msgjson.Message, f msgFunc) error {
  2066  			const feeRate = 50
  2067  			resp, _ := msgjson.NewResponse(msg.ID, feeRate, nil)
  2068  			f(resp)
  2069  			return nil
  2070  		})
  2071  	}
  2072  
  2073  	queuePostBondSequence := func() {
  2074  		rig.queuePrevalidateBond()
  2075  		rig.queuePostBond(postBondResult)
  2076  		queueTipChange()
  2077  		rig.queueConnect(nil, nil, nil)
  2078  	}
  2079  
  2080  	queueResponses := func() {
  2081  		queueConfigAndConnectUnknownAcct()
  2082  		queuePostBondSequence()
  2083  	}
  2084  
  2085  	form := &PostBondForm{
  2086  		Addr:    tDexHost,
  2087  		AppPass: tPW,
  2088  		Asset:   &dcrBondAsset.ID,
  2089  		Bond:    dcrBondAsset.Amt,
  2090  		Cert:    []byte{0x1}, // not empty signals TLS, otherwise no TLS allowed hidden services
  2091  	}
  2092  
  2093  	// Suppress warnings about SendTransaction returning a mismatching ID.
  2094  	tWallet.feeCoinSent = tWallet.bondTxCoinID
  2095  
  2096  	ch := tCore.NotificationFeed()
  2097  
  2098  	var err error
  2099  	run := func() {
  2100  		// Register method will error if url is already in conns map.
  2101  		clearConn()
  2102  
  2103  		tWallet.setConfs(tWallet.bondTxCoinID, 0, nil)
  2104  		// Skip finding bonds.
  2105  		tWallet.findBondErr = errors.New("purposeful error")
  2106  		_, err = tCore.PostBond(form)
  2107  	}
  2108  
  2109  	getNotification := func(tag string) any {
  2110  		t.Helper()
  2111  		select {
  2112  		case n := <-ch.C:
  2113  			return n
  2114  			// When it works, it should be virtually instant, but I have seen it fail
  2115  			// at 1 millisecond.
  2116  		case <-time.NewTimer(time.Second * 2).C:
  2117  			t.Fatalf("timed out waiting for %s notification", tag)
  2118  		}
  2119  		return nil
  2120  	}
  2121  
  2122  	// The feepayment note for mined fee payment txn notification to server, and
  2123  	// the balance note from tip change are concurrent and thus come in no
  2124  	// guaranteed order.
  2125  	getBondAndBalanceNote := func() {
  2126  		t.Helper()
  2127  		var bondNote *BondPostNote
  2128  		var balanceNotes uint8
  2129  		// For a normal PostBond, there are three balance updates.
  2130  		// 1) makeAndPostBond, 2) monitorBondConfs.trigger, and 3) tipChange.
  2131  		for bondNote == nil || balanceNotes < 3 {
  2132  			ntfn := getNotification("bond posted or balance")
  2133  			switch note := ntfn.(type) {
  2134  			case *BondPostNote:
  2135  				if note.TopicID == TopicAccountRegistered {
  2136  					bondNote = note
  2137  				}
  2138  			case *BalanceNote:
  2139  				balanceNotes++
  2140  			case *ReputationNote: // ignore
  2141  			default:
  2142  				t.Fatalf("wrong notification (%T). Expected FeePaymentNote or BalanceNote", ntfn)
  2143  			}
  2144  		}
  2145  	}
  2146  
  2147  	queueResponses()
  2148  	run()
  2149  	if err != nil {
  2150  		t.Fatalf("postbond error: %v", err)
  2151  	}
  2152  
  2153  	// Should be two success notifications. One for fee paid on-chain, one for
  2154  	// fee notification sent, each along with a balance note.
  2155  	getBondAndBalanceNote()
  2156  
  2157  	// password error
  2158  	rig.crypter.(*tCrypter).recryptErr = tErr
  2159  	run()
  2160  	if !errorHasCode(err, passwordErr) {
  2161  		t.Fatalf("wrong password error: %v", err)
  2162  	}
  2163  	rig.crypter.(*tCrypter).recryptErr = nil
  2164  
  2165  	// no host error
  2166  	form.Addr = ""
  2167  	run()
  2168  	if !errorHasCode(err, emptyHostErr) {
  2169  		t.Fatalf("wrong empty host error: %v", err)
  2170  	}
  2171  	form.Addr = tDexHost
  2172  
  2173  	// wallet not found
  2174  	delete(tCore.wallets, tUTXOAssetA.ID)
  2175  	run()
  2176  	if !errorHasCode(err, missingWalletErr) {
  2177  		t.Fatalf("wrong missing wallet error: %v", err)
  2178  	}
  2179  	tCore.wallets[tUTXOAssetA.ID] = wallet
  2180  
  2181  	// Unlock wallet error
  2182  	tWallet.unlockErr = tErr
  2183  	tWallet.locked = true
  2184  	run()
  2185  	if !errorHasCode(err, walletAuthErr) {
  2186  		t.Fatalf("wrong wallet auth error: %v", err)
  2187  	}
  2188  	tWallet.unlockErr = nil
  2189  	tWallet.locked = false
  2190  
  2191  	// connectDEX error
  2192  	form.Addr = tUnparseableHost
  2193  	run()
  2194  	if !errorHasCode(err, connectionErr) {
  2195  		t.Fatalf("wrong connectDEX error: %v", err)
  2196  	}
  2197  	form.Addr = tDexHost
  2198  
  2199  	// fee asset not found, no cfg.Fee fallback
  2200  	bondAssets := dc.cfg.BondAssets
  2201  	dc.cfg.BondAssets = nil
  2202  	queueConfigAndConnectUnknownAcct()
  2203  	run()
  2204  	if !errorHasCode(err, assetSupportErr) {
  2205  		t.Fatalf("wrong error for missing asset: %v", err)
  2206  	}
  2207  	dc.cfg.BondAssets = bondAssets
  2208  
  2209  	// error creating signing key
  2210  	rig.crypter.(*tCrypter).encryptErr = tErr
  2211  	rig.queueConfig()
  2212  	run()
  2213  	if !errorHasCode(err, acctKeyErr) {
  2214  		t.Fatalf("wrong account key error: %v", err)
  2215  	}
  2216  	rig.crypter.(*tCrypter).encryptErr = nil
  2217  
  2218  	bal0 := tWallet.bal.Available
  2219  	tWallet.bal.Available = 0
  2220  	run()
  2221  	if !errorHasCode(err, walletBalanceErr) {
  2222  		t.Fatalf("expected low balance error, got: %v", err)
  2223  	}
  2224  	tWallet.bal.Available = bal0
  2225  
  2226  	// signature error
  2227  	queueConfigAndConnectUnknownAcct()
  2228  	rig.ws.queueResponse(msgjson.PreValidateBondRoute, func(msg *msgjson.Message, f msgFunc) error {
  2229  		preEval := new(msgjson.PreValidateBond)
  2230  		msg.Unmarshal(preEval)
  2231  
  2232  		preEvalResult := &msgjson.PreValidateBondResult{
  2233  			Signature: msgjson.Signature{
  2234  				Sig: []byte{0xb, 0xa, 0xd},
  2235  			},
  2236  		}
  2237  		resp, _ := msgjson.NewResponse(msg.ID, preEvalResult, nil)
  2238  		f(resp)
  2239  		return nil
  2240  	})
  2241  	run()
  2242  	if !errorHasCode(err, signatureErr) {
  2243  		t.Fatalf("wrong error for bad signature on prevalidate response: %v", err)
  2244  	}
  2245  
  2246  	// Wrong bond size on form
  2247  	goodAmt := form.Bond
  2248  	form.Bond = goodAmt + 1
  2249  	queueConfigAndConnectUnknownAcct()
  2250  	run()
  2251  	if !errorHasCode(err, bondAmtErr) {
  2252  		t.Fatalf("wrong error for wrong fee in form: %v", err)
  2253  	}
  2254  	form.Bond = goodAmt
  2255  
  2256  	// MakeBondTx error
  2257  	queueConfigAndConnectUnknownAcct()
  2258  	tWallet.makeBondTxErr = tErr
  2259  	run()
  2260  	if !errorHasCode(err, bondPostErr) {
  2261  		t.Fatalf("wrong error for bondPostErr: %v", err)
  2262  	}
  2263  	tWallet.makeBondTxErr = nil
  2264  
  2265  	// Make sure it's good again.
  2266  	queueResponses()
  2267  	run()
  2268  	if err != nil {
  2269  		t.Fatalf("error after regaining valid state: %v", err)
  2270  	}
  2271  	getBondAndBalanceNote()
  2272  
  2273  	// Test the account recovery path.
  2274  	rig.queueConfig()
  2275  	rig.queueConnect(nil, nil, nil) // account exists
  2276  	run()
  2277  	if err != nil {
  2278  		t.Fatalf("Paid account error: %v", err)
  2279  	}
  2280  
  2281  	// Account suspended should derive new HD credentials.
  2282  	rig.queueConnect(nil, nil, nil, true) // first try exists but suspended
  2283  	queueResponses()
  2284  	run()
  2285  	if err != nil {
  2286  		t.Fatalf("Suspension recovery error: %v", err)
  2287  	}
  2288  	getBondAndBalanceNote()
  2289  }
  2290  
  2291  func TestCredentialsUpgrade(t *testing.T) {
  2292  	rig := newTestRig()
  2293  	defer rig.shutdown()
  2294  	tCore := rig.core
  2295  	rig.db.legacyKeyErr = nil
  2296  
  2297  	clearUpgrade := func() {
  2298  		rig.db.creds.EncInnerKey = nil
  2299  		tCore.credentials.EncInnerKey = nil
  2300  	}
  2301  
  2302  	clearUpgrade()
  2303  
  2304  	// initial success
  2305  	err := tCore.Login(tPW)
  2306  	if err != nil {
  2307  		t.Fatalf("initial Login error: %v", err)
  2308  	}
  2309  
  2310  	clearUpgrade()
  2311  
  2312  	// Recrypt error
  2313  	rig.db.recryptErr = tErr
  2314  	err = tCore.Login(tPW)
  2315  	if err == nil {
  2316  		t.Fatalf("no error for recryptErr")
  2317  	}
  2318  	rig.db.recryptErr = nil
  2319  
  2320  	// final success
  2321  	err = tCore.Login(tPW)
  2322  	if err != nil {
  2323  		t.Fatalf("final Login error: %v", err)
  2324  	}
  2325  }
  2326  
  2327  func unauth(a *dexAccount) {
  2328  	a.authMtx.Lock()
  2329  	a.isAuthed = false
  2330  	a.authMtx.Unlock()
  2331  }
  2332  
  2333  func TestLogin(t *testing.T) {
  2334  	rig := newTestRig()
  2335  	defer rig.shutdown()
  2336  	tCore := rig.core
  2337  	rig.acct.rep = account.Reputation{BondedTier: 1}
  2338  
  2339  	rig.queueConnect(nil, nil, nil)
  2340  	err := tCore.Login(tPW)
  2341  	if err != nil || !rig.acct.authed() {
  2342  		t.Fatalf("initial Login error: %v", err)
  2343  	}
  2344  
  2345  	// No encryption key.
  2346  	unauth(rig.acct)
  2347  	creds := tCore.credentials
  2348  	tCore.credentials = nil
  2349  	err = tCore.Login(tPW)
  2350  	if err == nil || rig.acct.authed() {
  2351  		t.Fatalf("no error for missing app key")
  2352  	}
  2353  	tCore.credentials = creds
  2354  
  2355  	// Account not Paid. No error, and account should be unlocked.
  2356  	rig.acct.rep = account.Reputation{BondedTier: 0}
  2357  	rig.queueConnect(nil, nil, nil)
  2358  	err = tCore.Login(tPW)
  2359  	if err != nil || rig.acct.authed() {
  2360  		t.Fatalf("error for unpaid account: %v", err)
  2361  	}
  2362  	if rig.acct.locked() {
  2363  		t.Fatalf("unpaid account is locked")
  2364  	}
  2365  	rig.acct.rep = account.Reputation{BondedTier: 1}
  2366  
  2367  	// 'connect' route error.
  2368  	rig = newTestRig()
  2369  	defer rig.shutdown()
  2370  	tCore = rig.core
  2371  	unauth(rig.acct)
  2372  	rig.ws.queueResponse(msgjson.ConnectRoute, func(msg *msgjson.Message, f msgFunc) error {
  2373  		resp, _ := msgjson.NewResponse(msg.ID, nil, msgjson.NewError(1, "test error"))
  2374  		f(resp)
  2375  		return nil
  2376  	})
  2377  	err = tCore.Login(tPW)
  2378  	// Should be no error, but also not authed. Error is sent and logged
  2379  	// as a notification.
  2380  	if err != nil || rig.acct.authed() {
  2381  		t.Fatalf("account authed after 'connect' error")
  2382  	}
  2383  
  2384  	// Success with some matches in the response.
  2385  	rig = newTestRig()
  2386  	defer rig.shutdown()
  2387  	dc := rig.dc
  2388  	qty := 3 * dcrBtcLotSize
  2389  	lo, dbOrder, preImg, addr := makeLimitOrder(dc, true, qty, dcrBtcRateStep*10)
  2390  	lo.Force = order.StandingTiF
  2391  	dbOrder.MetaData.Status = order.OrderStatusBooked // leave unfunded to have it canceled on auth/'connect'
  2392  	oid := lo.ID()
  2393  	dcrWallet, _ := newTWallet(tUTXOAssetA.ID)
  2394  	tCore.wallets[tUTXOAssetA.ID] = dcrWallet
  2395  	btcWallet, tBtcWallet := newTWallet(tUTXOAssetB.ID)
  2396  	tCore.wallets[tUTXOAssetB.ID] = btcWallet
  2397  	walletSet, _, _, _ := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, true)
  2398  	tracker := newTrackedTrade(dbOrder, preImg, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker,
  2399  		rig.db, rig.queue, walletSet, nil, rig.core.notify, rig.core.formatDetails) // nil means no funding coins
  2400  	matchID := ordertest.RandomMatchID()
  2401  	match := &matchTracker{
  2402  		MetaMatch: db.MetaMatch{
  2403  			UserMatch: &order.UserMatch{MatchID: matchID},
  2404  			MetaData:  &db.MatchMetaData{},
  2405  		},
  2406  	}
  2407  	tracker.matches[matchID] = match
  2408  	knownMsgMatch := &msgjson.Match{OrderID: oid[:], MatchID: matchID[:]}
  2409  
  2410  	// Known trade, but missing match
  2411  	missingID := ordertest.RandomMatchID()
  2412  	missingMatch := &matchTracker{
  2413  		MetaMatch: db.MetaMatch{
  2414  			UserMatch: &order.UserMatch{MatchID: missingID},
  2415  			MetaData:  &db.MatchMetaData{},
  2416  		},
  2417  	}
  2418  	tracker.matches[missingID] = missingMatch
  2419  
  2420  	// extra match
  2421  	extraID := ordertest.RandomMatchID()
  2422  	matchTime := time.Now()
  2423  	extraMsgMatch := &msgjson.Match{
  2424  		OrderID:    oid[:],
  2425  		MatchID:    extraID[:],
  2426  		Side:       uint8(order.Taker),
  2427  		Status:     uint8(order.MakerSwapCast),
  2428  		ServerTime: uint64(matchTime.UnixMilli()),
  2429  	}
  2430  
  2431  	// The extra match is already at MakerSwapCast, and we're the taker, which
  2432  	// will invoke match status conflict resolution and a contract audit.
  2433  	_, auditInfo := tMsgAudit(oid, extraID, addr, qty, encode.RandomBytes(32))
  2434  	auditInfo.Expiration = encode.DropMilliseconds(matchTime.Add(tracker.lockTimeMaker))
  2435  	tBtcWallet.auditInfo = auditInfo
  2436  	missedContract := encode.RandomBytes(50)
  2437  	rig.ws.queueResponse(msgjson.MatchStatusRoute, func(msg *msgjson.Message, f msgFunc) error {
  2438  		resp, _ := msgjson.NewResponse(msg.ID, []*msgjson.MatchStatusResult{{
  2439  			MatchID:       extraID[:],
  2440  			Status:        uint8(order.MakerSwapCast),
  2441  			MakerContract: missedContract,
  2442  			MakerSwap:     auditInfo.Coin.ID(),
  2443  			Active:        true,
  2444  			MakerTxData:   []byte{0x01},
  2445  		}}, nil)
  2446  		f(resp)
  2447  		return nil
  2448  	})
  2449  
  2450  	dc.trades = map[order.OrderID]*trackedTrade{
  2451  		oid: tracker,
  2452  	}
  2453  
  2454  	tCore = rig.core
  2455  	rig.queueConnect(nil, []*msgjson.Match{knownMsgMatch /* missing missingMatch! */, extraMsgMatch}, nil)
  2456  	rig.queueCancel(nil) // for the unfunded order that gets canceled in authDEX
  2457  	// Login>authDEX will do 4 match DB updates for these two matches:
  2458  	// missing -> revoke -> update match
  2459  	// extra -> negotiate -> newTrackers -> update match
  2460  	// matchConflicts (from extras) -> resolveMatchConflicts -> resolveConflictWithServerData
  2461  	// 	-> update match after spawning auditContract
  2462  	// 	-> update match in auditContract (second because of lock) ** the ASYNC one we have to wait for **
  2463  	rig.db.updateMatchChan = make(chan order.MatchStatus, 4)
  2464  	err = tCore.Login(tPW) // authDEX -> async contract audit for the extra match
  2465  	if err != nil || !rig.acct.authed() {
  2466  		t.Fatalf("final Login error: %v", err)
  2467  	}
  2468  	// Wait for expected db updates.
  2469  	for i := 0; i < 4; i++ {
  2470  		<-rig.db.updateMatchChan
  2471  	}
  2472  
  2473  	// check t.metaData.LinkedOrder or for db.LinkOrder call, then db.UpdateOrder call
  2474  	if tracker.metaData.LinkedOrder.IsZero() {
  2475  		t.Errorf("cancel order not set")
  2476  	}
  2477  	if rig.db.linkedFromID != oid || rig.db.linkedToID.IsZero() {
  2478  		t.Errorf("automatic cancel order not linked")
  2479  	}
  2480  
  2481  	if !tracker.matches[missingID].MetaData.Proof.SelfRevoked {
  2482  		t.Errorf("SelfRevoked not true for missing match tracker")
  2483  	}
  2484  	if tracker.matches[matchID].swapErr != nil {
  2485  		t.Errorf("swapErr set for non-missing match tracker")
  2486  	}
  2487  	if tracker.matches[matchID].MetaData.Proof.IsRevoked() {
  2488  		t.Errorf("IsRevoked true for non-missing match tracker")
  2489  	}
  2490  	// Conflict resolution will have run negotiate on the extra match from the
  2491  	// connect response, bringing our match count up to 3.
  2492  	if len(tracker.matches) != 3 {
  2493  		t.Errorf("Extra trade not accepted into matches")
  2494  	}
  2495  	tracker.mtx.Lock()
  2496  	defer tracker.mtx.Unlock()
  2497  	match = tracker.matches[extraID]
  2498  	if !bytes.Equal(match.MetaData.Proof.CounterContract, missedContract) {
  2499  		t.Errorf("Missed maker contract not retrieved, %s, %s", match, hex.EncodeToString(match.MetaData.Proof.CounterContract))
  2500  	}
  2501  }
  2502  
  2503  func TestAccountNotFoundError(t *testing.T) {
  2504  	rig := newTestRig()
  2505  	defer rig.shutdown()
  2506  	tCore := rig.core
  2507  	rig.acct.rep = account.Reputation{BondedTier: 1}
  2508  
  2509  	const expectedErrorMessage = "test account not found error"
  2510  	accountNotFoundError := msgjson.NewError(msgjson.AccountNotFoundError, expectedErrorMessage)
  2511  	rig.queueConnect(accountNotFoundError, nil, nil)
  2512  	rig.queueConnect(accountNotFoundError, nil, nil)
  2513  
  2514  	wallet, _ := newTWallet(tUTXOAssetA.ID)
  2515  	tCore.wallets[tUTXOAssetA.ID] = wallet
  2516  	rig.queueConnect(nil, nil, nil)
  2517  
  2518  	feed := tCore.NotificationFeed()
  2519  
  2520  	tCore.initializeDEXConnections(rig.crypter)
  2521  
  2522  	// Make sure that the connections did not get authenticated
  2523  	for _, dc := range tCore.dexConnections() {
  2524  		if dc.acct.authed() {
  2525  			t.Fatalf("dex connection should not have been authenticated")
  2526  		}
  2527  	}
  2528  
  2529  	// Make sure that an error notification was sent
  2530  	for {
  2531  		select {
  2532  		case note := <-feed.C:
  2533  			if note.Topic() == TopicDexAuthError && strings.Contains(note.Details(), expectedErrorMessage) {
  2534  				return
  2535  			}
  2536  		case <-time.After(1 * time.Second):
  2537  			t.Fatalf("error notification could not be found")
  2538  		}
  2539  	}
  2540  }
  2541  
  2542  func TestInitializeDEXConnectionsSuccess(t *testing.T) {
  2543  	rig := newTestRig()
  2544  	defer rig.shutdown()
  2545  	tCore := rig.core
  2546  	rig.acct.rep = account.Reputation{BondedTier: 1}
  2547  	rig.queueConnect(nil, nil, nil)
  2548  
  2549  	// Make sure that the connections got authenticated
  2550  	tCore.initializeDEXConnections(rig.crypter)
  2551  	for _, dc := range tCore.dexConnections() {
  2552  		if !dc.acct.authed() {
  2553  			t.Fatalf("dex connection was not authenticated")
  2554  		}
  2555  	}
  2556  }
  2557  
  2558  func TestConnectDEX(t *testing.T) {
  2559  	rig := newTestRig()
  2560  	defer rig.shutdown()
  2561  	tCore := rig.core
  2562  
  2563  	ai := &db.AccountInfo{
  2564  		Host: "somedex.com",
  2565  	}
  2566  
  2567  	_, err := tCore.connectDEX(ai)
  2568  	if err == nil {
  2569  		t.Fatalf("expected error for no TLS plain internet DEX host")
  2570  	}
  2571  
  2572  	ai.Host = "somedex13254214214.onion" // not a valid onion host in case we decide to validate them
  2573  	// No onion proxy set => error
  2574  	_, err = tCore.connectDEX(ai)
  2575  	if err == nil {
  2576  		t.Fatalf("expected error with no onion proxy set")
  2577  	}
  2578  
  2579  	rig.queueConfig()
  2580  	tCore.cfg.Onion = "127.0.0.1:9050"
  2581  	dc, err := tCore.connectDEX(ai)
  2582  	if err != nil {
  2583  		t.Fatalf("error connecting to onion host with an onion proxy configured: %v", err)
  2584  	}
  2585  	dc.connMaster.Disconnect()
  2586  
  2587  	rig.queueConfig()
  2588  	ai.Host = "somedex.com"
  2589  	ai.Cert = []byte{0x1}
  2590  	dc, err = tCore.connectDEX(ai)
  2591  	if err != nil {
  2592  		t.Fatalf("initial connectDEX error: %v", err)
  2593  	}
  2594  	dc.connMaster.Disconnect()
  2595  
  2596  	// Bad URL.
  2597  	ai.Host = tUnparseableHost // Illegal ASCII control character
  2598  	_, err = tCore.connectDEX(ai)
  2599  	if err == nil {
  2600  		t.Fatalf("no error for bad URL")
  2601  	}
  2602  	ai.Host = "someotherdex.org"
  2603  
  2604  	// Constructor error.
  2605  	ogConstructor := tCore.wsConstructor
  2606  	tCore.wsConstructor = func(*comms.WsCfg) (comms.WsConn, error) {
  2607  		return nil, tErr
  2608  	}
  2609  	_, err = tCore.connectDEX(ai)
  2610  	if err == nil {
  2611  		t.Fatalf("no error for WsConn constructor error")
  2612  	}
  2613  	tCore.wsConstructor = ogConstructor
  2614  
  2615  	// WsConn.Connect error.
  2616  	rig.ws.connectErr = tErr
  2617  	_, err = tCore.connectDEX(ai)
  2618  	if err == nil {
  2619  		t.Fatalf("no error for WsConn.Connect error")
  2620  	}
  2621  
  2622  	rig.ws.connectErr = nil
  2623  
  2624  	// 'config' route error.
  2625  	rig.ws.queueResponse(msgjson.ConfigRoute, func(msg *msgjson.Message, f msgFunc) error {
  2626  		resp, _ := msgjson.NewResponse(msg.ID, nil, msgjson.NewError(1, "test error"))
  2627  		f(resp)
  2628  		return nil
  2629  	})
  2630  	_, err = tCore.connectDEX(ai)
  2631  	if err == nil {
  2632  		t.Fatalf("no error for 'config' route error")
  2633  	}
  2634  
  2635  	// Success again.
  2636  	rig.queueConfig()
  2637  	dc, err = tCore.connectDEX(ai)
  2638  	if err != nil {
  2639  		t.Fatalf("final connectDEX error: %v", err)
  2640  	}
  2641  	dc.connMaster.Disconnect()
  2642  
  2643  	// TODO: test temporary, ensure listen isn't running, somehow
  2644  }
  2645  
  2646  func TestInitializeClient(t *testing.T) {
  2647  	rig := newTestRig()
  2648  	defer rig.shutdown()
  2649  	tCore := rig.core
  2650  
  2651  	clearCreds := func() {
  2652  		tCore.credentials = nil
  2653  		rig.db.creds = nil
  2654  	}
  2655  
  2656  	clearCreds()
  2657  
  2658  	_, err := tCore.InitializeClient(tPW, nil)
  2659  	if err != nil {
  2660  		t.Fatalf("InitializeClient error: %v", err)
  2661  	}
  2662  
  2663  	clearCreds()
  2664  
  2665  	// Empty password.
  2666  	emptyPass := []byte("")
  2667  	_, err = tCore.InitializeClient(emptyPass, nil)
  2668  	if err == nil {
  2669  		t.Fatalf("no error for empty password")
  2670  	}
  2671  
  2672  	// Store error. Use a non-empty password to pass empty password check.
  2673  	rig.db.setCredsErr = tErr
  2674  	_, err = tCore.InitializeClient(tPW, nil)
  2675  	if err == nil {
  2676  		t.Fatalf("no error for StoreEncryptedKey error")
  2677  	}
  2678  	rig.db.setCredsErr = nil
  2679  
  2680  	// Success again
  2681  	_, err = tCore.InitializeClient(tPW, nil)
  2682  	if err != nil {
  2683  		t.Fatalf("final InitializeClient error: %v", err)
  2684  	}
  2685  }
  2686  
  2687  func TestSend(t *testing.T) {
  2688  	rig := newTestRig()
  2689  	defer rig.shutdown()
  2690  	tCore := rig.core
  2691  	wallet, tWallet := newTWallet(tUTXOAssetA.ID)
  2692  	tCore.wallets[tUTXOAssetA.ID] = wallet
  2693  	tWallet.sendCoin = &tCoin{id: encode.RandomBytes(36)}
  2694  	address := "addr"
  2695  
  2696  	// Successful
  2697  	coin, err := tCore.Send(tPW, tUTXOAssetA.ID, 1e8, address, false)
  2698  	if err != nil {
  2699  		t.Fatalf("Send error: %v", err)
  2700  	}
  2701  	if coin.Value() != 1e8 {
  2702  		t.Fatalf("Expected sent value to be %v, got %v", 1e8, coin.Value())
  2703  	}
  2704  
  2705  	// 0 value
  2706  	_, err = tCore.Send(tPW, tUTXOAssetA.ID, 0, address, false)
  2707  	if err == nil {
  2708  		t.Fatalf("no error for zero value send")
  2709  	}
  2710  
  2711  	// no wallet
  2712  	_, err = tCore.Send(tPW, 12345, 1e8, address, false)
  2713  	if err == nil {
  2714  		t.Fatalf("no error for unknown wallet")
  2715  	}
  2716  
  2717  	// connect error
  2718  	wallet.hookedUp = false
  2719  	tWallet.connectErr = tErr
  2720  	_, err = tCore.Send(tPW, tUTXOAssetA.ID, 1e8, address, false)
  2721  	if err == nil {
  2722  		t.Fatalf("no error for wallet connect error")
  2723  	}
  2724  	tWallet.connectErr = nil
  2725  
  2726  	// Send error
  2727  	tWallet.sendErr = tErr
  2728  	_, err = tCore.Send(tPW, tUTXOAssetA.ID, 1e8, address, false)
  2729  	if err == nil {
  2730  		t.Fatalf("no error for wallet send error")
  2731  	}
  2732  	tWallet.sendErr = nil
  2733  
  2734  	// Check the coin.
  2735  	tWallet.sendCoin = &tCoin{id: []byte{'a'}}
  2736  	coin, err = tCore.Send(tPW, tUTXOAssetA.ID, 3e8, address, false)
  2737  	if err != nil {
  2738  		t.Fatalf("coin check error: %v", err)
  2739  	}
  2740  	coinID := coin.ID()
  2741  	if len(coinID) != 1 || coinID[0] != 'a' {
  2742  		t.Fatalf("coin ID not propagated")
  2743  	}
  2744  	if coin.Value() != 3e8 {
  2745  		t.Fatalf("Expected sent value to be %v, got %v", 3e8, coin.Value())
  2746  	}
  2747  
  2748  	// So far, the fee suggestion should have always been zero.
  2749  	if tWallet.sendFeeSuggestion != 0 {
  2750  		t.Fatalf("unexpected non-zero fee rate when no books or responses prepared")
  2751  	}
  2752  
  2753  	const feeRate = 54321
  2754  
  2755  	feeRater := &TFeeRater{
  2756  		TXCWallet: tWallet,
  2757  		feeRate:   feeRate,
  2758  	}
  2759  
  2760  	wallet.Wallet = feeRater
  2761  
  2762  	coin, err = tCore.Send(tPW, tUTXOAssetA.ID, 2e8, address, false)
  2763  	if err != nil {
  2764  		t.Fatalf("FeeRater Withdraw/send error: %v", err)
  2765  	}
  2766  	if coin.Value() != 2e8 {
  2767  		t.Fatalf("Expected sent value to be %v, got %v", 2e8, coin.Value())
  2768  	}
  2769  
  2770  	if tWallet.sendFeeSuggestion != feeRate {
  2771  		t.Fatalf("unexpected fee rate from FeeRater. wanted %d, got %d", feeRate, tWallet.sendFeeSuggestion)
  2772  	}
  2773  
  2774  	// wallet is not synced
  2775  	wallet.syncStatus.Synced = false
  2776  	_, err = tCore.Send(tPW, tUTXOAssetA.ID, 1e8, address, false)
  2777  	if err == nil {
  2778  		t.Fatalf("Expected error for a non-synchronized wallet")
  2779  	}
  2780  }
  2781  
  2782  func trade(t *testing.T, async bool) {
  2783  	rig := newTestRig()
  2784  	defer rig.shutdown()
  2785  	tCore := rig.core
  2786  	dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID)
  2787  	dcrWallet.hookedUp = false
  2788  	tCore.wallets[tUTXOAssetA.ID] = dcrWallet
  2789  	dcrWallet.address = "DsVmA7aqqWeKWy461hXjytbZbgCqbB8g2dq"
  2790  	dcrWallet.Unlock(rig.crypter)
  2791  	_ = dcrWallet.Connect() // connector will panic on Wait, and sync status goroutines will exit if disconnected
  2792  	defer dcrWallet.Disconnect()
  2793  	syncTickerPeriod = 10 * time.Millisecond
  2794  
  2795  	btcWallet, tBtcWallet := newTWallet(tUTXOAssetB.ID)
  2796  	tCore.wallets[tUTXOAssetB.ID] = btcWallet
  2797  	btcWallet.address = "12DXGkvxFjuq5btXYkwWfBZaz1rVwFgini"
  2798  	btcWallet.Unlock(rig.crypter)
  2799  
  2800  	ethWallet, tEthWallet := newTAccountLocker(tACCTAsset.ID)
  2801  	tCore.wallets[tACCTAsset.ID] = ethWallet
  2802  	ethWallet.address = "18d65fb8d60c1199bb1ad381be47aa692b482605"
  2803  	ethWallet.Unlock(rig.crypter)
  2804  
  2805  	var lots uint64 = 10
  2806  	qty := dcrBtcLotSize * lots
  2807  	rate := dcrBtcRateStep * 1000
  2808  
  2809  	form := &TradeForm{
  2810  		Host:    tDexHost,
  2811  		IsLimit: true,
  2812  		Sell:    true,
  2813  		Base:    tUTXOAssetA.ID,
  2814  		Quote:   tUTXOAssetB.ID,
  2815  		Qty:     qty,
  2816  		Rate:    rate,
  2817  		TifNow:  false,
  2818  	}
  2819  
  2820  	dcrCoin := &tCoin{
  2821  		id:  encode.RandomBytes(36),
  2822  		val: qty * 2,
  2823  	}
  2824  	tDcrWallet.fundingCoins = asset.Coins{dcrCoin}
  2825  	tDcrWallet.fundRedeemScripts = []dex.Bytes{nil}
  2826  
  2827  	btcVal := calc.BaseToQuote(rate, qty*2)
  2828  	btcCoin := &tCoin{
  2829  		id:  encode.RandomBytes(36),
  2830  		val: btcVal,
  2831  	}
  2832  	tBtcWallet.fundingCoins = asset.Coins{btcCoin}
  2833  	tBtcWallet.fundRedeemScripts = []dex.Bytes{nil}
  2834  
  2835  	book := newBookie(rig.dc, tUTXOAssetA.ID, tUTXOAssetB.ID, nil, tLogger)
  2836  	rig.dc.books[tDcrBtcMktName] = book
  2837  
  2838  	msgOrderNote := &msgjson.BookOrderNote{
  2839  		OrderNote: msgjson.OrderNote{
  2840  			OrderID: encode.RandomBytes(32),
  2841  		},
  2842  		TradeNote: msgjson.TradeNote{
  2843  			Side:     msgjson.SellOrderNum,
  2844  			Quantity: dcrBtcLotSize,
  2845  			Time:     uint64(time.Now().Unix()),
  2846  			Rate:     rate,
  2847  		},
  2848  	}
  2849  
  2850  	err := book.Sync(&msgjson.OrderBook{
  2851  		MarketID: tDcrBtcMktName,
  2852  		Seq:      1,
  2853  		Epoch:    1,
  2854  		Orders:   []*msgjson.BookOrderNote{msgOrderNote},
  2855  	})
  2856  	if err != nil {
  2857  		t.Fatalf("order book sync error: %v", err)
  2858  	}
  2859  
  2860  	badSig := false
  2861  	noID := false
  2862  	badID := false
  2863  	handleLimit := func(msg *msgjson.Message, f msgFunc) error {
  2864  		t.Helper()
  2865  		// Need to stamp and sign the message with the server's key.
  2866  		msgOrder := new(msgjson.LimitOrder)
  2867  		err := msg.Unmarshal(msgOrder)
  2868  		if err != nil {
  2869  			t.Fatalf("unmarshal error: %v", err)
  2870  		}
  2871  		lo := convertMsgLimitOrder(msgOrder)
  2872  		f(orderResponse(msg.ID, msgOrder, lo, badSig, noID, badID))
  2873  		return nil
  2874  	}
  2875  
  2876  	handleMarket := func(msg *msgjson.Message, f msgFunc) error {
  2877  		t.Helper()
  2878  		// Need to stamp and sign the message with the server's key.
  2879  		msgOrder := new(msgjson.MarketOrder)
  2880  		err := msg.Unmarshal(msgOrder)
  2881  		if err != nil {
  2882  			t.Fatalf("unmarshal error: %v", err)
  2883  		}
  2884  		mo := convertMsgMarketOrder(msgOrder)
  2885  		f(orderResponse(msg.ID, msgOrder, mo, badSig, noID, badID))
  2886  		return nil
  2887  	}
  2888  
  2889  	ch := tCore.NotificationFeed() // detect when sync goroutine completes
  2890  	waitForOrderNotification := func() (*Order, uint64, error) {
  2891  		var corder *Order
  2892  		var tempID uint64
  2893  	wait:
  2894  		for {
  2895  			select {
  2896  			case note := <-ch.C:
  2897  				if note.Type() == NoteTypeOrder {
  2898  					n, ok := note.(*OrderNote)
  2899  					if !ok {
  2900  						t.Fatalf("Expected OrderNote type, got %T", note)
  2901  					}
  2902  					if note.Topic() == TopicAsyncOrderSubmitted {
  2903  						tempID = n.TemporaryID
  2904  					} else if tempID == n.TemporaryID && note.Topic() == TopicAsyncOrderFailure {
  2905  						return nil, tempID, fmt.Errorf("%v", note.Details())
  2906  					} else {
  2907  						corder = n.Order
  2908  						break wait
  2909  					}
  2910  				}
  2911  			case <-time.After(1 * time.Second):
  2912  				t.Fatal("Failed to receive queued order note")
  2913  			}
  2914  		}
  2915  		return corder, tempID, nil
  2916  	}
  2917  
  2918  	trade := func() (*Order, error) {
  2919  		if !async {
  2920  			return tCore.Trade(tPW, form)
  2921  		}
  2922  
  2923  		inFlight, err := tCore.TradeAsync(tPW, form)
  2924  		if err != nil {
  2925  			return nil, err
  2926  		}
  2927  
  2928  		corder, tempID, err := waitForOrderNotification()
  2929  		if err != nil {
  2930  			return nil, err
  2931  		}
  2932  
  2933  		if inFlight.TemporaryID != tempID {
  2934  			t.Fatalf("received wrong in-flight order, expected %d got %d", inFlight.TemporaryID, tempID)
  2935  		}
  2936  
  2937  		return corder, nil
  2938  	}
  2939  
  2940  	ensureOrderErr := func(tag string, waitForErr bool) {
  2941  		t.Helper()
  2942  		var err error
  2943  		if async {
  2944  			_, err = tCore.TradeAsync(tPW, form)
  2945  		} else {
  2946  			_, err = tCore.Trade(tPW, form)
  2947  		}
  2948  		if !waitForErr && err == nil {
  2949  			t.Fatalf("%s: no error", tag)
  2950  		}
  2951  
  2952  		if waitForErr {
  2953  			_, _, err := waitForOrderNotification()
  2954  			if err == nil {
  2955  				t.Fatalf("%s: no error for queued order", tag)
  2956  			}
  2957  		}
  2958  	}
  2959  
  2960  	ensureErr := func(tag string) {
  2961  		t.Helper()
  2962  		ensureOrderErr(tag, false)
  2963  	}
  2964  
  2965  	// Initial success
  2966  	rig.ws.queueResponse(msgjson.LimitRoute, handleLimit)
  2967  	corder, err := trade()
  2968  	if err != nil {
  2969  		t.Fatalf("limit order error: %v", err)
  2970  	}
  2971  	t.Logf("Order with ID(%s) has been placed successfully!", corder.ID.String())
  2972  
  2973  	// Check that the Fund request for a limit sell came through and that
  2974  	// value was not adjusted internally with BaseToQuote.
  2975  	if tDcrWallet.fundedVal != qty {
  2976  		t.Fatalf("limit sell expected funded value %d, got %d", qty, tDcrWallet.fundedVal)
  2977  	}
  2978  	tDcrWallet.fundedVal = 0
  2979  	if tDcrWallet.fundedSwaps != lots {
  2980  		t.Fatalf("limit sell expected %d max swaps, got %d", lots, tDcrWallet.fundedSwaps)
  2981  	}
  2982  	tDcrWallet.fundedSwaps = 0
  2983  
  2984  	// Should not be able to close wallet now, since there are orders.
  2985  	if tCore.CloseWallet(tUTXOAssetA.ID) == nil {
  2986  		t.Fatalf("no error for closing DCR wallet with active orders")
  2987  	}
  2988  	if tCore.CloseWallet(tUTXOAssetB.ID) == nil {
  2989  		t.Fatalf("no error for closing BTC wallet with active orders")
  2990  	}
  2991  
  2992  	// Should not be able to disable wallet, since there are active orders.
  2993  	if tCore.ToggleWalletStatus(tUTXOAssetA.ID, true) == nil {
  2994  		t.Fatalf("no error for disabling DCR wallet with active orders")
  2995  	}
  2996  	if tCore.ToggleWalletStatus(tUTXOAssetB.ID, true) == nil {
  2997  		t.Fatalf("no error for disabling BTC wallet with active orders")
  2998  	}
  2999  
  3000  	// We want to set peerCount to 0 (from 1), but we'll do this the hard way to
  3001  	// ensure the peerChange handler works as intended.
  3002  	// dcrWallet.mtx.Lock()
  3003  	// dcrWallet.peerCount = 0
  3004  	// dcrWallet.mtx.Unlock()
  3005  	tCore.peerChange(dcrWallet, 0, nil)
  3006  	_, err = tCore.Trade(tPW, form)
  3007  	if err == nil {
  3008  		t.Fatalf("no error for no peers")
  3009  	}
  3010  	tCore.peerChange(dcrWallet, 1, nil)
  3011  
  3012  	// Dex not found
  3013  	form.Host = "someotherdex.org"
  3014  	_, err = tCore.Trade(tPW, form)
  3015  	if err == nil {
  3016  		t.Fatalf("no error for unknown dex")
  3017  	}
  3018  	form.Host = tDexHost
  3019  
  3020  	// Account locked = probably not logged in
  3021  	rig.dc.acct.lock()
  3022  	_, err = tCore.Trade(tPW, form)
  3023  	if err == nil {
  3024  		t.Fatalf("no error for disconnected dex")
  3025  	}
  3026  	rig.dc.acct.unlock(rig.crypter)
  3027  
  3028  	// DEX not connected
  3029  	atomic.StoreUint32(&rig.dc.connectionStatus, uint32(comms.Disconnected))
  3030  	_, err = tCore.Trade(tPW, form)
  3031  	if err == nil {
  3032  		t.Fatalf("no error for disconnected dex")
  3033  	}
  3034  	atomic.StoreUint32(&rig.dc.connectionStatus, uint32(comms.Connected))
  3035  
  3036  	setWalletSyncStatus := func(w *xcWallet, status bool) {
  3037  		w.mtx.Lock()
  3038  		w.syncStatus.Synced = status
  3039  		w.mtx.Unlock()
  3040  	}
  3041  
  3042  	// No base asset
  3043  	form.Base = 12345
  3044  	ensureErr("bad base asset")
  3045  	form.Base = tUTXOAssetA.ID
  3046  
  3047  	// No quote asset
  3048  	form.Quote = 12345
  3049  	ensureErr("bad quote asset")
  3050  	form.Quote = tUTXOAssetB.ID
  3051  
  3052  	// Limit order zero rate
  3053  	form.Rate = 0
  3054  	ensureErr("zero rate limit")
  3055  	form.Rate = rate
  3056  
  3057  	// No from wallet
  3058  	tCore.walletMtx.Lock()
  3059  	delete(tCore.wallets, tUTXOAssetA.ID)
  3060  	tCore.walletMtx.Unlock()
  3061  	ensureErr("no dcr wallet")
  3062  	tCore.walletMtx.Lock()
  3063  	tCore.wallets[tUTXOAssetA.ID] = dcrWallet
  3064  	tCore.walletMtx.Unlock()
  3065  
  3066  	// No to wallet
  3067  	tCore.walletMtx.Lock()
  3068  	delete(tCore.wallets, tUTXOAssetB.ID)
  3069  	tCore.walletMtx.Unlock()
  3070  	ensureErr("no btc wallet")
  3071  	tCore.walletMtx.Lock()
  3072  	tCore.wallets[tUTXOAssetB.ID] = btcWallet
  3073  	tCore.walletMtx.Unlock()
  3074  
  3075  	// Address error
  3076  	tBtcWallet.addrErr = tErr
  3077  	ensureErr("address error")
  3078  	tBtcWallet.addrErr = nil
  3079  
  3080  	// Not enough funds
  3081  	tDcrWallet.fundingCoinErr = tErr
  3082  	ensureErr("funds error")
  3083  	tDcrWallet.fundingCoinErr = nil
  3084  
  3085  	// Lot size violation
  3086  	ogQty := form.Qty
  3087  	form.Qty += dcrBtcLotSize / 2
  3088  	ensureErr("bad size")
  3089  	form.Qty = ogQty
  3090  
  3091  	// Coin signature error
  3092  	tDcrWallet.signCoinErr = tErr
  3093  	ensureErr("signature error")
  3094  	tDcrWallet.signCoinErr = nil
  3095  
  3096  	// Sync-in-progress error
  3097  	setWalletSyncStatus(dcrWallet, false)
  3098  	ensureErr("base not synced")
  3099  	setWalletSyncStatus(dcrWallet, true)
  3100  
  3101  	setWalletSyncStatus(btcWallet, false)
  3102  	ensureErr("quote not synced")
  3103  	setWalletSyncStatus(btcWallet, true)
  3104  
  3105  	// LimitRoute error
  3106  	rig.ws.reqErr = tErr
  3107  	ensureOrderErr("Request error", async)
  3108  	rig.ws.reqErr = nil
  3109  
  3110  	// The rest need a queued handler
  3111  
  3112  	// Bad signature
  3113  	rig.ws.queueResponse(msgjson.LimitRoute, handleLimit)
  3114  	badSig = true
  3115  	ensureOrderErr("bad server sig", async)
  3116  	badSig = false
  3117  
  3118  	// No order ID in response
  3119  	rig.ws.queueResponse(msgjson.LimitRoute, handleLimit)
  3120  	noID = true
  3121  	ensureOrderErr("no ID", async)
  3122  	noID = false
  3123  
  3124  	// Wrong order ID in response
  3125  	rig.ws.queueResponse(msgjson.LimitRoute, handleLimit)
  3126  	badID = true
  3127  	ensureOrderErr("no ID", async)
  3128  	badID = false
  3129  
  3130  	// Storage failure
  3131  	rig.ws.queueResponse(msgjson.LimitRoute, handleLimit)
  3132  	rig.db.updateOrderErr = tErr
  3133  	ensureOrderErr("db failure", async)
  3134  	rig.db.updateOrderErr = nil
  3135  
  3136  	// Success when buying.
  3137  	form.Sell = false
  3138  	rig.ws.queueResponse(msgjson.LimitRoute, handleLimit)
  3139  	corder, err = trade()
  3140  	if err != nil {
  3141  		t.Fatalf("limit order error: %v", err)
  3142  	}
  3143  	t.Logf("Order with ID(%s) has been placed successfully!", corder.ID.String())
  3144  
  3145  	// Check that the Fund request for a limit buy came through to the BTC wallet
  3146  	// and that the value was adjusted internally with BaseToQuote.
  3147  	expQty := calc.BaseToQuote(rate, qty)
  3148  	if tBtcWallet.fundedVal != expQty {
  3149  		t.Fatalf("limit buy expected funded value %d, got %d", expQty, tBtcWallet.fundedVal)
  3150  	}
  3151  	tBtcWallet.fundedVal = 0
  3152  	// The number of lots should still be the same as for a sell order.
  3153  	if tBtcWallet.fundedSwaps != lots {
  3154  		t.Fatalf("limit buy expected %d max swaps, got %d", lots, tBtcWallet.fundedSwaps)
  3155  	}
  3156  	tBtcWallet.fundedSwaps = 0
  3157  
  3158  	// Successful market buy order
  3159  	form.IsLimit = false
  3160  	form.Qty = calc.BaseToQuote(rate, qty)
  3161  	rig.ws.queueResponse(msgjson.MarketRoute, handleMarket)
  3162  	corder, err = trade()
  3163  	if err != nil {
  3164  		t.Fatalf("market order error: %v", err)
  3165  	}
  3166  	t.Logf("Order with ID(%s) has been placed successfully!", corder.ID.String())
  3167  
  3168  	// The funded qty for a market buy should not be adjusted.
  3169  	if tBtcWallet.fundedVal != form.Qty {
  3170  		t.Fatalf("market buy expected funded value %d, got %d", qty, tBtcWallet.fundedVal)
  3171  	}
  3172  	tBtcWallet.fundedVal = 0
  3173  	if tBtcWallet.fundedSwaps != lots {
  3174  		t.Fatalf("market buy expected %d max swaps, got %d", lots, tBtcWallet.fundedSwaps)
  3175  	}
  3176  	tBtcWallet.fundedSwaps = 0
  3177  
  3178  	// Successful market sell order.
  3179  	form.Sell = true
  3180  	form.Qty = qty
  3181  	rig.ws.queueResponse(msgjson.MarketRoute, handleMarket)
  3182  	corder, err = trade()
  3183  	if err != nil {
  3184  		t.Fatalf("market order error: %v", err)
  3185  	}
  3186  	t.Logf("Order with ID(%s) has been placed successfully!", corder.ID.String())
  3187  
  3188  	// The funded qty for a market sell order should not be adjusted.
  3189  	if tDcrWallet.fundedVal != qty {
  3190  		t.Fatalf("market sell expected funded value %d, got %d", qty, tDcrWallet.fundedVal)
  3191  	}
  3192  	if tDcrWallet.fundedSwaps != lots {
  3193  		t.Fatalf("market sell expected %d max swaps, got %d", lots, tDcrWallet.fundedSwaps)
  3194  	}
  3195  
  3196  	// Selling to an account-based quote asset.
  3197  	const reserveN = 50
  3198  	form.Base = tUTXOAssetB.ID
  3199  	form.Quote = tACCTAsset.ID
  3200  	rig.ws.queueResponse(msgjson.MarketRoute, handleMarket)
  3201  	tEthWallet.fundingMtx.Lock()
  3202  	tEthWallet.reserveNRedemptions = reserveN
  3203  	tEthWallet.fundingMtx.Unlock()
  3204  	tEthWallet.sigs = []dex.Bytes{{}}
  3205  	tEthWallet.pubKeys = []dex.Bytes{{}}
  3206  	corder, err = trade()
  3207  	if err != nil {
  3208  		t.Fatalf("account-redeemed order error: %v", err)
  3209  	}
  3210  	t.Logf("Order with ID(%s) has been placed successfully!", corder.ID.String())
  3211  
  3212  	// redeem sig error
  3213  	tEthWallet.signCoinErr = tErr
  3214  	ensureErr("redeem sig error")
  3215  	tEthWallet.signCoinErr = nil
  3216  
  3217  	// missing sig
  3218  	tEthWallet.sigs = []dex.Bytes{}
  3219  	ensureErr("no redeem sig is result")
  3220  	tEthWallet.sigs = []dex.Bytes{{}}
  3221  
  3222  	// ReserveN error
  3223  	tEthWallet.reserveNRedemptionsErr = tErr
  3224  	ensureErr("reserveN error")
  3225  	tEthWallet.reserveNRedemptionsErr = nil
  3226  
  3227  	// Funds returned for later error.
  3228  	tEthWallet.fundingMtx.Lock()
  3229  	tEthWallet.redemptionUnlocked = 0
  3230  	tEthWallet.fundingMtx.Unlock()
  3231  	rig.db.updateOrderErr = tErr
  3232  	rig.ws.queueResponse(msgjson.MarketRoute, handleMarket)
  3233  	ensureOrderErr("db error after redeem funds checked out", async)
  3234  	rig.db.updateOrderErr = nil
  3235  	tEthWallet.fundingMtx.Lock()
  3236  	defer tEthWallet.fundingMtx.Unlock()
  3237  	if tEthWallet.redemptionUnlocked != reserveN {
  3238  		t.Fatalf("redeem funds not returned")
  3239  	}
  3240  }
  3241  
  3242  func TestTrade(t *testing.T) {
  3243  	trade(t, false)
  3244  }
  3245  
  3246  func TestTradeAsync(t *testing.T) {
  3247  	trade(t, true)
  3248  }
  3249  
  3250  func TestRefundReserves(t *testing.T) {
  3251  	const reserves = 100_000
  3252  
  3253  	rig := newTestRig()
  3254  	defer rig.shutdown()
  3255  	dc := rig.dc
  3256  	tCore := rig.core
  3257  
  3258  	btcWallet, _ := newTWallet(tUTXOAssetA.ID)
  3259  	tCore.wallets[tUTXOAssetA.ID] = btcWallet
  3260  	btcWallet.address = "12DXGkvxFjuq5btXYkwWfBZaz1rVwFgini"
  3261  	btcWallet.Unlock(rig.crypter)
  3262  
  3263  	ethWallet, tEthWallet := newTAccountLocker(tACCTAsset.ID)
  3264  	tCore.wallets[tACCTAsset.ID] = ethWallet
  3265  	ethWallet.address = "18d65fb8d60c1199bb1ad381be47aa692b482605"
  3266  	ethWallet.Unlock(rig.crypter)
  3267  
  3268  	lotSize := dcrBtcLotSize
  3269  	qty := lotSize * 10
  3270  	rate := dcrBtcRateStep * 100
  3271  
  3272  	lo, dbOrder, preImg, _ := makeLimitOrder(dc, true, qty, rate)
  3273  	lo.BaseAsset = tUTXOAssetB.ID
  3274  	lo.QuoteAsset = tACCTAsset.ID
  3275  	lo.Force = order.StandingTiF
  3276  	loid := lo.ID()
  3277  
  3278  	walletSet, _, _, err := tCore.walletSet(dc, tACCTAsset.ID, tUTXOAssetA.ID, true)
  3279  	if err != nil {
  3280  		t.Fatalf("walletSet error: %v", err)
  3281  	}
  3282  
  3283  	dbOrder.MetaData.RefundReserves = reserves
  3284  
  3285  	tracker := newTrackedTrade(dbOrder, preImg, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker,
  3286  		rig.db, rig.queue, walletSet, nil, rig.core.notify, rig.core.formatDetails)
  3287  	dc.trades[loid] = tracker
  3288  	preImgC := newPreimage()
  3289  	co := &order.CancelOrder{
  3290  		P: order.Prefix{
  3291  			AccountID:  dc.acct.ID(),
  3292  			BaseAsset:  tACCTAsset.ID,
  3293  			QuoteAsset: tUTXOAssetA.ID,
  3294  			OrderType:  order.MarketOrderType,
  3295  			ClientTime: time.Now(),
  3296  			ServerTime: time.Now().Add(time.Millisecond),
  3297  			Commit:     preImgC.Commit(),
  3298  		},
  3299  	}
  3300  
  3301  	msgCancelMatch := &msgjson.Match{
  3302  		OrderID:  loid[:],
  3303  		MatchID:  encode.RandomBytes(32),
  3304  		Quantity: qty / 3,
  3305  		// empty Address signals cancel order match
  3306  	}
  3307  	sign(tDexPriv, msgCancelMatch)
  3308  
  3309  	matchQty := qty * 2 / 3
  3310  	matchReserves := applyFraction(2, 3, reserves)
  3311  	msgMatch := &msgjson.Match{
  3312  		OrderID:  loid[:],
  3313  		MatchID:  encode.RandomBytes(32),
  3314  		Quantity: matchQty,
  3315  		Rate:     rate,
  3316  		Address:  "somenonemptyaddress",
  3317  	}
  3318  	sign(tDexPriv, msgMatch)
  3319  
  3320  	test := func(tag string, expUnlock uint64, f func()) {
  3321  		t.Helper()
  3322  		tEthWallet.refundUnlocked = 0
  3323  		tracker.refundLocked = reserves
  3324  		tracker.metaData.Status = order.OrderStatusEpoch
  3325  		f()
  3326  		if tEthWallet.refundUnlocked != expUnlock {
  3327  			t.Fatalf("%s: expected %d to be unlocked. saw %d", tag, expUnlock, tEthWallet.refundUnlocked)
  3328  		}
  3329  	}
  3330  
  3331  	test("revoke_order in epoch", reserves, func() {
  3332  		tracker.revoke()
  3333  	})
  3334  
  3335  	test("revoke_order in booked, partial fill", reserves/2, func() {
  3336  		// Revoke in booked with partial fill.
  3337  		tracker.Trade().SetFill(qty / 2)
  3338  		tracker.metaData.Status = order.OrderStatusBooked
  3339  		tracker.revoke()
  3340  	})
  3341  
  3342  	test("canceled, partially filled", reserves/3, func() {
  3343  		tracker.cancel = &trackedCancel{CancelOrder: *co}
  3344  		msgCancel, _ := msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{msgMatch, msgCancelMatch})
  3345  		if err := handleMatchRoute(tCore, rig.dc, msgCancel); err != nil {
  3346  			t.Fatalf("handleMatchRoute error: %v", err)
  3347  		}
  3348  	})
  3349  
  3350  	tracker.cancel = nil
  3351  
  3352  	lo.Force = order.ImmediateTiF
  3353  	loid = lo.ID()
  3354  	msgMatch.OrderID = loid[:]
  3355  	sign(tDexPriv, msgMatch)
  3356  
  3357  	test("partial immediate TiF limit order", reserves/3, func() {
  3358  		matchReq, _ := msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{msgMatch})
  3359  		if err := handleMatchRoute(tCore, rig.dc, matchReq); err != nil {
  3360  			t.Fatalf("handleMatchRoute error: %v", err)
  3361  		}
  3362  	})
  3363  
  3364  	lo.Force = order.StandingTiF
  3365  	loid = lo.ID()
  3366  	msgMatch.OrderID = loid[:]
  3367  	sign(tDexPriv, msgMatch)
  3368  
  3369  	addMatch := func(side order.MatchSide, status order.MatchStatus, qty uint64) order.MatchID {
  3370  		t.Helper()
  3371  		msgMatch.Side = uint8(side)
  3372  		m := *msgMatch
  3373  		var mid order.MatchID
  3374  		copy(mid[:], encode.RandomBytes(32))
  3375  		m.MatchID = mid[:]
  3376  		m.Quantity = qty
  3377  		sign(tDexPriv, &m)
  3378  		matchReq, _ := msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{&m})
  3379  		if err := handleMatchRoute(tCore, rig.dc, matchReq); err != nil {
  3380  			t.Fatalf("handleMatchRoute error: %v", err)
  3381  		}
  3382  		mt, ok := tracker.matches[mid]
  3383  		if !ok {
  3384  			t.Fatalf("match not found")
  3385  		}
  3386  		mt.Status = status
  3387  		if status >= order.TakerSwapCast {
  3388  			mt.counterSwap = &asset.AuditInfo{}
  3389  		}
  3390  		return mid
  3391  	}
  3392  
  3393  	resetMatches := func() {
  3394  		tracker.matches = make(map[order.MatchID]*matchTracker)
  3395  	}
  3396  
  3397  	test("redemption received", reserves/10, func() {
  3398  		mid := addMatch(order.Taker, order.TakerSwapCast, lotSize)
  3399  		redemption := &msgjson.Redemption{
  3400  			Redeem: msgjson.Redeem{
  3401  				OrderID: loid[:],
  3402  				MatchID: mid[:],
  3403  				CoinID:  encode.RandomBytes(36),
  3404  			},
  3405  		}
  3406  		tracker.processRedemption(1, redemption)
  3407  	})
  3408  
  3409  	// Market sell order
  3410  	mo := &order.MarketOrder{
  3411  		P: lo.P,
  3412  		T: *lo.Trade(),
  3413  	}
  3414  	mo.Prefix().OrderType = order.MarketOrderType
  3415  	moid := mo.ID()
  3416  	dbOrder.Order = mo
  3417  	msgMatch.OrderID = moid[:]
  3418  	sign(tDexPriv, msgMatch)
  3419  
  3420  	tracker = newTrackedTrade(dbOrder, preImg, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker,
  3421  		rig.db, rig.queue, walletSet, nil, rig.core.notify, rig.core.formatDetails)
  3422  	dc.trades = map[order.OrderID]*trackedTrade{moid: tracker}
  3423  
  3424  	test("nomatch", reserves, func() {
  3425  		tracker.nomatch(moid)
  3426  	})
  3427  
  3428  	test("partial market sell match", reserves/3, func() {
  3429  		matchReq, _ := msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{msgMatch})
  3430  		if err := handleMatchRoute(tCore, rig.dc, matchReq); err != nil {
  3431  			t.Fatalf("handleMatchRoute error: %v", err)
  3432  		}
  3433  	})
  3434  
  3435  	resetMatches()
  3436  
  3437  	testRevokeMatch := func(side order.MatchSide, status order.MatchStatus, expReserves uint64) {
  3438  		t.Helper()
  3439  		resetMatches()
  3440  		matchID := addMatch(side, status, matchQty)
  3441  		desc := fmt.Sprintf("match revoke - %s in %s", side, status)
  3442  		test(desc, expReserves, func() {
  3443  			tracker.revokeMatch(matchID, true)
  3444  		})
  3445  	}
  3446  
  3447  	testRevokeMatch(order.Maker, order.NewlyMatched, matchReserves)
  3448  
  3449  	testRevokeMatch(order.Taker, order.NewlyMatched, matchReserves)
  3450  
  3451  	testRevokeMatch(order.Taker, order.MakerSwapCast, matchReserves)
  3452  
  3453  	// But Maker in MakerSwapCast shouldn't return reserves, because they will
  3454  	// need to do a refund
  3455  	testRevokeMatch(order.Maker, order.MakerSwapCast, 0)
  3456  
  3457  	// Similarly Taker in TakerSwapCast shouldn't return anything, since
  3458  	// they will need to do a refund.
  3459  	testRevokeMatch(order.Taker, order.TakerSwapCast, 0)
  3460  
  3461  	resetMatches()
  3462  
  3463  	// Market buy order
  3464  	mo.BaseAsset, mo.QuoteAsset = mo.QuoteAsset, mo.BaseAsset
  3465  	mo.Sell = false
  3466  	tracker.wallets, _, _, _ = tCore.walletSet(dc, tUTXOAssetA.ID, tACCTAsset.ID, false)
  3467  
  3468  	test("redemption received, market buy", reserves, func() {
  3469  		mid := addMatch(order.Taker, order.TakerSwapCast, lotSize)
  3470  		redemption := &msgjson.Redemption{
  3471  			Redeem: msgjson.Redeem{
  3472  				OrderID: loid[:],
  3473  				MatchID: mid[:],
  3474  				CoinID:  encode.RandomBytes(36),
  3475  			},
  3476  		}
  3477  		tracker.processRedemption(1, redemption)
  3478  	})
  3479  
  3480  	resetMatches()
  3481  	mids := []order.MatchID{
  3482  		addMatch(order.Maker, order.NewlyMatched, lotSize*2),
  3483  		addMatch(order.Maker, order.NewlyMatched, lotSize*2),
  3484  		addMatch(order.Maker, order.NewlyMatched, lotSize*2),
  3485  	}
  3486  
  3487  	tracker.refundLocked = reserves
  3488  	tEthWallet.refundUnlocked = 0
  3489  	for _, mid := range mids {
  3490  		// Third match should catch the market buy order dust filter.
  3491  		if err := tracker.revokeMatch(mid, true); err != nil {
  3492  			t.Fatalf("revokeMatch error: %v", err)
  3493  		}
  3494  	}
  3495  	if tracker.refundLocked != 0 {
  3496  		t.Fatalf("redemptionLocked (1/3) * 3 != 1: %d still reserved of %d", tracker.refundLocked, reserves)
  3497  	}
  3498  	if tEthWallet.refundUnlocked != reserves {
  3499  		t.Fatalf("redemptionUnlocked (1/3) * 3 != 1: %d returned of %d", tEthWallet.refundUnlocked, reserves)
  3500  	}
  3501  }
  3502  
  3503  func TestRedemptionReserves(t *testing.T) {
  3504  	const reserves = 100_000
  3505  
  3506  	rig := newTestRig()
  3507  	defer rig.shutdown()
  3508  	dc := rig.dc
  3509  	tCore := rig.core
  3510  
  3511  	btcWallet, _ := newTWallet(tUTXOAssetB.ID)
  3512  	tCore.wallets[tUTXOAssetB.ID] = btcWallet
  3513  	btcWallet.address = "12DXGkvxFjuq5btXYkwWfBZaz1rVwFgini"
  3514  	btcWallet.Unlock(rig.crypter)
  3515  
  3516  	ethWallet, tEthWallet := newTAccountLocker(tACCTAsset.ID)
  3517  	tCore.wallets[tACCTAsset.ID] = ethWallet
  3518  	ethWallet.address = "18d65fb8d60c1199bb1ad381be47aa692b482605"
  3519  	ethWallet.Unlock(rig.crypter)
  3520  
  3521  	lotSize := dcrBtcLotSize
  3522  	qty := lotSize * 10
  3523  	rate := dcrBtcRateStep * 100
  3524  
  3525  	lo, dbOrder, preImg, _ := makeLimitOrder(dc, true, qty, rate)
  3526  	lo.BaseAsset = tUTXOAssetB.ID
  3527  	lo.QuoteAsset = tACCTAsset.ID
  3528  	lo.Force = order.StandingTiF
  3529  	loid := lo.ID()
  3530  
  3531  	walletSet, _, _, err := tCore.walletSet(dc, tUTXOAssetB.ID, tACCTAsset.ID, true)
  3532  	if err != nil {
  3533  		t.Fatalf("walletSet error: %v", err)
  3534  	}
  3535  
  3536  	dbOrder.MetaData.RedemptionReserves = reserves
  3537  
  3538  	tracker := newTrackedTrade(dbOrder, preImg, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker,
  3539  		rig.db, rig.queue, walletSet, nil, rig.core.notify, rig.core.formatDetails)
  3540  	dc.trades[loid] = tracker
  3541  	preImgC := newPreimage()
  3542  	co := &order.CancelOrder{
  3543  		P: order.Prefix{
  3544  			AccountID:  dc.acct.ID(),
  3545  			BaseAsset:  tACCTAsset.ID,
  3546  			QuoteAsset: tUTXOAssetB.ID,
  3547  			OrderType:  order.MarketOrderType,
  3548  			ClientTime: time.Now(),
  3549  			ServerTime: time.Now().Add(time.Millisecond),
  3550  			Commit:     preImgC.Commit(),
  3551  		},
  3552  	}
  3553  
  3554  	msgCancelMatch := &msgjson.Match{
  3555  		OrderID:  loid[:],
  3556  		MatchID:  encode.RandomBytes(32),
  3557  		Quantity: qty / 3,
  3558  		// empty Address signals cancel order match
  3559  	}
  3560  	sign(tDexPriv, msgCancelMatch)
  3561  
  3562  	matchQty := qty * 2 / 3
  3563  	matchReserves := applyFraction(2, 3, reserves)
  3564  	msgMatch := &msgjson.Match{
  3565  		OrderID:  loid[:],
  3566  		MatchID:  encode.RandomBytes(32),
  3567  		Quantity: matchQty,
  3568  		Rate:     rate,
  3569  		Address:  "somenonemptyaddress",
  3570  	}
  3571  	sign(tDexPriv, msgMatch)
  3572  
  3573  	test := func(tag string, expUnlock uint64, f func()) {
  3574  		t.Helper()
  3575  		tEthWallet.redemptionUnlocked = 0
  3576  		tracker.redemptionLocked = reserves
  3577  		tracker.metaData.Status = order.OrderStatusEpoch
  3578  		f()
  3579  		if tEthWallet.redemptionUnlocked != expUnlock {
  3580  			t.Fatalf("%s: expected %d to be unlocked. saw %d", tag, expUnlock, tEthWallet.redemptionUnlocked)
  3581  		}
  3582  	}
  3583  
  3584  	test("revoke_order in epoch", reserves, func() {
  3585  		tracker.revoke()
  3586  	})
  3587  
  3588  	test("revoke_order in booked, partial fill", reserves/2, func() {
  3589  		// Revoke in booked with partial fill.
  3590  		tracker.Trade().SetFill(qty / 2)
  3591  		tracker.metaData.Status = order.OrderStatusBooked
  3592  		tracker.revoke()
  3593  	})
  3594  
  3595  	test("canceled, partially filled", reserves/3, func() {
  3596  		tracker.cancel = &trackedCancel{CancelOrder: *co}
  3597  		msgCancel, _ := msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{msgMatch, msgCancelMatch})
  3598  		if err := handleMatchRoute(tCore, rig.dc, msgCancel); err != nil {
  3599  			t.Fatalf("handleMatchRoute error: %v", err)
  3600  		}
  3601  	})
  3602  
  3603  	tracker.cancel = nil
  3604  
  3605  	lo.Force = order.ImmediateTiF
  3606  	loid = lo.ID()
  3607  	msgMatch.OrderID = loid[:]
  3608  	sign(tDexPriv, msgMatch)
  3609  
  3610  	test("partially filled immediate TiF limit order", reserves/3, func() {
  3611  		matchReq, _ := msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{msgMatch})
  3612  		if err := handleMatchRoute(tCore, rig.dc, matchReq); err != nil {
  3613  			t.Fatalf("handleMatchRoute error: %v", err)
  3614  		}
  3615  	})
  3616  
  3617  	mo := &order.MarketOrder{
  3618  		P: lo.P,
  3619  		T: *lo.Trade(),
  3620  	}
  3621  	mo.Prefix().OrderType = order.MarketOrderType
  3622  	moid := mo.ID()
  3623  	dbOrder.Order = mo
  3624  	msgMatch.OrderID = moid[:]
  3625  	sign(tDexPriv, msgMatch)
  3626  
  3627  	tracker = newTrackedTrade(dbOrder, preImg, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker,
  3628  		rig.db, rig.queue, walletSet, nil, rig.core.notify, rig.core.formatDetails)
  3629  	dc.trades = map[order.OrderID]*trackedTrade{moid: tracker}
  3630  
  3631  	test("nomatch", reserves, func() {
  3632  		tracker.nomatch(moid)
  3633  	})
  3634  
  3635  	test("partial market sell match", reserves/3, func() {
  3636  		matchReq, _ := msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{msgMatch})
  3637  		if err := handleMatchRoute(tCore, rig.dc, matchReq); err != nil {
  3638  			t.Fatalf("handleMatchRoute error: %v", err)
  3639  		}
  3640  	})
  3641  
  3642  	addMatch := func(side order.MatchSide, status order.MatchStatus, qty uint64) order.MatchID {
  3643  		msgMatch.Side = uint8(side)
  3644  		m := *msgMatch
  3645  		var mid order.MatchID
  3646  		copy(mid[:], encode.RandomBytes(32))
  3647  		m.MatchID = mid[:]
  3648  		m.Quantity = qty
  3649  		sign(tDexPriv, &m)
  3650  		matchReq, _ := msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{&m})
  3651  		if err := handleMatchRoute(tCore, rig.dc, matchReq); err != nil {
  3652  			t.Fatalf("handleMatchRoute error: %v", err)
  3653  		}
  3654  		mt, ok := tracker.matches[mid]
  3655  		if !ok {
  3656  			t.Fatalf("match not found")
  3657  		}
  3658  		mt.Status = status
  3659  		return mid
  3660  	}
  3661  
  3662  	resetMatches := func() {
  3663  		tracker.matches = make(map[order.MatchID]*matchTracker)
  3664  	}
  3665  
  3666  	testRevokeMatch := func(side order.MatchSide, status order.MatchStatus, expReserves uint64) {
  3667  		t.Helper()
  3668  		resetMatches()
  3669  		matchID := addMatch(side, status, matchQty)
  3670  		desc := fmt.Sprintf("match revoke - %s in %s", side, status)
  3671  		test(desc, expReserves, func() {
  3672  			tracker.revokeMatch(matchID, true)
  3673  		})
  3674  	}
  3675  
  3676  	testRevokeMatch(order.Maker, order.NewlyMatched, matchReserves)
  3677  
  3678  	testRevokeMatch(order.Taker, order.NewlyMatched, matchReserves)
  3679  
  3680  	testRevokeMatch(order.Taker, order.MakerSwapCast, matchReserves)
  3681  
  3682  	// But Maker in MakerSwapCast shouldn't return reserves, since the trade
  3683  	// will proceed to redeem.
  3684  	testRevokeMatch(order.Maker, order.MakerSwapCast, 0)
  3685  
  3686  	// Similarly Taker in TakerSwapCast shouldn't return anything, since we will
  3687  	// be watching for a redemption.
  3688  	testRevokeMatch(order.Taker, order.TakerSwapCast, 0)
  3689  
  3690  	// Market buy order with dust handling.
  3691  	mo.BaseAsset, mo.QuoteAsset = mo.QuoteAsset, mo.BaseAsset
  3692  	mo.Sell = false
  3693  	tracker.wallets, _, _, _ = tCore.walletSet(dc, tACCTAsset.ID, tUTXOAssetB.ID, false)
  3694  
  3695  	resetMatches()
  3696  	mids := []order.MatchID{
  3697  		addMatch(order.Maker, order.NewlyMatched, lotSize*2),
  3698  		addMatch(order.Maker, order.NewlyMatched, lotSize*2),
  3699  		addMatch(order.Maker, order.NewlyMatched, lotSize*2),
  3700  	}
  3701  
  3702  	tracker.redemptionLocked = reserves
  3703  	tEthWallet.redemptionUnlocked = 0
  3704  	for _, mid := range mids {
  3705  		// Third match should catch the market buy order dust filter.
  3706  		if err := tracker.revokeMatch(mid, true); err != nil {
  3707  			t.Fatalf("revokeMatch error: %v", err)
  3708  		}
  3709  	}
  3710  	if tracker.redemptionLocked != 0 {
  3711  		t.Fatalf("redemptionLocked (1/3) * 3 != 1: %d still reserved of %d", tracker.redemptionLocked, reserves)
  3712  	}
  3713  	if tEthWallet.redemptionUnlocked != reserves {
  3714  		t.Fatalf("redemptionUnlocked (1/3) * 3 != 1: %d returned of %d", tEthWallet.redemptionUnlocked, reserves)
  3715  	}
  3716  }
  3717  
  3718  func TestCancel(t *testing.T) {
  3719  	rig := newTestRig()
  3720  	defer rig.shutdown()
  3721  	dc := rig.dc
  3722  	lo, dbOrder, preImg, _ := makeLimitOrder(dc, true, 0, 0)
  3723  	lo.Force = order.StandingTiF
  3724  	oid := lo.ID()
  3725  	tracker := newTrackedTrade(dbOrder, preImg, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker,
  3726  		rig.db, rig.queue, nil, nil, rig.core.notify, rig.core.formatDetails)
  3727  	dc.trades[oid] = tracker
  3728  
  3729  	rig.queueCancel(nil)
  3730  	err := rig.core.Cancel(oid[:])
  3731  	if err != nil {
  3732  		t.Fatalf("cancel error: %v", err)
  3733  	}
  3734  	if tracker.cancel == nil {
  3735  		t.Fatalf("cancel order not found")
  3736  	}
  3737  
  3738  	ensureErr := func(tag string) {
  3739  		t.Helper()
  3740  		err := rig.core.Cancel(oid[:])
  3741  		if err == nil {
  3742  			t.Fatalf("%s: no error", tag)
  3743  		}
  3744  	}
  3745  
  3746  	// Should get an error for existing cancel order.
  3747  	ensureErr("second cancel")
  3748  
  3749  	// remove the cancel order so we can check its nilness on error.
  3750  	tracker.cancel = nil
  3751  
  3752  	ensureNilCancel := func(tag string) {
  3753  		if tracker.cancel != nil {
  3754  			t.Fatalf("%s: cancel order found", tag)
  3755  		}
  3756  	}
  3757  
  3758  	// Bad order ID
  3759  	ogID := oid
  3760  	oid = order.OrderID{0x01, 0x02}
  3761  	ensureErr("bad id")
  3762  	ensureNilCancel("bad id")
  3763  	oid = ogID
  3764  
  3765  	// Order not found
  3766  	delete(dc.trades, oid)
  3767  	ensureErr("no order")
  3768  	ensureNilCancel("no order")
  3769  	dc.trades[oid] = tracker
  3770  
  3771  	// Send error
  3772  	rig.ws.reqErr = tErr
  3773  	ensureErr("Request error")
  3774  	ensureNilCancel("Request error")
  3775  	rig.ws.reqErr = nil
  3776  }
  3777  
  3778  func TestHandlePreimageRequest(t *testing.T) {
  3779  	t.Run("basic checks", func(t *testing.T) {
  3780  		rig := newTestRig()
  3781  		defer rig.shutdown()
  3782  		ord := &order.LimitOrder{P: order.Prefix{ServerTime: time.Now()}}
  3783  		oid := ord.ID()
  3784  		preImg := newPreimage()
  3785  
  3786  		// It is no longer OK for server to omit the commitment.
  3787  		payload := &msgjson.PreimageRequest{
  3788  			OrderID: oid[:],
  3789  			// No commitment in this request.
  3790  		}
  3791  		reqNoCommit, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.PreimageRoute, payload)
  3792  		// mkt := dc.marketConfig(tDcrBtcMktName)
  3793  
  3794  		tracker := &trackedTrade{
  3795  			Order:    ord,
  3796  			preImg:   preImg,
  3797  			mktID:    tDcrBtcMktName,
  3798  			db:       rig.db,
  3799  			dc:       rig.dc,
  3800  			metaData: &db.OrderMetaData{},
  3801  		}
  3802  
  3803  		// resetCsum resets csum for further preimage request since multiple
  3804  		// testing scenarios use the same tracker object.
  3805  		resetCsum := func(tracker *trackedTrade) {
  3806  			tracker.csumMtx.Lock()
  3807  			tracker.csum = nil
  3808  			tracker.csumMtx.Unlock()
  3809  		}
  3810  
  3811  		rig.dc.trades[oid] = tracker
  3812  		err := handlePreimageRequest(rig.core, rig.dc, reqNoCommit)
  3813  		if err == nil {
  3814  			t.Fatalf("handlePreimageRequest succeeded with no commitment in the request")
  3815  		}
  3816  		resetCsum(tracker)
  3817  
  3818  		// Test the new path with rig.core.sentCommits.
  3819  		readyCommitment := func(commit order.Commitment) chan struct{} {
  3820  			commitSig := make(chan struct{}) // close after fake order submission is "done"
  3821  			rig.core.sentCommitsMtx.Lock()
  3822  			rig.core.sentCommits[commit] = commitSig
  3823  			rig.core.sentCommitsMtx.Unlock()
  3824  			return commitSig
  3825  		}
  3826  
  3827  		commit := preImg.Commit()
  3828  		commitSig := readyCommitment(commit)
  3829  		payload = &msgjson.PreimageRequest{
  3830  			OrderID:    oid[:],
  3831  			Commitment: commit[:],
  3832  		}
  3833  		reqCommit, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.PreimageRoute, payload)
  3834  
  3835  		notes := rig.core.NotificationFeed()
  3836  
  3837  		rig.dc.trades[oid] = tracker
  3838  		err = handlePreimageRequest(rig.core, rig.dc, reqCommit)
  3839  		if err != nil {
  3840  			t.Fatalf("handlePreimageRequest error: %v", err)
  3841  		}
  3842  		resetCsum(tracker)
  3843  
  3844  		// It has gone async now, waiting for commitSig.
  3845  		// i.e. "Received preimage request for %v with no corresponding order submission response! Waiting..."
  3846  		close(commitSig) // pretend like the order submission just finished
  3847  
  3848  		select {
  3849  		case note := <-notes.C:
  3850  			if note.Topic() != TopicPreimageSent {
  3851  				t.Fatalf("note subject is %v, not %v", note.Topic(), TopicPreimageSent)
  3852  			}
  3853  		case <-time.After(time.Second):
  3854  			t.Fatal("no order note from preimage request handling")
  3855  		}
  3856  
  3857  		// negative paths
  3858  		ensureErr := func(tag string, req *msgjson.Message, errPrefix string) {
  3859  			t.Helper()
  3860  			commitSig := readyCommitment(commit)
  3861  			close(commitSig) // ready before preimage request
  3862  			err := handlePreimageRequest(rig.core, rig.dc, req)
  3863  			if err == nil {
  3864  				t.Fatalf("%s: no error", tag)
  3865  			}
  3866  			if !strings.HasPrefix(err.Error(), errPrefix) {
  3867  				t.Fatalf("expected error starting with %q, got %q", errPrefix, err)
  3868  			}
  3869  			resetCsum(tracker)
  3870  		}
  3871  
  3872  		// unknown commitment in request
  3873  		payloadBad := &msgjson.PreimageRequest{
  3874  			OrderID:    oid[:],
  3875  			Commitment: encode.RandomBytes(order.CommitmentSize), // junk, but correct length
  3876  		}
  3877  		reqCommitBad, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.PreimageRoute, payloadBad)
  3878  		ensureErr("unknown commitment", reqCommitBad, "received preimage request for unknown commitment")
  3879  	})
  3880  	t.Run("csum for order", func(t *testing.T) {
  3881  		rig := newTestRig()
  3882  		defer rig.shutdown()
  3883  		ord := &order.LimitOrder{P: order.Prefix{ServerTime: time.Now()}}
  3884  		oid := ord.ID()
  3885  		preImg := newPreimage()
  3886  		// mkt := dc.marketConfig(tDcrBtcMktName)
  3887  
  3888  		tracker := &trackedTrade{
  3889  			Order:    ord,
  3890  			preImg:   preImg,
  3891  			mktID:    tDcrBtcMktName,
  3892  			db:       rig.db,
  3893  			dc:       rig.dc,
  3894  			metaData: &db.OrderMetaData{},
  3895  		}
  3896  
  3897  		// Test the new path with rig.core.sentCommits.
  3898  		readyCommitment := func(commit order.Commitment) chan struct{} {
  3899  			commitSig := make(chan struct{}) // close after fake order submission is "done"
  3900  			rig.core.sentCommitsMtx.Lock()
  3901  			rig.core.sentCommits[commit] = commitSig
  3902  			rig.core.sentCommitsMtx.Unlock()
  3903  			return commitSig
  3904  		}
  3905  
  3906  		commit := preImg.Commit()
  3907  		commitCSum := dex.Bytes{2, 3, 5, 7, 11, 13}
  3908  		commitSig := readyCommitment(commit)
  3909  		payload := &msgjson.PreimageRequest{
  3910  			OrderID:        oid[:],
  3911  			Commitment:     commit[:],
  3912  			CommitChecksum: commitCSum,
  3913  		}
  3914  		reqCommit, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.PreimageRoute, payload)
  3915  
  3916  		notes := rig.core.NotificationFeed()
  3917  
  3918  		rig.dc.trades[oid] = tracker
  3919  		err := handlePreimageRequest(rig.core, rig.dc, reqCommit)
  3920  		if err != nil {
  3921  			t.Fatalf("handlePreimageRequest error: %v", err)
  3922  		}
  3923  
  3924  		// It has gone async now, waiting for commitSig.
  3925  		// i.e. "Received preimage request for %v with no corresponding order submission response! Waiting..."
  3926  		close(commitSig) // pretend like the order submission just finished
  3927  
  3928  		select {
  3929  		case note := <-notes.C:
  3930  			if note.Topic() != TopicPreimageSent {
  3931  				t.Fatalf("note subject is %v, not %v", note.Topic(), TopicPreimageSent)
  3932  			}
  3933  		case <-time.After(time.Second):
  3934  			t.Fatal("no order note from preimage request handling")
  3935  		}
  3936  
  3937  		tracker.csumMtx.RLock()
  3938  		csum := tracker.csum
  3939  		tracker.csumMtx.RUnlock()
  3940  		if !bytes.Equal(commitCSum, csum) {
  3941  			t.Fatalf(
  3942  				"handlePreimageRequest must initialize tracker csum, exp: %s, got: %s",
  3943  				commitCSum,
  3944  				csum,
  3945  			)
  3946  		}
  3947  
  3948  	})
  3949  	t.Run("more than one preimage request for order (different csums)", func(t *testing.T) {
  3950  		rig := newTestRig()
  3951  		defer rig.shutdown()
  3952  		ord := &order.LimitOrder{P: order.Prefix{ServerTime: time.Now()}}
  3953  		oid := ord.ID()
  3954  		preImg := newPreimage()
  3955  		// mkt := dc.marketConfig(tDcrBtcMktName)
  3956  		firstCSum := dex.Bytes{2, 3, 5, 7, 11, 13}
  3957  
  3958  		tracker := &trackedTrade{
  3959  			Order:  ord,
  3960  			preImg: preImg,
  3961  			mktID:  tDcrBtcMktName,
  3962  			db:     rig.db,
  3963  			dc:     rig.dc,
  3964  			// Simulate first preimage request by initializing csum here.
  3965  			csum:     firstCSum,
  3966  			metaData: &db.OrderMetaData{},
  3967  		}
  3968  
  3969  		// Test the new path with rig.core.sentCommits.
  3970  		readyCommitment := func(commit order.Commitment) chan struct{} {
  3971  			commitSig := make(chan struct{}) // close after fake order submission is "done"
  3972  			rig.core.sentCommitsMtx.Lock()
  3973  			rig.core.sentCommits[commit] = commitSig
  3974  			rig.core.sentCommitsMtx.Unlock()
  3975  			return commitSig
  3976  		}
  3977  
  3978  		commit := preImg.Commit()
  3979  		commitSig := readyCommitment(commit)
  3980  		secondCSum := dex.Bytes{2, 3, 5, 7, 11, 14}
  3981  		payload := &msgjson.PreimageRequest{
  3982  			OrderID:        oid[:],
  3983  			Commitment:     commit[:],
  3984  			CommitChecksum: secondCSum,
  3985  		}
  3986  		reqCommit, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.PreimageRoute, payload)
  3987  
  3988  		// Prepare to have processPreimageRequest respond with a payload with
  3989  		// the Error field set.
  3990  		rig.ws.sendMsgErrChan = make(chan *msgjson.Error, 1)
  3991  		defer func() { rig.ws.sendMsgErrChan = nil }()
  3992  
  3993  		rig.dc.trades[oid] = tracker
  3994  		err := handlePreimageRequest(rig.core, rig.dc, reqCommit)
  3995  		if err != nil {
  3996  			t.Fatalf("handlePreimageRequest error: %v", err)
  3997  		}
  3998  
  3999  		// It has gone async now, waiting for commitSig.
  4000  		// i.e. "Received preimage request for %v with no corresponding order submission response! Waiting..."
  4001  		close(commitSig) // pretend like the order submission just finished
  4002  
  4003  		select {
  4004  		case msgErr := <-rig.ws.sendMsgErrChan:
  4005  			if msgErr.Code != msgjson.InvalidRequestError {
  4006  				t.Fatalf("expected error code %d got %d", msgjson.InvalidRequestError, msgErr.Code)
  4007  			}
  4008  		case <-time.After(time.Second):
  4009  			t.Fatal("no msgjson.Error sent from preimage request handling")
  4010  		}
  4011  
  4012  		tracker.csumMtx.RLock()
  4013  		csum := tracker.csum
  4014  		tracker.csumMtx.RUnlock()
  4015  		if !bytes.Equal(firstCSum, csum) {
  4016  			t.Fatalf(
  4017  				"[handlePreimageRequest] csum was changed, exp: %s, got: %s",
  4018  				firstCSum,
  4019  				csum,
  4020  			)
  4021  		}
  4022  
  4023  	})
  4024  	t.Run("more than one preimage request for order (same csum)", func(t *testing.T) {
  4025  		rig := newTestRig()
  4026  		defer rig.shutdown()
  4027  		ord := &order.LimitOrder{P: order.Prefix{ServerTime: time.Now()}}
  4028  		oid := ord.ID()
  4029  		preImg := newPreimage()
  4030  		// mkt := dc.marketConfig(tDcrBtcMktName)
  4031  		csum := dex.Bytes{2, 3, 5, 7, 11, 13}
  4032  
  4033  		tracker := &trackedTrade{
  4034  			Order:  ord,
  4035  			preImg: preImg,
  4036  			mktID:  tDcrBtcMktName,
  4037  			db:     rig.db,
  4038  			dc:     rig.dc,
  4039  			// Simulate first preimage request by initializing csum here.
  4040  			csum:     csum,
  4041  			metaData: &db.OrderMetaData{},
  4042  		}
  4043  
  4044  		// Test the new path with rig.core.sentCommits.
  4045  		readyCommitment := func(commit order.Commitment) chan struct{} {
  4046  			commitSig := make(chan struct{}) // close after fake order submission is "done"
  4047  			rig.core.sentCommitsMtx.Lock()
  4048  			rig.core.sentCommits[commit] = commitSig
  4049  			rig.core.sentCommitsMtx.Unlock()
  4050  			return commitSig
  4051  		}
  4052  
  4053  		commit := preImg.Commit()
  4054  		commitSig := readyCommitment(commit)
  4055  		payload := &msgjson.PreimageRequest{
  4056  			OrderID:        oid[:],
  4057  			Commitment:     commit[:],
  4058  			CommitChecksum: csum,
  4059  		}
  4060  		reqCommit, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.PreimageRoute, payload)
  4061  
  4062  		notes := rig.core.NotificationFeed()
  4063  
  4064  		rig.dc.trades[oid] = tracker
  4065  		err := handlePreimageRequest(rig.core, rig.dc, reqCommit)
  4066  		if err != nil {
  4067  			t.Fatalf("handlePreimageRequest error: %v", err)
  4068  		}
  4069  
  4070  		// It has gone async now, waiting for commitSig.
  4071  		// i.e. "Received preimage request for %v with no corresponding order submission response! Waiting..."
  4072  		close(commitSig) // pretend like the order submission just finished
  4073  
  4074  		select {
  4075  		case note := <-notes.C:
  4076  			if note.Topic() != TopicPreimageSent {
  4077  				t.Fatalf("note subject is %v, not %v", note.Topic(), TopicPreimageSent)
  4078  			}
  4079  		case <-time.After(time.Second):
  4080  			t.Fatal("no order note from preimage request handling")
  4081  		}
  4082  
  4083  		tracker.csumMtx.RLock()
  4084  		checkSum := tracker.csum
  4085  		tracker.csumMtx.RUnlock()
  4086  		if !bytes.Equal(csum, checkSum) {
  4087  			t.Fatalf(
  4088  				"[handlePreimageRequest] csum was changed, exp: %s, got: %s",
  4089  				csum,
  4090  				checkSum,
  4091  			)
  4092  		}
  4093  	})
  4094  	t.Run("csum for cancel order", func(t *testing.T) {
  4095  		rig := newTestRig()
  4096  		defer rig.shutdown()
  4097  		ord := &order.LimitOrder{P: order.Prefix{ServerTime: time.Now()}}
  4098  		preImg := newPreimage()
  4099  		mkt := rig.dc.marketConfig(tDcrBtcMktName)
  4100  
  4101  		tracker := &trackedTrade{
  4102  			Order:    ord,
  4103  			preImg:   preImg,
  4104  			mktID:    tDcrBtcMktName,
  4105  			db:       rig.db,
  4106  			dc:       rig.dc,
  4107  			metaData: &db.OrderMetaData{},
  4108  			cancel: &trackedCancel{
  4109  				CancelOrder: order.CancelOrder{
  4110  					P: order.Prefix{
  4111  						AccountID:  rig.dc.acct.ID(),
  4112  						BaseAsset:  tUTXOAssetA.ID,
  4113  						QuoteAsset: tUTXOAssetB.ID,
  4114  						OrderType:  order.MarketOrderType,
  4115  						ClientTime: time.Now(),
  4116  						ServerTime: time.Now().Add(time.Millisecond),
  4117  						Commit:     preImg.Commit(),
  4118  					},
  4119  				},
  4120  				epochLen: mkt.EpochLen,
  4121  			},
  4122  		}
  4123  		oid := tracker.ID()
  4124  		cid := tracker.cancel.ID()
  4125  
  4126  		// Test the new path with rig.core.sentCommits.
  4127  		readyCommitment := func(commit order.Commitment) chan struct{} {
  4128  			commitSig := make(chan struct{}) // close after fake order submission is "done"
  4129  			rig.core.sentCommitsMtx.Lock()
  4130  			rig.core.sentCommits[commit] = commitSig
  4131  			rig.core.sentCommitsMtx.Unlock()
  4132  			return commitSig
  4133  		}
  4134  
  4135  		commit := preImg.Commit()
  4136  		commitCSum := dex.Bytes{2, 3, 5, 7, 11, 13}
  4137  		commitSig := readyCommitment(commit)
  4138  		payload := &msgjson.PreimageRequest{
  4139  			OrderID:        cid[:],
  4140  			Commitment:     commit[:],
  4141  			CommitChecksum: commitCSum,
  4142  		}
  4143  		reqCommit, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.PreimageRoute, payload)
  4144  
  4145  		notes := rig.core.NotificationFeed()
  4146  
  4147  		rig.dc.trades[oid] = tracker
  4148  		rig.dc.registerCancelLink(cid, oid)
  4149  		err := handlePreimageRequest(rig.core, rig.dc, reqCommit)
  4150  		if err != nil {
  4151  			t.Fatalf("handlePreimageRequest error: %v", err)
  4152  		}
  4153  
  4154  		// It has gone async now, waiting for commitSig.
  4155  		// i.e. "Received preimage request for %v with no corresponding order submission response! Waiting..."
  4156  		close(commitSig) // pretend like the order submission just finished
  4157  
  4158  		select {
  4159  		case note := <-notes.C:
  4160  			if note.Topic() != TopicCancelPreimageSent {
  4161  				t.Fatalf("note subject is %v, not %v", note.Topic(), TopicCancelPreimageSent)
  4162  			}
  4163  		case <-time.After(time.Second):
  4164  			t.Fatal("no order note from preimage request handling")
  4165  		}
  4166  
  4167  		tracker.csumMtx.RLock()
  4168  		cancelCsum := tracker.cancelCsum
  4169  		tracker.csumMtx.RUnlock()
  4170  		if !bytes.Equal(commitCSum, cancelCsum) {
  4171  			t.Fatalf(
  4172  				"handlePreimageRequest must initialize tracker cancel csum, exp: %s, got: %s",
  4173  				commitCSum,
  4174  				cancelCsum,
  4175  			)
  4176  		}
  4177  
  4178  	})
  4179  	t.Run("more than one preimage request for cancel order (different csums)", func(t *testing.T) {
  4180  		rig := newTestRig()
  4181  		defer rig.shutdown()
  4182  		ord := &order.LimitOrder{P: order.Prefix{ServerTime: time.Now()}}
  4183  		preImg := newPreimage()
  4184  		mkt := rig.dc.marketConfig(tDcrBtcMktName)
  4185  		firstCSum := dex.Bytes{2, 3, 5, 7, 11, 13}
  4186  
  4187  		tracker := &trackedTrade{
  4188  			Order:    ord,
  4189  			preImg:   preImg,
  4190  			mktID:    tDcrBtcMktName,
  4191  			db:       rig.db,
  4192  			dc:       rig.dc,
  4193  			metaData: &db.OrderMetaData{},
  4194  			// Simulate first preimage request by initializing csum here.
  4195  			cancelCsum: firstCSum,
  4196  			cancel: &trackedCancel{
  4197  				CancelOrder: order.CancelOrder{
  4198  					P: order.Prefix{
  4199  						AccountID:  rig.dc.acct.ID(),
  4200  						BaseAsset:  tUTXOAssetA.ID,
  4201  						QuoteAsset: tUTXOAssetB.ID,
  4202  						OrderType:  order.MarketOrderType,
  4203  						ClientTime: time.Now(),
  4204  						ServerTime: time.Now().Add(time.Millisecond),
  4205  						Commit:     preImg.Commit(),
  4206  					},
  4207  				},
  4208  				epochLen: mkt.EpochLen,
  4209  			},
  4210  		}
  4211  		oid := tracker.ID()
  4212  		cid := tracker.cancel.ID()
  4213  
  4214  		// Test the new path with rig.core.sentCommits.
  4215  		readyCommitment := func(commit order.Commitment) chan struct{} {
  4216  			commitSig := make(chan struct{}) // close after fake order submission is "done"
  4217  			rig.core.sentCommitsMtx.Lock()
  4218  			rig.core.sentCommits[commit] = commitSig
  4219  			rig.core.sentCommitsMtx.Unlock()
  4220  			return commitSig
  4221  		}
  4222  
  4223  		commit := preImg.Commit()
  4224  		secondCSum := dex.Bytes{2, 3, 5, 7, 11, 14}
  4225  		commitSig := readyCommitment(commit)
  4226  		payload := &msgjson.PreimageRequest{
  4227  			OrderID:        cid[:],
  4228  			Commitment:     commit[:],
  4229  			CommitChecksum: secondCSum,
  4230  		}
  4231  		reqCommit, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.PreimageRoute, payload)
  4232  
  4233  		// Prepare to have processPreimageRequest respond with a payload with
  4234  		// the Error field set.
  4235  		rig.ws.sendMsgErrChan = make(chan *msgjson.Error, 1)
  4236  		defer func() { rig.ws.sendMsgErrChan = nil }()
  4237  
  4238  		rig.dc.trades[oid] = tracker
  4239  		rig.dc.registerCancelLink(cid, oid)
  4240  		err := handlePreimageRequest(rig.core, rig.dc, reqCommit)
  4241  		if err != nil {
  4242  			t.Fatalf("handlePreimageRequest error: %v", err)
  4243  		}
  4244  
  4245  		// It has gone async now, waiting for commitSig.
  4246  		// i.e. "Received preimage request for %v with no corresponding order submission response! Waiting..."
  4247  		close(commitSig) // pretend like the order submission just finished
  4248  
  4249  		select {
  4250  		case msgErr := <-rig.ws.sendMsgErrChan:
  4251  			if msgErr.Code != msgjson.InvalidRequestError {
  4252  				t.Fatalf("expected error code %d got %d", msgjson.InvalidRequestError, msgErr.Code)
  4253  			}
  4254  		case <-time.After(time.Second):
  4255  			t.Fatal("no msgjson.Error sent from preimage request handling")
  4256  		}
  4257  		tracker.csumMtx.RLock()
  4258  		cancelCsum := tracker.cancelCsum
  4259  		tracker.csumMtx.RUnlock()
  4260  		if !bytes.Equal(firstCSum, cancelCsum) {
  4261  			t.Fatalf(
  4262  				"[handlePreimageRequest] cancel csum was changed, exp: %s, got: %s",
  4263  				firstCSum,
  4264  				cancelCsum,
  4265  			)
  4266  		}
  4267  	})
  4268  	t.Run("more than one preimage request for cancel order (same csum)", func(t *testing.T) {
  4269  		rig := newTestRig()
  4270  		defer rig.shutdown()
  4271  		ord := &order.LimitOrder{P: order.Prefix{ServerTime: time.Now()}}
  4272  		preImg := newPreimage()
  4273  		mkt := rig.dc.marketConfig(tDcrBtcMktName)
  4274  		csum := dex.Bytes{2, 3, 5, 7, 11, 13}
  4275  
  4276  		tracker := &trackedTrade{
  4277  			Order:    ord,
  4278  			preImg:   preImg,
  4279  			mktID:    tDcrBtcMktName,
  4280  			db:       rig.db,
  4281  			dc:       rig.dc,
  4282  			metaData: &db.OrderMetaData{},
  4283  			// Simulate first preimage request by initializing csum here.
  4284  			cancelCsum: csum,
  4285  			cancel: &trackedCancel{
  4286  				CancelOrder: order.CancelOrder{
  4287  					P: order.Prefix{
  4288  						AccountID:  rig.dc.acct.ID(),
  4289  						BaseAsset:  tUTXOAssetA.ID,
  4290  						QuoteAsset: tUTXOAssetB.ID,
  4291  						OrderType:  order.MarketOrderType,
  4292  						ClientTime: time.Now(),
  4293  						ServerTime: time.Now().Add(time.Millisecond),
  4294  						Commit:     preImg.Commit(),
  4295  					},
  4296  				},
  4297  				epochLen: mkt.EpochLen,
  4298  			},
  4299  		}
  4300  		oid := tracker.ID()
  4301  		cid := tracker.cancel.ID()
  4302  
  4303  		// Test the new path with rig.core.sentCommits.
  4304  		readyCommitment := func(commit order.Commitment) chan struct{} {
  4305  			commitSig := make(chan struct{}) // close after fake order submission is "done"
  4306  			rig.core.sentCommitsMtx.Lock()
  4307  			rig.core.sentCommits[commit] = commitSig
  4308  			rig.core.sentCommitsMtx.Unlock()
  4309  			return commitSig
  4310  		}
  4311  
  4312  		commit := preImg.Commit()
  4313  		commitSig := readyCommitment(commit)
  4314  		payload := &msgjson.PreimageRequest{
  4315  			OrderID:        cid[:],
  4316  			Commitment:     commit[:],
  4317  			CommitChecksum: csum,
  4318  		}
  4319  		reqCommit, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.PreimageRoute, payload)
  4320  
  4321  		notes := rig.core.NotificationFeed()
  4322  
  4323  		rig.dc.trades[oid] = tracker
  4324  		rig.dc.registerCancelLink(cid, oid)
  4325  		err := handlePreimageRequest(rig.core, rig.dc, reqCommit)
  4326  		if err != nil {
  4327  			t.Fatalf("handlePreimageRequest error: %v", err)
  4328  		}
  4329  
  4330  		// It has gone async now, waiting for commitSig.
  4331  		// i.e. "Received preimage request for %v with no corresponding order submission response! Waiting..."
  4332  		close(commitSig) // pretend like the order submission just finished
  4333  
  4334  		select {
  4335  		case note := <-notes.C:
  4336  			if note.Topic() != TopicCancelPreimageSent {
  4337  				t.Fatalf("note subject is %v, not %v", note.Topic(), TopicCancelPreimageSent)
  4338  			}
  4339  		case <-time.After(time.Second):
  4340  			t.Fatal("no order note from preimage request handling")
  4341  		}
  4342  
  4343  		tracker.csumMtx.RLock()
  4344  		cancelCsum := tracker.cancelCsum
  4345  		tracker.csumMtx.RUnlock()
  4346  		if !bytes.Equal(csum, cancelCsum) {
  4347  			t.Fatalf(
  4348  				"[handlePreimageRequest] cancel csum was changed, exp: %s, got: %s",
  4349  				csum,
  4350  				cancelCsum,
  4351  			)
  4352  		}
  4353  	})
  4354  }
  4355  
  4356  func TestHandleRevokeOrderMsg(t *testing.T) {
  4357  	rig := newTestRig()
  4358  	defer rig.shutdown()
  4359  	dc := rig.dc
  4360  	tCore := rig.core
  4361  	dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID)
  4362  	tCore.wallets[tUTXOAssetA.ID] = dcrWallet
  4363  	dcrWallet.address = "DsVmA7aqqWeKWy461hXjytbZbgCqbB8g2dq"
  4364  	dcrWallet.Unlock(rig.crypter)
  4365  
  4366  	fundCoinDcrID := encode.RandomBytes(36)
  4367  	fundCoinDcr := &tCoin{id: fundCoinDcrID}
  4368  
  4369  	btcWallet, _ := newTWallet(tUTXOAssetB.ID)
  4370  	tCore.wallets[tUTXOAssetB.ID] = btcWallet
  4371  	btcWallet.address = "12DXGkvxFjuq5btXYkwWfBZaz1rVwFgini"
  4372  	btcWallet.Unlock(rig.crypter)
  4373  
  4374  	// fundCoinBID := encode.RandomBytes(36)
  4375  	// fundCoinB := &tCoin{id: fundCoinBID}
  4376  
  4377  	qty := 2 * dcrBtcLotSize
  4378  	rate := dcrBtcRateStep * 10
  4379  	lo, dbOrder, preImg, _ := makeLimitOrder(dc, true, qty, rate) // sell DCR
  4380  	lo.Coins = []order.CoinID{fundCoinDcrID}
  4381  	dbOrder.MetaData.Status = order.OrderStatusBooked
  4382  	oid := lo.ID()
  4383  
  4384  	tDcrWallet.fundingCoins = asset.Coins{fundCoinDcr}
  4385  
  4386  	walletSet, _, _, err := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, true)
  4387  	if err != nil {
  4388  		t.Fatalf("walletSet error: %v", err)
  4389  	}
  4390  
  4391  	// Not in dc.trades yet.
  4392  
  4393  	// Send a request for the unknown order.
  4394  	payload := &msgjson.RevokeOrder{
  4395  		OrderID: oid[:],
  4396  	}
  4397  	req, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.RevokeOrderRoute, payload)
  4398  
  4399  	// Ensure revoking a non-existent order generates an error.
  4400  	err = handleRevokeOrderMsg(rig.core, rig.dc, req)
  4401  	if err == nil {
  4402  		t.Fatal("[handleRevokeOrderMsg] expected a non-existent order")
  4403  	}
  4404  
  4405  	// Now store the order in dc.trades, with a linked cancel order.
  4406  	tracker := newTrackedTrade(dbOrder, preImg, dc,
  4407  		rig.core.lockTimeTaker, rig.core.lockTimeMaker,
  4408  		rig.db, rig.queue, walletSet, tDcrWallet.fundingCoins, rig.core.notify,
  4409  		rig.core.formatDetails)
  4410  	preImgC := newPreimage()
  4411  	co := &order.CancelOrder{
  4412  		P: order.Prefix{
  4413  			ServerTime: time.Now(),
  4414  			Commit:     preImgC.Commit(),
  4415  		},
  4416  	}
  4417  	tracker.cancel = &trackedCancel{CancelOrder: *co}
  4418  	coid := co.ID()
  4419  	rig.dc.trades[oid] = tracker
  4420  	rig.dc.registerCancelLink(coid, oid)
  4421  
  4422  	orderNotes, feedDone := orderNoteFeed(tCore)
  4423  	defer feedDone()
  4424  
  4425  	// Revoke the cancel order, not the targeted order.
  4426  	payloadC := &msgjson.RevokeOrder{
  4427  		OrderID: coid[:],
  4428  	}
  4429  	reqC, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.RevokeOrderRoute, payloadC)
  4430  	err = handleRevokeOrderMsg(rig.core, rig.dc, reqC)
  4431  	if err != nil {
  4432  		t.Fatalf("handleRevokeOrderMsg error: %v", err)
  4433  	}
  4434  
  4435  	verifyRevokeNotification(orderNotes, TopicFailedCancel, t)
  4436  
  4437  	if tracker.metaData.Status == order.OrderStatusRevoked {
  4438  		t.Errorf("Incorrectly revoked the targeted order instead of clearing the cancel order!")
  4439  	}
  4440  	if tracker.cancel != nil {
  4441  		t.Fatalf("Did not clear the cancel order")
  4442  	}
  4443  
  4444  	// Now revoke the actual trade order.
  4445  	err = handleRevokeOrderMsg(rig.core, rig.dc, req)
  4446  	if err != nil {
  4447  		t.Fatalf("handleRevokeOrderMsg error: %v", err)
  4448  	}
  4449  
  4450  	verifyRevokeNotification(orderNotes, TopicOrderRevoked, t)
  4451  
  4452  	if tracker.metaData.Status != order.OrderStatusRevoked {
  4453  		t.Errorf("expected order status %v, got %v", order.OrderStatusRevoked, tracker.metaData.Status)
  4454  	}
  4455  }
  4456  
  4457  func TestHandleRevokeMatchMsg(t *testing.T) {
  4458  	rig := newTestRig()
  4459  	defer rig.shutdown()
  4460  	dc := rig.dc
  4461  	tCore := rig.core
  4462  	dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID)
  4463  	tCore.wallets[tUTXOAssetA.ID] = dcrWallet
  4464  	dcrWallet.address = "DsVmA7aqqWeKWy461hXjytbZbgCqbB8g2dq"
  4465  	dcrWallet.Unlock(rig.crypter)
  4466  
  4467  	fundCoinDcrID := encode.RandomBytes(36)
  4468  	fundCoinDcr := &tCoin{id: fundCoinDcrID}
  4469  
  4470  	btcWallet, _ := newTWallet(tUTXOAssetB.ID)
  4471  	tCore.wallets[tUTXOAssetB.ID] = btcWallet
  4472  	btcWallet.address = "12DXGkvxFjuq5btXYkwWfBZaz1rVwFgini"
  4473  	btcWallet.Unlock(rig.crypter)
  4474  
  4475  	// fundCoinBID := encode.RandomBytes(36)
  4476  	// fundCoinB := &tCoin{id: fundCoinBID}
  4477  
  4478  	matchSize := 4 * dcrBtcLotSize
  4479  	cancelledQty := dcrBtcLotSize
  4480  	qty := 2*matchSize + cancelledQty
  4481  	lo, dbOrder, preImg, _ := makeLimitOrder(dc, true, qty, dcrBtcRateStep)
  4482  	lo.Coins = []order.CoinID{fundCoinDcrID}
  4483  	dbOrder.MetaData.Status = order.OrderStatusBooked
  4484  	oid := lo.ID()
  4485  
  4486  	tDcrWallet.fundingCoins = asset.Coins{fundCoinDcr}
  4487  
  4488  	mid := ordertest.RandomMatchID()
  4489  	walletSet, _, _, err := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, true)
  4490  	if err != nil {
  4491  		t.Fatalf("walletSet error: %v", err)
  4492  	}
  4493  
  4494  	tracker := newTrackedTrade(dbOrder, preImg, dc,
  4495  		rig.core.lockTimeTaker, rig.core.lockTimeMaker,
  4496  		rig.db, rig.queue, walletSet, tDcrWallet.fundingCoins, rig.core.notify,
  4497  		rig.core.formatDetails)
  4498  
  4499  	match := &matchTracker{
  4500  		MetaMatch: db.MetaMatch{
  4501  			UserMatch: &order.UserMatch{MatchID: mid},
  4502  			MetaData:  &db.MatchMetaData{},
  4503  		},
  4504  	}
  4505  	tracker.matches[mid] = match
  4506  
  4507  	payload := &msgjson.RevokeMatch{
  4508  		OrderID: oid[:],
  4509  		MatchID: mid[:],
  4510  	}
  4511  	req, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.RevokeMatchRoute, payload)
  4512  
  4513  	// Ensure revoking a non-existent order generates an error.
  4514  	err = handleRevokeMatchMsg(rig.core, rig.dc, req)
  4515  	if err == nil {
  4516  		t.Fatal("[handleRevokeMatchMsg] expected a non-existent order")
  4517  	}
  4518  
  4519  	rig.dc.trades[oid] = tracker
  4520  
  4521  	// Success
  4522  	err = handleRevokeMatchMsg(rig.core, rig.dc, req)
  4523  	if err != nil {
  4524  		t.Fatalf("handleRevokeMatchMsg error: %v", err)
  4525  	}
  4526  }
  4527  
  4528  func TestTradeTracking(t *testing.T) {
  4529  	rig := newTestRig()
  4530  	defer rig.shutdown()
  4531  	dc := rig.dc
  4532  	tCore := rig.core
  4533  	tCore.loggedIn = true
  4534  	dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID)
  4535  	tCore.wallets[tUTXOAssetA.ID] = dcrWallet
  4536  	dcrWallet.address = "DsVmA7aqqWeKWy461hXjytbZbgCqbB8g2dq"
  4537  	dcrWallet.Unlock(rig.crypter)
  4538  
  4539  	btcWallet, tBtcWallet := newTWallet(tUTXOAssetB.ID)
  4540  	tCore.wallets[tUTXOAssetB.ID] = btcWallet
  4541  	btcWallet.address = "12DXGkvxFjuq5btXYkwWfBZaz1rVwFgini"
  4542  	btcWallet.Unlock(rig.crypter)
  4543  
  4544  	tBtcWallet.confirmRedemptionErr = errors.New("")
  4545  	tDcrWallet.confirmRedemptionErr = errors.New("")
  4546  
  4547  	matchSize := 4 * dcrBtcLotSize
  4548  	cancelledQty := dcrBtcLotSize
  4549  	qty := 2*matchSize + cancelledQty
  4550  	rate := dcrBtcRateStep * 10
  4551  	lo, dbOrder, preImgL, addr := makeLimitOrder(dc, true, qty, dcrBtcRateStep)
  4552  	lo.Force = order.StandingTiF
  4553  	// fundCoinDcrID := encode.RandomBytes(36)
  4554  	// lo.Coins = []order.CoinID{fundCoinDcrID}
  4555  	loid := lo.ID()
  4556  
  4557  	//fundCoinDcr := &tCoin{id: fundCoinDcrID}
  4558  	//tDcrWallet.fundingCoins = asset.Coins{fundCoinDcr}
  4559  
  4560  	mid := ordertest.RandomMatchID()
  4561  	walletSet, _, _, err := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, true)
  4562  	if err != nil {
  4563  		t.Fatalf("walletSet error: %v", err)
  4564  	}
  4565  	mkt := dc.marketConfig(tDcrBtcMktName)
  4566  	fundCoinDcrID := encode.RandomBytes(36)
  4567  	fundingCoins := asset.Coins{&tCoin{id: fundCoinDcrID}}
  4568  	tracker := newTrackedTrade(dbOrder, preImgL, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker,
  4569  		rig.db, rig.queue, walletSet, fundingCoins, rig.core.notify, rig.core.formatDetails)
  4570  	rig.dc.trades[tracker.ID()] = tracker
  4571  	var match *matchTracker
  4572  	checkStatus := func(tag string, wantStatus order.MatchStatus) {
  4573  		t.Helper()
  4574  		if match.Status != wantStatus {
  4575  			t.Fatalf("%s: wrong status wanted %v, got %v", tag,
  4576  				wantStatus, match.Status)
  4577  		}
  4578  	}
  4579  
  4580  	// create new notification feed to catch swap-related errors from goroutines
  4581  	notes := tCore.NotificationFeed()
  4582  	drainNotes := func() {
  4583  		for {
  4584  			select {
  4585  			case <-notes.C:
  4586  			default:
  4587  				return
  4588  			}
  4589  		}
  4590  	}
  4591  
  4592  	lastSwapErrorNote := func() Notification {
  4593  		for {
  4594  			select {
  4595  			case note := <-notes.C:
  4596  				if note.Severity() == db.ErrorLevel && (note.Topic() == TopicSwapSendError ||
  4597  					note.Topic() == TopicInitError || note.Topic() == TopicReportRedeemError) {
  4598  
  4599  					return note
  4600  				}
  4601  			default:
  4602  				return nil
  4603  			}
  4604  		}
  4605  	}
  4606  
  4607  	type swapRelatedAction struct {
  4608  		name                 string
  4609  		fn                   func() error
  4610  		expectError          bool
  4611  		expectMatchDBUpdates int
  4612  		expectSwapErrorNote  bool
  4613  	}
  4614  	testSwapRelatedAction := func(action swapRelatedAction) {
  4615  		t.Helper()
  4616  		drainNotes() // clear previous (swap error) notes before exec'ing swap-related action
  4617  		if action.expectMatchDBUpdates > 0 {
  4618  			rig.db.updateMatchChan = make(chan order.MatchStatus, action.expectMatchDBUpdates)
  4619  		}
  4620  		// Try the action and confirm the behaviour is as expected.
  4621  		err := action.fn()
  4622  		if action.expectError && err == nil {
  4623  			t.Fatalf("%s: expected error but got nil", action.name)
  4624  		} else if !action.expectError && err != nil {
  4625  			t.Fatalf("%s: unexpected error: %v", action.name, err)
  4626  		}
  4627  		// Check that we received the expected number of match db updates.
  4628  		for i := 0; i < action.expectMatchDBUpdates; i++ {
  4629  			<-rig.db.updateMatchChan
  4630  		}
  4631  		rig.db.updateMatchChan = nil
  4632  		// Check that we received a swap error note (if expected), and that
  4633  		// no error note was received, if not expected.
  4634  		time.Sleep(100 * time.Millisecond) // wait briefly as swap error notes may be sent from a goroutine
  4635  		swapErrNote := lastSwapErrorNote()
  4636  		if action.expectSwapErrorNote && swapErrNote == nil {
  4637  			t.Fatalf("%s: expected swap error note but got nil", action.name)
  4638  		} else if !action.expectSwapErrorNote && swapErrNote != nil {
  4639  			t.Fatalf("%s: unexpected swap error note: %s", action.name, swapErrNote.Details())
  4640  		}
  4641  	}
  4642  
  4643  	// MAKER MATCH
  4644  	matchTime := time.Now()
  4645  	msgMatch := &msgjson.Match{
  4646  		OrderID:    loid[:],
  4647  		MatchID:    mid[:],
  4648  		Quantity:   matchSize,
  4649  		Rate:       rate,
  4650  		Address:    "counterparty-address",
  4651  		Side:       uint8(order.Maker),
  4652  		ServerTime: uint64(matchTime.UnixMilli()),
  4653  	}
  4654  	counterSwapID := encode.RandomBytes(36)
  4655  	tDcrWallet.swapReceipts = []asset.Receipt{&tReceipt{coin: &tCoin{id: counterSwapID}}}
  4656  	sign(tDexPriv, msgMatch)
  4657  
  4658  	// Make sure that a fee rate higher than our recorded MaxFeeRate results in
  4659  	// an error.
  4660  	msgMatch.FeeRateBase = tMaxFeeRate + 1
  4661  	sign(tDexPriv, msgMatch)
  4662  	msg, _ := msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{msgMatch})
  4663  	err = handleMatchRoute(tCore, rig.dc, msg)
  4664  	if err == nil || !strings.Contains(err.Error(), "is > MaxFeeRate") {
  4665  		t.Fatalf("no error for fee rate > MaxFeeRate %t", lo.Trade().Sell)
  4666  	}
  4667  
  4668  	// Restore fee rate.
  4669  	msgMatch.FeeRateBase = tMaxFeeRate
  4670  	sign(tDexPriv, msgMatch)
  4671  	msg, _ = msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{msgMatch})
  4672  
  4673  	// Handle new match as maker with a queued invalid DEX init ack.
  4674  	// handleMatchRoute should have no errors but trigger a match db update (status = NewlyMatched).
  4675  	// Maker's swap should be bcasted, triggering another match db update (NewlyMatched->MakerSwapCast).
  4676  	// sendInitAsync should fail because of invalid ack and produce a swap error note.
  4677  	testSwapRelatedAction(swapRelatedAction{
  4678  		name: "handleMatchRoute",
  4679  		fn: func() error {
  4680  			// queue an invalid DEX init ack
  4681  			rig.ws.queueResponse(msgjson.InitRoute, invalidAcker)
  4682  			return handleMatchRoute(tCore, rig.dc, msg)
  4683  		},
  4684  		expectError:          false,
  4685  		expectMatchDBUpdates: 2,
  4686  		expectSwapErrorNote:  true,
  4687  	})
  4688  
  4689  	var found bool
  4690  	match, found = tracker.matches[mid]
  4691  	if !found {
  4692  		t.Fatalf("match not found")
  4693  	}
  4694  
  4695  	// We're the maker, so the init transaction should be broadcast.
  4696  	checkStatus("maker swapped", order.MakerSwapCast)
  4697  	proof, auth := &match.MetaData.Proof, &match.MetaData.Proof.Auth
  4698  	if len(auth.MatchSig) == 0 {
  4699  		t.Fatalf("no match sig recorded")
  4700  	}
  4701  	if !bytes.Equal(proof.MakerSwap, counterSwapID) {
  4702  		t.Fatalf("receipt ID not recorded")
  4703  	}
  4704  	if len(proof.Secret) == 0 {
  4705  		t.Fatalf("secret not set")
  4706  	}
  4707  	if len(proof.SecretHash) == 0 {
  4708  		t.Fatalf("secret hash not set")
  4709  	}
  4710  	// auth.InitSig should be unset because our init request received
  4711  	// an invalid ack
  4712  	if len(auth.InitSig) != 0 {
  4713  		t.Fatalf("init sig recorded for invalid init ack")
  4714  	}
  4715  
  4716  	// requeue an invalid DEX init ack and resend pending init request
  4717  	testSwapRelatedAction(swapRelatedAction{
  4718  		name: "resend pending init (invalid ack)",
  4719  		fn: func() error {
  4720  			rig.ws.queueResponse(msgjson.InitRoute, invalidAcker)
  4721  			tCore.resendPendingRequests(tracker)
  4722  			return nil
  4723  		},
  4724  		expectError:          false,
  4725  		expectMatchDBUpdates: 0,    // no db update for invalid init ack
  4726  		expectSwapErrorNote:  true, // expect swap error note for invalid init ack
  4727  	})
  4728  	// auth.InitSig should remain unset because our resent init request
  4729  	// received an invalid ack still
  4730  	if len(auth.InitSig) != 0 {
  4731  		t.Fatalf("init sig recorded for second invalid init ack")
  4732  	}
  4733  
  4734  	// queue a valid DEX init ack and re-send pending init request
  4735  	// a valid ack should produce a db update otherwise it's an error
  4736  	testSwapRelatedAction(swapRelatedAction{
  4737  		name: "resend pending init (valid ack)",
  4738  		fn: func() error {
  4739  			rig.ws.queueResponse(msgjson.InitRoute, initAcker)
  4740  			tCore.resendPendingRequests(tracker)
  4741  			return nil
  4742  		},
  4743  		expectError:          false,
  4744  		expectMatchDBUpdates: 1,     // expect db update for valid init ack
  4745  		expectSwapErrorNote:  false, // no swap error note for valid init ack
  4746  	})
  4747  	// auth.InitSig should now be set because our init request received
  4748  	// a valid ack
  4749  	if len(auth.InitSig) == 0 {
  4750  		t.Fatalf("init sig not recorded for valid init ack")
  4751  	}
  4752  
  4753  	// Send the counter-party's init info.
  4754  	auditQty := calc.BaseToQuote(rate, matchSize)
  4755  	audit, auditInfo := tMsgAudit(loid, mid, addr, auditQty, proof.SecretHash)
  4756  	auditInfo.Expiration = encode.DropMilliseconds(matchTime.Add(tracker.lockTimeTaker))
  4757  	tBtcWallet.auditInfo = auditInfo
  4758  	msg, _ = msgjson.NewRequest(1, msgjson.AuditRoute, audit)
  4759  
  4760  	// Check audit errors.
  4761  	tBtcWallet.auditErr = tErr
  4762  	err = tracker.auditContract(match, audit.CoinID, audit.Contract, nil)
  4763  	if err == nil {
  4764  		t.Fatalf("no maker error for AuditContract error")
  4765  	}
  4766  
  4767  	// Check expiration error.
  4768  	match.MetaData.Proof.SelfRevoked = true // keeps trying unless revoked
  4769  	tBtcWallet.auditErr = asset.CoinNotFoundError
  4770  	err = tracker.auditContract(match, audit.CoinID, audit.Contract, nil)
  4771  	if err == nil {
  4772  		t.Fatalf("no maker error for AuditContract expiration")
  4773  	}
  4774  	var expErr ExpirationErr
  4775  	if !errors.As(err, &expErr) {
  4776  		t.Fatalf("wrong error type. expecting ExpirationTimeout, got %T: %v", err, err)
  4777  	}
  4778  	tBtcWallet.auditErr = nil
  4779  	match.MetaData.Proof.SelfRevoked = false
  4780  
  4781  	auditInfo.Coin.(*tCoin).val = auditQty - 1
  4782  	err = tracker.auditContract(match, audit.CoinID, audit.Contract, nil)
  4783  	if err == nil {
  4784  		t.Fatalf("no maker error for low value")
  4785  	}
  4786  	auditInfo.Coin.(*tCoin).val = auditQty
  4787  
  4788  	auditInfo.SecretHash = []byte{0x01}
  4789  	err = tracker.auditContract(match, audit.CoinID, audit.Contract, nil)
  4790  	if err == nil {
  4791  		t.Fatalf("no maker error for wrong secret hash")
  4792  	}
  4793  	auditInfo.SecretHash = proof.SecretHash
  4794  
  4795  	auditInfo.Recipient = "wrong address"
  4796  	err = tracker.auditContract(match, audit.CoinID, audit.Contract, nil)
  4797  	if err == nil {
  4798  		t.Fatalf("no maker error for wrong address")
  4799  	}
  4800  	auditInfo.Recipient = addr
  4801  
  4802  	auditInfo.Expiration = matchTime.Add(tracker.lockTimeTaker - time.Hour)
  4803  	err = tracker.auditContract(match, audit.CoinID, audit.Contract, nil)
  4804  	if err == nil {
  4805  		t.Fatalf("no maker error for early lock time")
  4806  	}
  4807  	auditInfo.Expiration = matchTime.Add(tracker.lockTimeTaker)
  4808  
  4809  	// success, full handleAuditRoute>processAuditMsg>auditContract
  4810  	rig.db.updateMatchChan = make(chan order.MatchStatus, 1)
  4811  	err = handleAuditRoute(tCore, rig.dc, msg)
  4812  	if err != nil {
  4813  		t.Fatalf("audit error: %v", err)
  4814  	}
  4815  	// let the async auditContract run
  4816  	newMatchStatus := <-rig.db.updateMatchChan
  4817  	if newMatchStatus != order.TakerSwapCast {
  4818  		t.Fatalf("wrong match status. wanted %v, got %v", order.TakerSwapCast, newMatchStatus)
  4819  	}
  4820  	if match.counterSwap == nil {
  4821  		t.Fatalf("counter-swap not set")
  4822  	}
  4823  	if !bytes.Equal(proof.CounterContract, audit.Contract) {
  4824  		t.Fatalf("counter-script not recorded")
  4825  	}
  4826  	if !bytes.Equal(proof.TakerSwap, audit.CoinID) {
  4827  		t.Fatalf("taker contract ID not set")
  4828  	}
  4829  	<-rig.db.updateMatchChan // AuditSig is set in a second match data update
  4830  	if !bytes.Equal(auth.AuditSig, audit.Sig) {
  4831  		t.Fatalf("audit sig not set")
  4832  	}
  4833  	if auth.AuditStamp != audit.Time {
  4834  		t.Fatalf("audit time not set")
  4835  	}
  4836  
  4837  	// Confirming the counter-swap triggers a redemption.
  4838  	tBtcWallet.setConfs(auditInfo.Coin.ID(), tUTXOAssetB.SwapConf, nil)
  4839  	redeemCoin := encode.RandomBytes(36)
  4840  	//<-tBtcWallet.redeemErrChan
  4841  	tBtcWallet.redeemCoins = []dex.Bytes{redeemCoin}
  4842  	rig.ws.queueResponse(msgjson.RedeemRoute, redeemAcker)
  4843  	tCore.tickAsset(dc, tUTXOAssetB.ID)
  4844  	// TakerSwapCast -> MakerRedeemed after broadcast, before redeem request
  4845  	newMatchStatus = <-rig.db.updateMatchChan
  4846  	if newMatchStatus != order.MakerRedeemed {
  4847  		t.Fatalf("wrong match status. wanted %v, got %v", order.MakerRedeemed, newMatchStatus)
  4848  	}
  4849  	// MakerRedeem -> MatchComplete after redeem request
  4850  	newMatchStatus = <-rig.db.updateMatchChan
  4851  	if newMatchStatus != order.MatchComplete {
  4852  		t.Fatalf("wrong match status. wanted %v, got %v", order.MatchComplete, newMatchStatus)
  4853  	}
  4854  	if !bytes.Equal(proof.MakerRedeem, redeemCoin) {
  4855  		t.Fatalf("redeem coin ID not logged")
  4856  	}
  4857  	// No redemption request received as maker. Only taker gets a redemption
  4858  	// request following maker's redeem.
  4859  
  4860  	// Check that fees were incremented appropriately.
  4861  	if tracker.metaData.SwapFeesPaid != tSwapFeesPaid {
  4862  		t.Fatalf("wrong fees recorded for swap. expected %d, got %d", tSwapFeesPaid, tracker.metaData.SwapFeesPaid)
  4863  	}
  4864  	// Check that fees were incremented appropriately.
  4865  	if tracker.metaData.RedemptionFeesPaid != tRedemptionFeesPaid {
  4866  		t.Fatalf("wrong fees recorded for redemption. expected %d, got %d", tRedemptionFeesPaid, tracker.metaData.SwapFeesPaid)
  4867  	}
  4868  	rig.db.updateMatchChan = nil
  4869  
  4870  	// TAKER MATCH
  4871  	//
  4872  	mid = ordertest.RandomMatchID()
  4873  	msgMatch = &msgjson.Match{
  4874  		OrderID:     loid[:],
  4875  		MatchID:     mid[:],
  4876  		Quantity:    matchSize,
  4877  		Rate:        rate,
  4878  		Address:     "counterparty-address",
  4879  		Side:        uint8(order.Taker),
  4880  		ServerTime:  uint64(matchTime.UnixMilli()),
  4881  		FeeRateBase: tMaxFeeRate,
  4882  	}
  4883  	sign(tDexPriv, msgMatch)
  4884  	msg, _ = msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{msgMatch})
  4885  	rig.db.updateMatchChan = make(chan order.MatchStatus, 1)
  4886  	err = handleMatchRoute(tCore, rig.dc, msg)
  4887  	if err != nil {
  4888  		t.Fatalf("match messages error: %v", err)
  4889  	}
  4890  	match, found = tracker.matches[mid]
  4891  	if !found {
  4892  		t.Fatalf("match not found")
  4893  	}
  4894  	newMatchStatus = <-rig.db.updateMatchChan
  4895  	if newMatchStatus != order.NewlyMatched {
  4896  		t.Fatalf("wrong match status. wanted %v, got %v", order.NewlyMatched, newMatchStatus)
  4897  	}
  4898  	proof, auth = &match.MetaData.Proof, &match.MetaData.Proof.Auth
  4899  	if len(auth.MatchSig) == 0 {
  4900  		t.Fatalf("no match sig recorded")
  4901  	}
  4902  	// Secret should not be set yet.
  4903  	if len(proof.Secret) != 0 {
  4904  		t.Fatalf("secret set for taker")
  4905  	}
  4906  	if len(proof.SecretHash) != 0 {
  4907  		t.Fatalf("secret hash set for taker")
  4908  	}
  4909  
  4910  	// Now send through the audit request for the maker's init.
  4911  	audit, auditInfo = tMsgAudit(loid, mid, addr, matchSize, nil)
  4912  	tBtcWallet.auditInfo = auditInfo
  4913  	// early lock time
  4914  	auditInfo.Expiration = matchTime.Add(tracker.lockTimeMaker - time.Hour)
  4915  	err = tracker.auditContract(match, audit.CoinID, audit.Contract, nil)
  4916  	if err == nil {
  4917  		t.Fatalf("no taker error for early lock time")
  4918  	}
  4919  
  4920  	// success, full handleAuditRoute>processAuditMsg>auditContract
  4921  	auditInfo.Expiration = encode.DropMilliseconds(matchTime.Add(tracker.lockTimeMaker))
  4922  	msg, _ = msgjson.NewRequest(1, msgjson.AuditRoute, audit)
  4923  	err = handleAuditRoute(tCore, rig.dc, msg)
  4924  	if err != nil {
  4925  		t.Fatalf("taker's match message error: %v", err)
  4926  	}
  4927  	// let the async auditContract run, updating match status
  4928  	newMatchStatus = <-rig.db.updateMatchChan
  4929  	if newMatchStatus != order.MakerSwapCast {
  4930  		t.Fatalf("wrong match status. wanted %v, got %v", order.MakerSwapCast, newMatchStatus)
  4931  	}
  4932  	if len(proof.SecretHash) == 0 {
  4933  		t.Fatalf("secret hash not set for taker")
  4934  	}
  4935  	if !bytes.Equal(proof.MakerSwap, audit.CoinID) {
  4936  		t.Fatalf("maker redeem coin not set")
  4937  	}
  4938  	<-rig.db.updateMatchChan // AuditSig is set in a second match data update
  4939  	if !bytes.Equal(auth.AuditSig, audit.Sig) {
  4940  		t.Fatalf("audit sig not set for taker")
  4941  	}
  4942  	if auth.AuditStamp != audit.Time {
  4943  		t.Fatalf("audit time not set for taker")
  4944  	}
  4945  	// The swap should not be sent, since the auditInfo coin doesn't have the
  4946  	// requisite confirmations.
  4947  	if len(proof.TakerSwap) != 0 {
  4948  		t.Fatalf("swap broadcast before confirmations")
  4949  	}
  4950  	// confirming maker's swap should trigger taker's swap bcast
  4951  	tBtcWallet.setConfs(auditInfo.Coin.ID(), tUTXOAssetB.SwapConf, nil)
  4952  	swapID := encode.RandomBytes(36)
  4953  	tDcrWallet.swapReceipts = []asset.Receipt{&tReceipt{coin: &tCoin{id: swapID}}}
  4954  	rig.ws.queueResponse(msgjson.InitRoute, initAcker)
  4955  	tCore.tickAsset(dc, tUTXOAssetB.ID)
  4956  	newMatchStatus = <-rig.db.updateMatchChan // MakerSwapCast->TakerSwapCast (after taker's swap bcast)
  4957  	if newMatchStatus != order.TakerSwapCast {
  4958  		t.Fatalf("wrong match status. wanted %v, got %v", order.TakerSwapCast, newMatchStatus)
  4959  	}
  4960  	if len(proof.TakerSwap) == 0 {
  4961  		t.Fatalf("swap not broadcast with confirmations")
  4962  	}
  4963  	<-rig.db.updateMatchChan // init ack sig is set in a second match data update
  4964  	if len(auth.InitSig) == 0 {
  4965  		t.Fatalf("init ack sig not set for taker")
  4966  	}
  4967  
  4968  	// Receive the maker's redemption.
  4969  	redemptionCoin := encode.RandomBytes(36)
  4970  	redemption := &msgjson.Redemption{
  4971  		Redeem: msgjson.Redeem{
  4972  			OrderID: loid[:],
  4973  			MatchID: mid[:],
  4974  			CoinID:  redemptionCoin,
  4975  		},
  4976  	}
  4977  	sign(tDexPriv, redemption)
  4978  	redeemCoin = encode.RandomBytes(36)
  4979  	tBtcWallet.redeemCoins = []dex.Bytes{redeemCoin}
  4980  	msg, _ = msgjson.NewRequest(1, msgjson.RedemptionRoute, redemption)
  4981  
  4982  	tBtcWallet.badSecret = true
  4983  	err = handleRedemptionRoute(tCore, rig.dc, msg)
  4984  	if err == nil {
  4985  		t.Fatalf("no error for wrong secret")
  4986  	}
  4987  	newMatchStatus = <-rig.db.updateMatchChan  // wrong secret still updates match
  4988  	if newMatchStatus != order.TakerSwapCast { // but status is same
  4989  		t.Fatalf("wrong match status. wanted %v, got %v", order.TakerSwapCast, newMatchStatus)
  4990  	}
  4991  	tBtcWallet.badSecret = false
  4992  
  4993  	tBtcWallet.redeemErrChan = make(chan error, 1)
  4994  	rig.ws.queueResponse(msgjson.RedeemRoute, redeemAcker)
  4995  	err = handleRedemptionRoute(tCore, rig.dc, msg)
  4996  	if err != nil {
  4997  		t.Fatalf("redemption message error: %v", err)
  4998  	}
  4999  	err = <-tBtcWallet.redeemErrChan
  5000  	if err != nil {
  5001  		t.Fatalf("should have worked, got: %v", err)
  5002  	}
  5003  	// For taker, there's one status update to MakerRedeemed prior to bcasting taker's redemption
  5004  	newMatchStatus = <-rig.db.updateMatchChan
  5005  	if newMatchStatus != order.MakerRedeemed {
  5006  		t.Fatalf("wrong match status. wanted %v, got %v", order.MakerRedeemed, newMatchStatus)
  5007  	}
  5008  	// and another status update to MatchComplete when taker's redemption is bcast
  5009  	newMatchStatus = <-rig.db.updateMatchChan
  5010  	if newMatchStatus != order.MatchComplete {
  5011  		t.Fatalf("wrong match status. wanted %v, got %v", order.MatchComplete, newMatchStatus)
  5012  	}
  5013  	if !bytes.Equal(proof.MakerRedeem, redemptionCoin) {
  5014  		t.Fatalf("redemption coin ID not logged")
  5015  	}
  5016  	if len(proof.TakerRedeem) == 0 {
  5017  		t.Fatalf("taker redemption not sent")
  5018  	}
  5019  	// Then a match update to set the redeem ack sig when the 'redeem' request back
  5020  	// to the server succeeds.
  5021  	<-rig.db.updateMatchChan
  5022  	if len(auth.RedeemSig) == 0 {
  5023  		t.Fatalf("redeem ack sig not set for taker")
  5024  	}
  5025  	rig.db.updateMatchChan = nil
  5026  	tBtcWallet.redeemErrChan = nil
  5027  
  5028  	// CANCEL ORDER MATCH
  5029  	//
  5030  	tDcrWallet.returnedCoins = nil
  5031  	copy(mid[:], encode.RandomBytes(32))
  5032  	preImgC := newPreimage()
  5033  	co := &order.CancelOrder{
  5034  		P: order.Prefix{
  5035  			AccountID:  dc.acct.ID(),
  5036  			BaseAsset:  tUTXOAssetA.ID,
  5037  			QuoteAsset: tUTXOAssetB.ID,
  5038  			OrderType:  order.MarketOrderType,
  5039  			ClientTime: time.Now(),
  5040  			ServerTime: time.Now().Add(time.Millisecond),
  5041  			Commit:     preImgC.Commit(),
  5042  		},
  5043  	}
  5044  	tracker.cancel = &trackedCancel{CancelOrder: *co, epochLen: mkt.EpochLen}
  5045  	coid := co.ID()
  5046  	rig.dc.registerCancelLink(coid, tracker.ID())
  5047  	m1 := &msgjson.Match{
  5048  		OrderID:  loid[:],
  5049  		MatchID:  mid[:],
  5050  		Quantity: cancelledQty,
  5051  		Rate:     rate,
  5052  		Address:  "",
  5053  	}
  5054  	m2 := &msgjson.Match{
  5055  		OrderID:  coid[:],
  5056  		MatchID:  mid[:],
  5057  		Quantity: cancelledQty,
  5058  		Address:  "testaddr",
  5059  	}
  5060  	sign(tDexPriv, m1)
  5061  	sign(tDexPriv, m2)
  5062  	msg, _ = msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{m1, m2})
  5063  	err = handleMatchRoute(tCore, rig.dc, msg)
  5064  	if err != nil {
  5065  		t.Fatalf("handleMatchRoute error (cancel with swaps): %v", err)
  5066  	}
  5067  	if tracker.cancel.matches.maker == nil {
  5068  		t.Fatalf("cancelMatches.maker not set")
  5069  	}
  5070  	if tracker.Trade().Filled() != qty {
  5071  		t.Fatalf("fill not set. %d != %d", tracker.Trade().Filled(), qty)
  5072  	}
  5073  	if tracker.cancel.matches.taker == nil {
  5074  		t.Fatalf("cancelMatches.taker not set")
  5075  	}
  5076  	// Since there are no unswapped orders, the change coin should be returned.
  5077  	if len(tDcrWallet.returnedCoins) != 1 || !bytes.Equal(tDcrWallet.returnedCoins[0].ID(), tDcrWallet.changeCoin.id) {
  5078  		t.Fatalf("change coin not returned")
  5079  	}
  5080  
  5081  	resetMatches := func() {
  5082  		tracker.matches = make(map[order.MatchID]*matchTracker)
  5083  		tracker.change = nil
  5084  		tracker.metaData.ChangeCoin = nil
  5085  		tracker.coinsLocked = true
  5086  	}
  5087  
  5088  	// If there is no change coin and no matches, the funding coin should be
  5089  	// returned instead.
  5090  	resetMatches()
  5091  	// The change coins would also have been added to the coins map, so delete
  5092  	// that too.
  5093  	delete(tracker.coins, tDcrWallet.changeCoin.String())
  5094  	err = handleMatchRoute(tCore, rig.dc, msg)
  5095  	if err != nil {
  5096  		t.Fatalf("handleMatchRoute error (cancel without swaps): %v", err)
  5097  	}
  5098  	if len(tDcrWallet.returnedCoins) != 1 || !bytes.Equal(tDcrWallet.returnedCoins[0].ID(), fundCoinDcrID) {
  5099  		t.Fatalf("change coin not returned (cancel without swaps)")
  5100  	}
  5101  
  5102  	// If the order is an immediate order, the asset.Swaps.LockChange should be
  5103  	// false regardless of whether the order is filled.
  5104  	resetMatches()
  5105  	tracker.cancel = nil
  5106  	rig.ws.queueResponse(msgjson.InitRoute, initAcker)
  5107  	msgMatch.Side = uint8(order.Maker)
  5108  	sign(tDexPriv, msgMatch) // Side is not in the serialization but whatever
  5109  	msg, _ = msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{msgMatch})
  5110  
  5111  	tracker.metaData.Status = order.OrderStatusEpoch
  5112  	lo.Force = order.ImmediateTiF
  5113  	err = handleMatchRoute(tCore, rig.dc, msg)
  5114  	if err != nil {
  5115  		t.Fatalf("handleMatchRoute error (immediate partial fill): %v", err)
  5116  	}
  5117  	lastSwaps := tDcrWallet.lastSwaps[len(tDcrWallet.lastSwaps)-1]
  5118  	if lastSwaps.LockChange != false {
  5119  		t.Fatalf("change locked for executed non-standing order (immediate partial fill)")
  5120  	}
  5121  }
  5122  
  5123  func TestReconcileTrades(t *testing.T) {
  5124  	rig := newTestRig()
  5125  	defer rig.shutdown()
  5126  	dc := rig.dc
  5127  
  5128  	mkt := dc.marketConfig(tDcrBtcMktName)
  5129  	rig.core.wallets[mkt.Base], _ = newTWallet(mkt.Base)
  5130  	rig.core.wallets[mkt.Quote], _ = newTWallet(mkt.Quote)
  5131  	walletSet, _, _, err := rig.core.walletSet(dc, mkt.Base, mkt.Quote, true)
  5132  	if err != nil {
  5133  		t.Fatalf("walletSet error: %v", err)
  5134  	}
  5135  
  5136  	type orderSet struct {
  5137  		epoch               *trackedTrade
  5138  		booked              *trackedTrade // standing limit orders only
  5139  		bookedPendingCancel *trackedTrade // standing limit orders only
  5140  		executed            *trackedTrade
  5141  	}
  5142  	makeOrderSet := func(force order.TimeInForce) *orderSet {
  5143  		orders := &orderSet{
  5144  			epoch:    makeTradeTracker(rig, walletSet, force, order.OrderStatusEpoch),
  5145  			executed: makeTradeTracker(rig, walletSet, force, order.OrderStatusExecuted),
  5146  		}
  5147  		if force == order.StandingTiF {
  5148  			orders.booked = makeTradeTracker(rig, walletSet, force, order.OrderStatusBooked)
  5149  			orders.bookedPendingCancel = makeTradeTracker(rig, walletSet, force, order.OrderStatusBooked)
  5150  			orders.bookedPendingCancel.cancel = &trackedCancel{
  5151  				CancelOrder: order.CancelOrder{
  5152  					P: order.Prefix{
  5153  						ServerTime: time.Now().UTC().Add(-16 * time.Minute),
  5154  					},
  5155  				},
  5156  				epochLen: mkt.EpochLen,
  5157  			}
  5158  		}
  5159  		return orders
  5160  	}
  5161  
  5162  	standingOrders := makeOrderSet(order.StandingTiF)
  5163  	immediateOrders := makeOrderSet(order.ImmediateTiF)
  5164  
  5165  	tests := []struct {
  5166  		name                string
  5167  		clientOrders        []*trackedTrade        // orders known to the client
  5168  		serverOrders        []*msgjson.OrderStatus // orders considered active by the server
  5169  		orderStatusRes      []*msgjson.OrderStatus // server's response to order_status requests
  5170  		expectOrderStatuses map[order.OrderID]order.OrderStatus
  5171  	}{
  5172  		{
  5173  			name:         "server orders unknown to client",
  5174  			clientOrders: []*trackedTrade{},
  5175  			serverOrders: []*msgjson.OrderStatus{
  5176  				{
  5177  					ID:     ordertest.RandomOrderID().Bytes(),
  5178  					Status: uint16(order.OrderStatusBooked),
  5179  				},
  5180  			},
  5181  			expectOrderStatuses: map[order.OrderID]order.OrderStatus{},
  5182  		},
  5183  		{
  5184  			name: "different server-client statuses",
  5185  			clientOrders: []*trackedTrade{
  5186  				standingOrders.epoch,
  5187  				standingOrders.booked,
  5188  				standingOrders.bookedPendingCancel,
  5189  				immediateOrders.epoch,
  5190  				immediateOrders.executed,
  5191  			},
  5192  			serverOrders: []*msgjson.OrderStatus{
  5193  				{
  5194  					ID:     standingOrders.epoch.ID().Bytes(),
  5195  					Status: uint16(order.OrderStatusBooked), // now booked
  5196  				},
  5197  				{
  5198  					ID:     standingOrders.booked.ID().Bytes(),
  5199  					Status: uint16(order.OrderStatusEpoch), // invald! booked orders cannot return to epoch!
  5200  				},
  5201  				{
  5202  					ID:     standingOrders.bookedPendingCancel.ID().Bytes(),
  5203  					Status: uint16(order.OrderStatusBooked), // still booked, cancel order should be deleted
  5204  				},
  5205  				{
  5206  					ID:     immediateOrders.epoch.ID().Bytes(),
  5207  					Status: uint16(order.OrderStatusBooked), // invalid, immediate orders cannot be booked!
  5208  				},
  5209  				{
  5210  					ID:     immediateOrders.executed.ID().Bytes(),
  5211  					Status: uint16(order.OrderStatusBooked), // invalid, inactive orders should not be returned by DEX!
  5212  				},
  5213  			},
  5214  			expectOrderStatuses: map[order.OrderID]order.OrderStatus{
  5215  				standingOrders.epoch.ID():               order.OrderStatusBooked,   // epoch => booked
  5216  				standingOrders.booked.ID():              order.OrderStatusBooked,   // should not change, cannot return to epoch
  5217  				standingOrders.bookedPendingCancel.ID(): order.OrderStatusBooked,   // no status change
  5218  				immediateOrders.epoch.ID():              order.OrderStatusEpoch,    // should not change, cannot be booked
  5219  				immediateOrders.executed.ID():           order.OrderStatusExecuted, // should not change, inactive cannot become active
  5220  			},
  5221  		},
  5222  		{
  5223  			name: "active becomes inactive",
  5224  			clientOrders: []*trackedTrade{
  5225  				standingOrders.epoch,
  5226  				standingOrders.booked,
  5227  				standingOrders.bookedPendingCancel,
  5228  				standingOrders.executed,
  5229  				immediateOrders.epoch,
  5230  				immediateOrders.executed,
  5231  			},
  5232  			serverOrders: []*msgjson.OrderStatus{}, // no active order reported by server
  5233  			orderStatusRes: []*msgjson.OrderStatus{
  5234  				{
  5235  					ID:     standingOrders.epoch.ID().Bytes(),
  5236  					Status: uint16(order.OrderStatusRevoked),
  5237  				},
  5238  				{
  5239  					ID:     standingOrders.booked.ID().Bytes(),
  5240  					Status: uint16(order.OrderStatusRevoked),
  5241  				},
  5242  				{
  5243  					ID:     standingOrders.bookedPendingCancel.ID().Bytes(),
  5244  					Status: uint16(order.OrderStatusCanceled),
  5245  				},
  5246  				{
  5247  					ID:     immediateOrders.epoch.ID().Bytes(),
  5248  					Status: uint16(order.OrderStatusExecuted),
  5249  				},
  5250  			},
  5251  			expectOrderStatuses: map[order.OrderID]order.OrderStatus{
  5252  				standingOrders.epoch.ID():               order.OrderStatusRevoked,  // preimage missed = revoked
  5253  				standingOrders.booked.ID():              order.OrderStatusRevoked,  // booked, not canceled = assume revoked (may actually be executed)
  5254  				standingOrders.bookedPendingCancel.ID(): order.OrderStatusCanceled, // booked pending canceled = assume canceled (may actually be revoked or executed)
  5255  				standingOrders.executed.ID():            order.OrderStatusExecuted, // should not change
  5256  				immediateOrders.epoch.ID():              order.OrderStatusExecuted, // preimage sent, not canceled = executed
  5257  				immediateOrders.executed.ID():           order.OrderStatusExecuted, // should not change
  5258  			},
  5259  		},
  5260  	}
  5261  
  5262  	for _, tt := range tests {
  5263  		// Track client orders in dc.trades.
  5264  		dc.tradeMtx.Lock()
  5265  		var pendingCancel *trackedTrade
  5266  		dc.trades = make(map[order.OrderID]*trackedTrade)
  5267  		for _, tracker := range tt.clientOrders {
  5268  			dc.trades[tracker.ID()] = tracker
  5269  			if tracker.cancel != nil {
  5270  				pendingCancel = tracker
  5271  			}
  5272  		}
  5273  		dc.tradeMtx.Unlock()
  5274  
  5275  		// Queue order_status response if required for reconciliation.
  5276  		if len(tt.orderStatusRes) > 0 {
  5277  			rig.ws.queueResponse(msgjson.OrderStatusRoute, func(msg *msgjson.Message, f msgFunc) error {
  5278  				resp, _ := msgjson.NewResponse(msg.ID, tt.orderStatusRes, nil)
  5279  				f(resp)
  5280  				return nil
  5281  			})
  5282  		}
  5283  
  5284  		// Reconcile tracked orders with server orders.
  5285  		dc.reconcileTrades(tt.serverOrders)
  5286  
  5287  		dc.tradeMtx.RLock()
  5288  		if len(dc.trades) != len(tt.expectOrderStatuses) {
  5289  			t.Fatalf("%s: post-reconcileTrades order count mismatch. expected %d, got %d",
  5290  				tt.name, len(tt.expectOrderStatuses), len(dc.trades))
  5291  		}
  5292  		for oid, tracker := range dc.trades {
  5293  			expectedStatus, expected := tt.expectOrderStatuses[oid]
  5294  			if !expected {
  5295  				t.Fatalf("%s: unexpected order %v tracked by client", tt.name, oid)
  5296  			}
  5297  			tracker.mtx.RLock()
  5298  			if tracker.metaData.Status != expectedStatus {
  5299  				t.Fatalf("%s: client reported wrong order status %v, expected %v",
  5300  					tt.name, tracker.metaData.Status, expectedStatus)
  5301  			}
  5302  			tracker.mtx.RUnlock()
  5303  		}
  5304  		dc.tradeMtx.RUnlock()
  5305  
  5306  		// Check if a previously canceled order existed; if the order is still
  5307  		// active (Epoch/Booked status) and the cancel order is deleted, having
  5308  		// been there for over 15 minutes since the cancel order's epoch ended.
  5309  		if pendingCancel != nil {
  5310  			pendingCancel.mtx.RLock()
  5311  			status, stillHasCancelOrder := pendingCancel.metaData.Status, pendingCancel.cancel != nil
  5312  			pendingCancel.mtx.RUnlock()
  5313  			if status == order.OrderStatusBooked {
  5314  				if stillHasCancelOrder {
  5315  					t.Fatalf("%s: expected stale cancel order to be deleted for now-booked order", tt.name)
  5316  				}
  5317  				// Cancel order deleted. Canceling the order again should succeed.
  5318  				rig.queueCancel(nil)
  5319  				err = rig.core.Cancel(pendingCancel.ID().Bytes())
  5320  				if err != nil {
  5321  					t.Fatalf("cancel order error after deleting previous stale cancel: %v", err)
  5322  				}
  5323  			}
  5324  		}
  5325  	}
  5326  }
  5327  
  5328  func makeTradeTracker(rig *testRig, walletSet *walletSet, force order.TimeInForce, status order.OrderStatus) *trackedTrade {
  5329  	qty := 4 * dcrBtcLotSize
  5330  	lo, dbOrder, preImg, _ := makeLimitOrder(rig.dc, true, qty, dcrBtcRateStep)
  5331  	lo.Force = force
  5332  	dbOrder.MetaData.Status = status
  5333  
  5334  	return newTrackedTrade(dbOrder, preImg, rig.dc,
  5335  		rig.core.lockTimeTaker, rig.core.lockTimeMaker,
  5336  		rig.db, rig.queue, walletSet, nil, rig.core.notify,
  5337  		rig.core.formatDetails)
  5338  }
  5339  
  5340  func TestRefunds(t *testing.T) {
  5341  	rig := newTestRig()
  5342  	defer rig.shutdown()
  5343  
  5344  	dc := rig.dc
  5345  	tCore := rig.core
  5346  	btcWallet, tBtcWallet := newTWallet(tUTXOAssetB.ID)
  5347  	tCore.wallets[tUTXOAssetB.ID] = btcWallet
  5348  	btcWallet.address = "DsVmA7aqqWeKWy461hXjytbZbgCqbB8g2dq"
  5349  	btcWallet.Unlock(rig.crypter)
  5350  
  5351  	ethWallet, tEthWallet := newTAccountLocker(tACCTAsset.ID)
  5352  	tCore.wallets[tACCTAsset.ID] = ethWallet
  5353  	ethWallet.address = "18d65fb8d60c1199bb1ad381be47aa692b482605"
  5354  	ethWallet.Unlock(rig.crypter)
  5355  
  5356  	checkStatus := func(tag string, match *matchTracker, wantStatus order.MatchStatus) {
  5357  		t.Helper()
  5358  		if match.Status != wantStatus {
  5359  			t.Fatalf("%s: wrong status wanted %v, got %v", tag, wantStatus, match.Status)
  5360  		}
  5361  	}
  5362  	checkRefund := func(tracker *trackedTrade, match *matchTracker, expectAmt uint64) {
  5363  		t.Helper()
  5364  		// Confirm that the status is SwapCast.
  5365  		if match.Side == order.Maker {
  5366  			checkStatus("maker swapped", match, order.MakerSwapCast)
  5367  		} else {
  5368  			checkStatus("taker swapped", match, order.TakerSwapCast)
  5369  		}
  5370  		// Confirm isRefundable = true.
  5371  		if !tracker.isRefundable(tCore.ctx, match) {
  5372  			t.Fatalf("%s's swap not refundable", match.Side)
  5373  		}
  5374  		// Check refund.
  5375  		amtRefunded, err := rig.core.refundMatches(tracker, []*matchTracker{match})
  5376  		if err != nil {
  5377  			t.Fatalf("unexpected refund error %v", err)
  5378  		}
  5379  		// Check refunded amount.
  5380  		if amtRefunded != expectAmt {
  5381  			t.Fatalf("expected %d refund amount, got %d", expectAmt, amtRefunded)
  5382  		}
  5383  		// Confirm isRefundable = false.
  5384  		if tracker.isRefundable(tCore.ctx, match) {
  5385  			t.Fatalf("%s's swap refundable after being refunded", match.Side)
  5386  		}
  5387  		// Expect refund re-attempt to not refund any coin.
  5388  		amtRefunded, err = rig.core.refundMatches(tracker, []*matchTracker{match})
  5389  		if err != nil {
  5390  			t.Fatalf("unexpected refund error %v", err)
  5391  		}
  5392  		if amtRefunded != 0 {
  5393  			t.Fatalf("expected 0 refund amount, got %d", amtRefunded)
  5394  		}
  5395  		// Confirm that the status is unchanged.
  5396  		if match.Side == order.Maker {
  5397  			checkStatus("maker swapped", match, order.MakerSwapCast)
  5398  		} else {
  5399  			checkStatus("taker swapped", match, order.TakerSwapCast)
  5400  		}
  5401  
  5402  		if _, is := tracker.accountRefunder(); is {
  5403  			if tEthWallet.refundFeeSuggestion != tMaxFeeRate {
  5404  				t.Fatalf("refund suggestion for account asset %v != server max fee rate %v",
  5405  					tEthWallet.refundFeeSuggestion, tACCTAsset.MaxFeeRate)
  5406  			}
  5407  		}
  5408  	}
  5409  
  5410  	matchSize := 4 * dcrBtcLotSize
  5411  	qty := 3 * matchSize
  5412  	rate := dcrBtcRateStep * 10
  5413  	lo, dbOrder, preImgL, addr := makeLimitOrder(dc, false, qty, dcrBtcRateStep)
  5414  	loid := lo.ID()
  5415  	mid := ordertest.RandomMatchID()
  5416  	walletSet, _, _, err := tCore.walletSet(dc, tUTXOAssetB.ID, tACCTAsset.ID, false)
  5417  	if err != nil {
  5418  		t.Fatalf("walletSet error: %v", err)
  5419  	}
  5420  	fundCoinsETH := asset.Coins{&tCoin{id: encode.RandomBytes(36)}}
  5421  	tEthWallet.fundingCoins = fundCoinsETH
  5422  	tEthWallet.fundRedeemScripts = []dex.Bytes{nil}
  5423  	tracker := newTrackedTrade(dbOrder, preImgL, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker,
  5424  		rig.db, rig.queue, walletSet, fundCoinsETH, rig.core.notify, rig.core.formatDetails)
  5425  	rig.dc.trades[tracker.ID()] = tracker
  5426  
  5427  	// MAKER REFUND, INVALID TAKER COUNTERSWAP
  5428  	//
  5429  
  5430  	matchTime := time.Now().Truncate(time.Millisecond).UTC()
  5431  	msgMatch := &msgjson.Match{
  5432  		OrderID:      loid[:],
  5433  		MatchID:      mid[:],
  5434  		Quantity:     matchSize,
  5435  		Rate:         rate,
  5436  		Address:      "counterparty-address",
  5437  		Side:         uint8(order.Maker),
  5438  		ServerTime:   uint64(matchTime.UnixMilli()),
  5439  		FeeRateQuote: tMaxFeeRate,
  5440  	}
  5441  	swapID := encode.RandomBytes(36)
  5442  	contract := encode.RandomBytes(36)
  5443  	tEthWallet.swapReceipts = []asset.Receipt{&tReceipt{coin: &tCoin{id: swapID}, contract: contract}}
  5444  	sign(tDexPriv, msgMatch)
  5445  	msg, _ := msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{msgMatch})
  5446  	rig.ws.queueResponse(msgjson.InitRoute, initAcker)
  5447  	err = handleMatchRoute(tCore, rig.dc, msg)
  5448  	if err != nil {
  5449  		t.Fatalf("match messages error: %v", err)
  5450  	}
  5451  	match, found := tracker.matches[mid]
  5452  	if !found {
  5453  		t.Fatalf("match not found")
  5454  	}
  5455  
  5456  	// We're the maker, so the init transaction should be broadcast.
  5457  	checkStatus("maker swapped", match, order.MakerSwapCast)
  5458  	proof := &match.MetaData.Proof
  5459  	if !bytes.Equal(proof.ContractData, contract) {
  5460  		t.Fatalf("invalid contract recorded for Maker swap")
  5461  	}
  5462  
  5463  	// Send the counter-party's init info.
  5464  	audit, auditInfo := tMsgAudit(loid, mid, addr, matchSize, proof.SecretHash)
  5465  	tBtcWallet.auditInfo = auditInfo
  5466  	auditInfo.Expiration = encode.DropMilliseconds(matchTime.Add(tracker.lockTimeMaker).UTC())
  5467  
  5468  	// Check audit errors.
  5469  	tBtcWallet.auditErr = tErr
  5470  	err = tracker.auditContract(match, audit.CoinID, audit.Contract, nil)
  5471  	if err == nil {
  5472  		t.Fatalf("no maker error for AuditContract error")
  5473  	}
  5474  
  5475  	// Attempt refund.
  5476  	tEthWallet.refundCoin = encode.RandomBytes(36)
  5477  	tEthWallet.refundErr = nil
  5478  	tBtcWallet.refundCoin = nil
  5479  	tBtcWallet.refundErr = fmt.Errorf("unexpected call to btcWallet.Refund")
  5480  	matchSizeQuoteUnits := calc.BaseToQuote(rate, matchSize)
  5481  	// Make the contract appear expired
  5482  	tEthWallet.contractExpired = true
  5483  	tEthWallet.contractLockTime = time.Now()
  5484  	checkRefund(tracker, match, matchSizeQuoteUnits)
  5485  	tEthWallet.contractExpired = false
  5486  	tEthWallet.contractLockTime = time.Now().Add(time.Minute)
  5487  
  5488  	// TAKER REFUND, NO MAKER REDEEM
  5489  	//
  5490  	// Reset funding coins in the trackedTrade, wipe change coin.
  5491  	matchTime = time.Now().Truncate(time.Millisecond).UTC()
  5492  	tracker.mtx.Lock()
  5493  	tracker.coins = mapifyCoins(fundCoinsETH)
  5494  	tracker.coinsLocked = true
  5495  	tracker.changeLocked = false
  5496  	tracker.change = nil
  5497  	tracker.metaData.ChangeCoin = nil
  5498  	tracker.mtx.Unlock()
  5499  	mid = ordertest.RandomMatchID()
  5500  	msgMatch = &msgjson.Match{
  5501  		OrderID:      loid[:],
  5502  		MatchID:      mid[:],
  5503  		Quantity:     matchSize,
  5504  		Rate:         rate,
  5505  		Address:      "counterparty-address",
  5506  		Side:         uint8(order.Taker),
  5507  		ServerTime:   uint64(matchTime.UnixMilli()),
  5508  		FeeRateQuote: tMaxFeeRate,
  5509  	}
  5510  	sign(tDexPriv, msgMatch)
  5511  	msg, _ = msgjson.NewRequest(1, msgjson.MatchRoute, []*msgjson.Match{msgMatch})
  5512  	err = handleMatchRoute(tCore, rig.dc, msg)
  5513  	if err != nil {
  5514  		t.Fatalf("match messages error: %v", err)
  5515  	}
  5516  	match, found = tracker.matches[mid]
  5517  	if !found {
  5518  		t.Fatalf("match not found")
  5519  	}
  5520  	checkStatus("taker matched", match, order.NewlyMatched)
  5521  	// Send through the audit request for the maker's init.
  5522  	rig.db.updateMatchChan = make(chan order.MatchStatus, 1)
  5523  	audit, auditInfo = tMsgAudit(loid, mid, addr, matchSize, nil)
  5524  	tBtcWallet.auditInfo = auditInfo
  5525  	auditInfo.Expiration = encode.DropMilliseconds(matchTime.Add(tracker.lockTimeMaker))
  5526  	tBtcWallet.auditErr = nil
  5527  	msg, _ = msgjson.NewRequest(1, msgjson.AuditRoute, audit)
  5528  	err = handleAuditRoute(tCore, rig.dc, msg)
  5529  	if err != nil {
  5530  		t.Fatalf("taker's match message error: %v", err)
  5531  	}
  5532  	// let the async auditContract run, updating match status
  5533  	newMatchStatus := <-rig.db.updateMatchChan
  5534  	if newMatchStatus != order.MakerSwapCast {
  5535  		t.Fatalf("wrong match status. wanted %v, got %v", order.MakerSwapCast, newMatchStatus)
  5536  	}
  5537  	<-rig.db.updateMatchChan // AuditSig set in second update to match data
  5538  	tracker.mtx.RLock()
  5539  	if !bytes.Equal(match.MetaData.Proof.Auth.AuditSig, audit.Sig) {
  5540  		t.Fatalf("audit sig not set for taker")
  5541  	}
  5542  	tracker.mtx.RUnlock()
  5543  	// maker's swap confirmation should trigger taker's swap bcast
  5544  	tBtcWallet.setConfs(auditInfo.Coin.ID(), tUTXOAssetB.SwapConf, nil)
  5545  	counterSwapID := encode.RandomBytes(36)
  5546  	counterScript := encode.RandomBytes(36)
  5547  	tEthWallet.swapReceipts = []asset.Receipt{&tReceipt{coin: &tCoin{id: counterSwapID}, contract: counterScript}}
  5548  	rig.ws.queueResponse(msgjson.InitRoute, initAcker)
  5549  	tCore.tickAsset(dc, tUTXOAssetB.ID)
  5550  	newMatchStatus = <-rig.db.updateMatchChan // MakerSwapCast->TakerSwapCast (after taker's swap bcast)
  5551  	if newMatchStatus != order.TakerSwapCast {
  5552  		t.Fatalf("wrong match status. wanted %v, got %v", order.TakerSwapCast, newMatchStatus)
  5553  	}
  5554  	tracker.mtx.RLock()
  5555  	if !bytes.Equal(match.MetaData.Proof.ContractData, counterScript) {
  5556  		t.Fatalf("invalid contract recorded for Taker swap")
  5557  	}
  5558  	tracker.mtx.RUnlock()
  5559  	// still takerswapcast, but with initsig
  5560  	newMatchStatus = <-rig.db.updateMatchChan
  5561  	if newMatchStatus != order.TakerSwapCast {
  5562  		t.Fatalf("wrong match status wanted %v, got %v", order.TakerSwapCast, newMatchStatus)
  5563  	}
  5564  	tracker.mtx.RLock()
  5565  	auth := &match.MetaData.Proof.Auth
  5566  	if len(auth.InitSig) == 0 {
  5567  		t.Fatalf("init sig not recorded for valid init ack")
  5568  	}
  5569  	tracker.mtx.RUnlock()
  5570  
  5571  	// Attempt refund.
  5572  	rig.db.updateMatchChan = nil
  5573  	tEthWallet.contractExpired = true
  5574  	tEthWallet.contractLockTime = time.Now()
  5575  	checkRefund(tracker, match, matchSizeQuoteUnits)
  5576  	tEthWallet.contractExpired = false
  5577  	tEthWallet.contractLockTime = time.Now().Add(time.Minute)
  5578  }
  5579  
  5580  func TestNotifications(t *testing.T) {
  5581  	rig := newTestRig()
  5582  	defer rig.shutdown()
  5583  
  5584  	// Insert a notification into the database.
  5585  	typedNote := newOrderNote("123", "abc", "def", 100, nil)
  5586  
  5587  	tCore := rig.core
  5588  	ch := tCore.NotificationFeed()
  5589  	tCore.notify(typedNote)
  5590  	select {
  5591  	case n := <-ch.C:
  5592  		dbtest.MustCompareNotifications(t, n.DBNote(), &typedNote.Notification)
  5593  	case <-time.After(time.Second):
  5594  		t.Fatalf("no notification received over the notification channel")
  5595  	}
  5596  }
  5597  
  5598  func generateMatch(rig *testRig, baseID, quoteID uint32) (uint64, *order.LimitOrder, *db.MetaOrder, *db.MetaMatch, *tCoin) {
  5599  	const redemptionReserves = 50
  5600  	const refundReserves = 75
  5601  
  5602  	qty := dcrBtcLotSize * 5
  5603  	rate := dcrBtcRateStep * 5
  5604  	lo := &order.LimitOrder{
  5605  		P: order.Prefix{
  5606  			OrderType:  order.LimitOrderType,
  5607  			BaseAsset:  baseID,
  5608  			QuoteAsset: quoteID,
  5609  			ClientTime: time.Now(),
  5610  			ServerTime: time.Now(),
  5611  			Commit:     ordertest.RandomCommitment(),
  5612  		},
  5613  		T: order.Trade{
  5614  			Quantity: qty,
  5615  			Sell:     true,
  5616  		},
  5617  		Rate:  rate,
  5618  		Force: order.StandingTiF, // we're calling it booked in OrderMetaData
  5619  	}
  5620  
  5621  	changeCoinID := encode.RandomBytes(36)
  5622  	changeCoin := &tCoin{id: changeCoinID}
  5623  
  5624  	dbOrder := &db.MetaOrder{
  5625  		MetaData: &db.OrderMetaData{
  5626  			Status:             order.OrderStatusBooked,
  5627  			Host:               tDexHost,
  5628  			Proof:              db.OrderProof{},
  5629  			ChangeCoin:         changeCoinID,
  5630  			RedemptionReserves: redemptionReserves,
  5631  			RefundReserves:     refundReserves,
  5632  		},
  5633  		Order: lo,
  5634  	}
  5635  
  5636  	oid := lo.ID()
  5637  	mid := ordertest.RandomMatchID()
  5638  	addr := ordertest.RandomAddress()
  5639  	matchQty := qty - dcrBtcLotSize
  5640  	match := &db.MetaMatch{
  5641  		MetaData: &db.MatchMetaData{
  5642  			Proof: db.MatchProof{
  5643  				CounterContract: encode.RandomBytes(50),
  5644  				SecretHash:      encode.RandomBytes(32),
  5645  				MakerSwap:       encode.RandomBytes(32),
  5646  				Auth: db.MatchAuth{
  5647  					MatchSig: encode.RandomBytes(32),
  5648  					InitSig:  encode.RandomBytes(32), // otherwise MatchComplete will be seen as a cancel order match (inactive)
  5649  				},
  5650  			},
  5651  			DEX:   tDexHost,
  5652  			Base:  baseID,
  5653  			Quote: quoteID,
  5654  		},
  5655  		UserMatch: &order.UserMatch{
  5656  			OrderID:  oid,
  5657  			MatchID:  mid,
  5658  			Quantity: matchQty,
  5659  			Rate:     rate,
  5660  			Address:  addr,
  5661  			Status:   order.MakerSwapCast,
  5662  			Side:     order.Taker,
  5663  		},
  5664  	}
  5665  
  5666  	// Need to return an order from db.ActiveDEXOrders
  5667  	rig.db.activeDEXOrders = []*db.MetaOrder{dbOrder}
  5668  	rig.db.orderOrders[oid] = dbOrder
  5669  
  5670  	rig.db.activeMatchOIDs = []order.OrderID{oid}
  5671  	rig.db.matchesForOID = []*db.MetaMatch{match}
  5672  
  5673  	return qty, lo, dbOrder, match, changeCoin
  5674  }
  5675  
  5676  var (
  5677  	activeStatuses   = []order.OrderStatus{order.OrderStatusEpoch, order.OrderStatusBooked}
  5678  	inactiveStatuses = []order.OrderStatus{order.OrderStatusExecuted, order.OrderStatusCanceled, order.OrderStatusRevoked}
  5679  
  5680  	reservationTests = []struct {
  5681  		name          string
  5682  		sell          bool
  5683  		side          []order.MatchSide
  5684  		orderStatuses []order.OrderStatus
  5685  		matchStatuses []order.MatchStatus
  5686  		expectedCoins int
  5687  	}{
  5688  		// With an active order, the change coin should always be loaded.
  5689  		{
  5690  			name:          "active-order, sell",
  5691  			sell:          true,
  5692  			side:          []order.MatchSide{order.Taker, order.Maker},
  5693  			orderStatuses: activeStatuses,
  5694  			matchStatuses: []order.MatchStatus{order.NewlyMatched, order.MakerSwapCast,
  5695  				order.TakerSwapCast, order.MakerRedeemed, order.MatchComplete},
  5696  			expectedCoins: 1,
  5697  		},
  5698  		{
  5699  			name:          "active-order, buy",
  5700  			sell:          false,
  5701  			side:          []order.MatchSide{order.Taker, order.Maker},
  5702  			orderStatuses: activeStatuses,
  5703  			matchStatuses: []order.MatchStatus{order.NewlyMatched, order.MakerSwapCast,
  5704  				order.TakerSwapCast, order.MakerRedeemed, order.MatchComplete},
  5705  			expectedCoins: 1,
  5706  		},
  5707  		// With an inactive order, as taker, if match is >= TakerSwapCast, there
  5708  		// will be no funding coin fetched.
  5709  		{
  5710  			name:          "inactive taker > MakerSwapCast, sell",
  5711  			sell:          true,
  5712  			side:          []order.MatchSide{order.Taker},
  5713  			orderStatuses: inactiveStatuses,
  5714  			matchStatuses: []order.MatchStatus{order.TakerSwapCast, order.MakerRedeemed,
  5715  				order.MatchComplete},
  5716  			expectedCoins: 0,
  5717  		},
  5718  		{
  5719  			name:          "inactive taker > MakerSwapCast, buy",
  5720  			sell:          false,
  5721  			side:          []order.MatchSide{order.Taker},
  5722  			orderStatuses: inactiveStatuses,
  5723  			matchStatuses: []order.MatchStatus{order.TakerSwapCast, order.MakerRedeemed,
  5724  				order.MatchComplete},
  5725  			expectedCoins: 0,
  5726  		},
  5727  		// But there will be for NewlyMatched && MakerSwapCast
  5728  		{
  5729  			name:          "inactive taker < TakerSwapCast, sell",
  5730  			sell:          true,
  5731  			side:          []order.MatchSide{order.Taker},
  5732  			orderStatuses: inactiveStatuses,
  5733  			matchStatuses: []order.MatchStatus{order.NewlyMatched, order.MakerSwapCast},
  5734  			expectedCoins: 1,
  5735  		},
  5736  		{
  5737  			name:          "inactive taker < TakerSwapCast, buy",
  5738  			sell:          false,
  5739  			side:          []order.MatchSide{order.Taker},
  5740  			orderStatuses: inactiveStatuses,
  5741  			matchStatuses: []order.MatchStatus{order.NewlyMatched, order.MakerSwapCast},
  5742  			expectedCoins: 1,
  5743  		},
  5744  		// For a maker with an inactive order, only NewlyMatched would
  5745  		// necessitate fetching of coins.
  5746  		{
  5747  			name:          "inactive maker NewlyMatched, sell",
  5748  			sell:          true,
  5749  			side:          []order.MatchSide{order.Maker},
  5750  			orderStatuses: inactiveStatuses,
  5751  			matchStatuses: []order.MatchStatus{order.NewlyMatched},
  5752  			expectedCoins: 1,
  5753  		},
  5754  		{
  5755  			name:          "inactive maker NewlyMatched, buy",
  5756  			sell:          false,
  5757  			side:          []order.MatchSide{order.Maker},
  5758  			orderStatuses: inactiveStatuses,
  5759  			matchStatuses: []order.MatchStatus{order.NewlyMatched},
  5760  			expectedCoins: 1,
  5761  		},
  5762  		{
  5763  			name:          "inactive maker > NewlyMatched, sell",
  5764  			sell:          true,
  5765  			side:          []order.MatchSide{order.Maker},
  5766  			orderStatuses: inactiveStatuses,
  5767  			matchStatuses: []order.MatchStatus{order.MakerSwapCast, order.TakerSwapCast,
  5768  				order.MakerRedeemed, order.MatchComplete},
  5769  			expectedCoins: 0,
  5770  		},
  5771  		{
  5772  			name:          "inactive maker > NewlyMatched, buy",
  5773  			sell:          false,
  5774  			side:          []order.MatchSide{order.Maker},
  5775  			orderStatuses: inactiveStatuses,
  5776  			matchStatuses: []order.MatchStatus{order.MakerSwapCast, order.TakerSwapCast,
  5777  				order.MakerRedeemed, order.MatchComplete},
  5778  			expectedCoins: 0,
  5779  		},
  5780  	}
  5781  )
  5782  
  5783  // auth sets the account as authenticated at the provided tier.
  5784  func auth(a *dexAccount) {
  5785  	a.authMtx.Lock()
  5786  	a.isAuthed = true
  5787  	a.rep = account.Reputation{BondedTier: 1}
  5788  	a.authMtx.Unlock()
  5789  }
  5790  
  5791  func TestResolveActiveTrades(t *testing.T) {
  5792  	rig := newTestRig()
  5793  	defer rig.shutdown()
  5794  	tCore := rig.core
  5795  
  5796  	auth(rig.acct) // Short path through initializeDEXConnections
  5797  
  5798  	utxoAsset /* base */, acctAsset /* quote */ := tUTXOAssetB, tACCTAsset
  5799  
  5800  	btcWallet, tBtcWallet := newTWallet(utxoAsset.ID)
  5801  	tCore.wallets[utxoAsset.ID] = btcWallet
  5802  
  5803  	ethWallet, tEthWallet := newTAccountLocker(acctAsset.ID)
  5804  	tCore.wallets[acctAsset.ID] = ethWallet
  5805  
  5806  	// Create an order
  5807  	qty, lo, dbOrder, match, changeCoin := generateMatch(rig, utxoAsset.ID, acctAsset.ID)
  5808  	redemptionReserves, refundReserves := dbOrder.MetaData.RedemptionReserves, dbOrder.MetaData.RefundReserves
  5809  
  5810  	tBtcWallet.fundingCoins = asset.Coins{changeCoin}
  5811  	tEthWallet.fundingCoins = asset.Coins{changeCoin}
  5812  
  5813  	// reset
  5814  	reset := func() {
  5815  		rig.acct.lock()
  5816  		btcWallet.Lock(time.Second)
  5817  		ethWallet.Lock(time.Second)
  5818  		tEthWallet.reservedRedemption = 0
  5819  		tEthWallet.reservedRefund = 0
  5820  
  5821  		rig.dc.trades = make(map[order.OrderID]*trackedTrade)
  5822  	}
  5823  
  5824  	// Ensure the order is good, and reset the state.
  5825  	runTest := func(tag string, expAddedToTradesMap, expReadyToTick, expBTCUnlocked, expETHUnlocked bool, expCoinsLoaded int) {
  5826  		t.Helper()
  5827  		defer reset()
  5828  
  5829  		description := fmt.Sprintf("%s: side = %s, order status = %s, match status = %s",
  5830  			tag, match.Side, dbOrder.MetaData.Status, match.Status)
  5831  		tCore.loginMtx.Lock()
  5832  		tCore.loggedIn = false
  5833  		tCore.loginMtx.Unlock()
  5834  		err := tCore.Login(tPW)
  5835  		if err != nil {
  5836  			t.Fatalf("%s: login error: %v", description, err)
  5837  		}
  5838  
  5839  		trade, found := rig.dc.trades[lo.ID()]
  5840  		if expAddedToTradesMap != found {
  5841  			t.Fatalf("%s: expected added to trades map = %v, but got %v. len(trades) = %d", description, expAddedToTradesMap, found, len(rig.dc.trades))
  5842  		}
  5843  		if !expAddedToTradesMap {
  5844  			return
  5845  		}
  5846  
  5847  		if expBTCUnlocked != btcWallet.unlocked() {
  5848  			t.Fatalf("%s: btc wallet unlocked = %v but got %v", description, expBTCUnlocked, btcWallet.unlocked())
  5849  		}
  5850  
  5851  		if expETHUnlocked != ethWallet.unlocked() {
  5852  			t.Fatalf("%s: eth wallet unlocked = %v but got %v", description, expETHUnlocked, ethWallet.unlocked())
  5853  		}
  5854  
  5855  		_, found = trade.matches[match.MatchID]
  5856  		if !found {
  5857  			t.Fatalf("%s: trade with expected order id not found. len(matches) = %d", description, len(trade.matches))
  5858  		}
  5859  
  5860  		if len(trade.coins) != expCoinsLoaded {
  5861  			t.Fatalf("%s: expected %d coin loaded, got %d", description, expCoinsLoaded, len(trade.coins))
  5862  		}
  5863  
  5864  		if found && expReadyToTick != trade.readyToTick {
  5865  			t.Fatalf("%s: expected ready to tick = %v, but got %v", description, expReadyToTick, trade.readyToTick)
  5866  		}
  5867  		if !expReadyToTick {
  5868  			return
  5869  		}
  5870  
  5871  		if lo.T.Sell && ((match.Side == order.Taker && match.Status < order.MatchComplete) ||
  5872  			(match.Side == order.Taker && match.Status < order.MakerRedeemed)) {
  5873  			var reReserveQty uint64 = redemptionReserves
  5874  			if dbOrder.MetaData.Status > order.OrderStatusBooked {
  5875  				reReserveQty = applyFraction(match.Quantity, qty, redemptionReserves)
  5876  			}
  5877  
  5878  			if tEthWallet.reservedRedemption != reReserveQty {
  5879  				t.Fatalf("%s: redemption funds not reserved, %d != %d", description, tEthWallet.reservedRedemption, reReserveQty)
  5880  			}
  5881  		}
  5882  
  5883  		if !lo.T.Sell && match.Status < order.MakerRedeemed {
  5884  			var reRefundQty uint64 = refundReserves
  5885  			if dbOrder.MetaData.Status > order.OrderStatusBooked {
  5886  				reRefundQty = applyFraction(match.Quantity, qty, refundReserves)
  5887  			}
  5888  
  5889  			if tEthWallet.reservedRefund != reRefundQty {
  5890  				t.Fatalf("%s: refund funds not reserved, %d != %d", description, tEthWallet.reservedRefund, reRefundQty)
  5891  			}
  5892  		}
  5893  
  5894  	}
  5895  
  5896  	runTest("initial", true, true, true, true, 1)
  5897  
  5898  	// No base wallet. Trade will not be in the map.
  5899  	delete(tCore.wallets, utxoAsset.ID)
  5900  	runTest("no base wallet", false, false, false, false, 0)
  5901  	tCore.wallets[utxoAsset.ID] = btcWallet
  5902  
  5903  	// Base wallet unlock errors. Trade will be in map, but it will not be
  5904  	// ready to tick.
  5905  	tBtcWallet.unlockErr = tErr
  5906  	tBtcWallet.locked = true
  5907  	runTest("base unlock", true, false, false, false, 0)
  5908  	tBtcWallet.unlockErr = nil
  5909  	tBtcWallet.locked = false
  5910  
  5911  	// No quote wallet. Trade will not be in the map.
  5912  	delete(tCore.wallets, acctAsset.ID)
  5913  	runTest("missing quote", false, false, false, false, 0)
  5914  	tCore.wallets[acctAsset.ID] = ethWallet
  5915  
  5916  	// Quote wallet unlock errors. Trade will be in map, but it will not be
  5917  	// ready to tick.
  5918  	tEthWallet.unlockErr = tErr
  5919  	tEthWallet.locked = true
  5920  	runTest("quote unlock", true, false, true, false, 0)
  5921  	tEthWallet.unlockErr = nil
  5922  	tEthWallet.locked = false
  5923  
  5924  	// Funding coin error still puts it in the trades map, and sets ready to tick,
  5925  	// just with no coins locked.
  5926  	tBtcWallet.fundingCoinErr = tErr
  5927  	runTest("funding coin", true, true, true, true, 0)
  5928  	tBtcWallet.fundingCoinErr = nil
  5929  
  5930  	// No matches
  5931  	rig.db.activeMatchOIDSErr = tErr
  5932  	runTest("matches error", false, false, false, false, 0)
  5933  	rig.db.activeMatchOIDSErr = nil
  5934  
  5935  	for _, tt := range reservationTests {
  5936  		lo.T.Sell = tt.sell
  5937  		if tt.sell {
  5938  			dbOrder.MetaData.RefundReserves = 0
  5939  			dbOrder.MetaData.RedemptionReserves = redemptionReserves
  5940  		} else {
  5941  			dbOrder.MetaData.RefundReserves = refundReserves
  5942  			dbOrder.MetaData.RedemptionReserves = 0
  5943  		}
  5944  		for _, side := range tt.side {
  5945  			match.Side = side
  5946  			for _, orderStatus := range tt.orderStatuses {
  5947  				dbOrder.MetaData.Status = orderStatus
  5948  				for _, matchStatus := range tt.matchStatuses {
  5949  					match.Status = matchStatus
  5950  					runTest(tt.name, true, true, true, true, tt.expectedCoins)
  5951  				}
  5952  			}
  5953  		}
  5954  	}
  5955  }
  5956  
  5957  func TestReReserveFunding(t *testing.T) {
  5958  	rig := newTestRig()
  5959  	defer rig.shutdown()
  5960  	tCore := rig.core
  5961  
  5962  	auth(rig.acct) // Short path through initializeDEXConnections
  5963  
  5964  	utxoAsset /* base */, acctAsset /* quote */ := tUTXOAssetB, tACCTAsset
  5965  
  5966  	btcWallet, tBtcWallet := newTWallet(utxoAsset.ID)
  5967  	tCore.wallets[utxoAsset.ID] = btcWallet
  5968  
  5969  	ethWallet, tEthWallet := newTAccountLocker(acctAsset.ID)
  5970  	tCore.wallets[acctAsset.ID] = ethWallet
  5971  
  5972  	// Create an order
  5973  	qty, lo, dbOrder, match, changeCoin := generateMatch(rig, utxoAsset.ID, acctAsset.ID)
  5974  	redemptionReserves, refundReserves := dbOrder.MetaData.RedemptionReserves, dbOrder.MetaData.RefundReserves
  5975  
  5976  	tBtcWallet.fundingCoins = asset.Coins{changeCoin}
  5977  	tEthWallet.fundingCoins = asset.Coins{changeCoin}
  5978  
  5979  	oid := lo.ID()
  5980  
  5981  	tracker := &trackedTrade{
  5982  		Order:    lo,
  5983  		dc:       rig.dc,
  5984  		metaData: dbOrder.MetaData,
  5985  		matches: map[order.MatchID]*matchTracker{
  5986  			match.MatchID: {
  5987  				MetaMatch: db.MetaMatch{
  5988  					MetaData: &db.MatchMetaData{},
  5989  					UserMatch: &order.UserMatch{
  5990  						OrderID:  lo.ID(),
  5991  						MatchID:  match.MatchID,
  5992  						Status:   order.NewlyMatched,
  5993  						Side:     order.Maker,
  5994  						Quantity: match.Quantity,
  5995  					},
  5996  				},
  5997  				prefix: lo.Prefix(),
  5998  				trade:  lo.Trade(),
  5999  				// counterConfirms: -1,
  6000  			},
  6001  		},
  6002  		coins:              map[string]asset.Coin{"changecoinid": changeCoin},
  6003  		redemptionReserves: redemptionReserves,
  6004  		refundReserves:     refundReserves,
  6005  	}
  6006  
  6007  	rig.dc.trades = map[order.OrderID]*trackedTrade{
  6008  		oid: tracker,
  6009  	}
  6010  
  6011  	// reset
  6012  	reset := func() {
  6013  		tEthWallet.reservedRedemption = 0
  6014  		tEthWallet.reservedRefund = 0
  6015  		tracker.redemptionLocked = 0
  6016  		tracker.refundLocked = 0
  6017  	}
  6018  
  6019  	run := func(tag string) {
  6020  		t.Helper()
  6021  		description := fmt.Sprintf("%s: side = %s, order status = %s, match status = %s",
  6022  			tag, match.Side, dbOrder.MetaData.Status, match.Status)
  6023  
  6024  		tCore.reReserveFunding(btcWallet)
  6025  		tCore.reReserveFunding(ethWallet)
  6026  
  6027  		if lo.T.Sell && ((match.Side == order.Taker && match.Status < order.MatchComplete) ||
  6028  			(match.Side == order.Taker && match.Status < order.MakerRedeemed)) {
  6029  			var reReserveQty uint64 = redemptionReserves
  6030  			if dbOrder.MetaData.Status > order.OrderStatusBooked {
  6031  				reReserveQty = applyFraction(match.Quantity, qty, redemptionReserves)
  6032  			}
  6033  
  6034  			if tEthWallet.reservedRedemption != reReserveQty {
  6035  				t.Fatalf("%s: redemption funds not reserved, %d != %d", description, tEthWallet.reservedRedemption, reReserveQty)
  6036  			}
  6037  		}
  6038  
  6039  		if !lo.T.Sell && match.Status < order.MakerRedeemed {
  6040  			var reRefundQty uint64 = refundReserves
  6041  			if dbOrder.MetaData.Status > order.OrderStatusBooked {
  6042  				reRefundQty = applyFraction(match.Quantity, qty, refundReserves)
  6043  			}
  6044  
  6045  			if tEthWallet.reservedRefund != reRefundQty {
  6046  				t.Fatalf("%s: refund funds not reserved, %d != %d", description, tEthWallet.reservedRefund, reRefundQty)
  6047  			}
  6048  		}
  6049  
  6050  		reset()
  6051  	}
  6052  
  6053  	for _, tt := range reservationTests {
  6054  
  6055  		lo.T.Sell = tt.sell
  6056  		tracker.wallets, _, _, _ = tCore.walletSet(rig.dc, utxoAsset.ID, acctAsset.ID, tt.sell)
  6057  		if tt.sell {
  6058  			dbOrder.MetaData.RefundReserves = 0
  6059  			dbOrder.MetaData.RedemptionReserves = redemptionReserves
  6060  		} else {
  6061  			dbOrder.MetaData.RefundReserves = refundReserves
  6062  			dbOrder.MetaData.RedemptionReserves = 0
  6063  		}
  6064  		for _, side := range tt.side {
  6065  			match.Side = side
  6066  			for _, orderStatus := range tt.orderStatuses {
  6067  				dbOrder.MetaData.Status = orderStatus
  6068  				for _, matchStatus := range tt.matchStatuses {
  6069  					match.Status = matchStatus
  6070  					run(tt.name)
  6071  				}
  6072  			}
  6073  		}
  6074  	}
  6075  
  6076  }
  6077  
  6078  func TestCompareServerMatches(t *testing.T) {
  6079  	rig := newTestRig()
  6080  	defer rig.shutdown()
  6081  	preImg := newPreimage()
  6082  	dc := rig.dc
  6083  
  6084  	notes := make(map[string][]Notification)
  6085  	notify := func(note Notification) {
  6086  		notes[note.Type()] = append(notes[note.Type()], note)
  6087  	}
  6088  
  6089  	lo := &order.LimitOrder{
  6090  		P: order.Prefix{
  6091  			// 	OrderType:  order.LimitOrderType,
  6092  			// 	BaseAsset:  tUTXOAssetA.ID,
  6093  			// 	QuoteAsset: tUTXOAssetB.ID,
  6094  			// 	ClientTime: time.Now(),
  6095  			ServerTime: time.Now(),
  6096  			// 	Commit:     preImg.Commit(),
  6097  		},
  6098  	}
  6099  	oid := lo.ID()
  6100  	dbOrder := &db.MetaOrder{
  6101  		MetaData: &db.OrderMetaData{},
  6102  		Order:    lo,
  6103  	}
  6104  	tracker := newTrackedTrade(dbOrder, preImg, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker,
  6105  		rig.db, rig.queue, nil, nil, rig.core.notify, rig.core.formatDetails)
  6106  
  6107  	// Known trade, and known match
  6108  	knownID := ordertest.RandomMatchID()
  6109  	knownMatch := &matchTracker{
  6110  		MetaMatch: db.MetaMatch{
  6111  			UserMatch: &order.UserMatch{MatchID: knownID},
  6112  			MetaData:  &db.MatchMetaData{},
  6113  		},
  6114  		counterConfirms: -1,
  6115  	}
  6116  	tracker.matches[knownID] = knownMatch
  6117  	knownMsgMatch := &msgjson.Match{OrderID: oid[:], MatchID: knownID[:]}
  6118  
  6119  	// Known trade, but missing match
  6120  	missingID := ordertest.RandomMatchID()
  6121  	missingMatch := &matchTracker{
  6122  		MetaMatch: db.MetaMatch{
  6123  			UserMatch: &order.UserMatch{MatchID: missingID},
  6124  			MetaData:  &db.MatchMetaData{},
  6125  		},
  6126  	}
  6127  	tracker.matches[missingID] = missingMatch
  6128  
  6129  	// extra match
  6130  	extraID := ordertest.RandomMatchID()
  6131  	extraMsgMatch := &msgjson.Match{OrderID: oid[:], MatchID: extraID[:]}
  6132  
  6133  	// Entirely missing order
  6134  	loMissing, dbOrderMissing, preImgMissing, _ := makeLimitOrder(dc, true, 3*dcrBtcLotSize, dcrBtcRateStep*10)
  6135  	trackerMissing := newTrackedTrade(dbOrderMissing, preImgMissing, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker,
  6136  		rig.db, rig.queue, nil, nil, notify, rig.core.formatDetails)
  6137  	oidMissing := loMissing.ID()
  6138  	// an active match for the missing trade
  6139  	matchIDMissing := ordertest.RandomMatchID()
  6140  	missingTradeMatch := &matchTracker{
  6141  		MetaMatch: db.MetaMatch{
  6142  			UserMatch: &order.UserMatch{MatchID: matchIDMissing},
  6143  			MetaData:  &db.MatchMetaData{},
  6144  		},
  6145  		counterConfirms: -1,
  6146  	}
  6147  	trackerMissing.matches[knownID] = missingTradeMatch
  6148  	// an inactive match for the missing trade
  6149  	matchIDMissingInactive := ordertest.RandomMatchID()
  6150  	missingTradeMatchInactive := &matchTracker{
  6151  		MetaMatch: db.MetaMatch{
  6152  			UserMatch: &order.UserMatch{
  6153  				MatchID: matchIDMissingInactive,
  6154  				Status:  order.MatchComplete,
  6155  			},
  6156  			MetaData: &db.MatchMetaData{
  6157  				Proof: db.MatchProof{
  6158  					Auth: db.MatchAuth{
  6159  						RedeemSig: []byte{1, 2, 3}, // won't be considered complete with out it
  6160  					},
  6161  				},
  6162  			},
  6163  		},
  6164  		counterConfirms: 1,
  6165  	}
  6166  	trackerMissing.matches[matchIDMissingInactive] = missingTradeMatchInactive
  6167  
  6168  	srvMatches := map[order.OrderID]*serverMatches{
  6169  		oid: {
  6170  			tracker:    tracker,
  6171  			msgMatches: []*msgjson.Match{knownMsgMatch, extraMsgMatch},
  6172  		},
  6173  		// oidMissing not included (missing!)
  6174  	}
  6175  
  6176  	dc.trades = map[order.OrderID]*trackedTrade{
  6177  		oid:        tracker,
  6178  		oidMissing: trackerMissing,
  6179  	}
  6180  
  6181  	exceptions, _ := dc.compareServerMatches(srvMatches)
  6182  	if len(exceptions) != 2 {
  6183  		t.Fatalf("exceptions did not include both trades, just %d", len(exceptions))
  6184  	}
  6185  
  6186  	exc, ok := exceptions[oid]
  6187  	if !ok {
  6188  		t.Fatalf("exceptions did not include trade %v", oid)
  6189  	}
  6190  	if exc.trade.ID() != oid {
  6191  		t.Fatalf("wrong trade ID, got %v, want %v", exc.trade.ID(), oid)
  6192  	}
  6193  	if len(exc.missing) != 1 {
  6194  		t.Fatalf("found %d missing matches for trade %v, expected 1", len(exc.missing), oid)
  6195  	}
  6196  	if exc.missing[0].MatchID != missingID {
  6197  		t.Fatalf("wrong missing match, got %v, expected %v", exc.missing[0].MatchID, missingID)
  6198  	}
  6199  	if len(exc.extra) != 1 {
  6200  		t.Fatalf("found %d extra matches for trade %v, expected 1", len(exc.extra), oid)
  6201  	}
  6202  	if !bytes.Equal(exc.extra[0].MatchID, extraID[:]) {
  6203  		t.Fatalf("wrong extra match, got %v, expected %v", exc.extra[0].MatchID, extraID)
  6204  	}
  6205  
  6206  	exc, ok = exceptions[oidMissing]
  6207  	if !ok {
  6208  		t.Fatalf("exceptions did not include trade %v", oidMissing)
  6209  	}
  6210  	if exc.trade.ID() != oidMissing {
  6211  		t.Fatalf("wrong trade ID, got %v, want %v", exc.trade.ID(), oidMissing)
  6212  	}
  6213  	if len(exc.missing) != 1 { // no matchIDMissingInactive
  6214  		t.Fatalf("found %d missing matches for trade %v, expected 1", len(exc.missing), oid)
  6215  	}
  6216  	if exc.missing[0].MatchID != matchIDMissing {
  6217  		t.Fatalf("wrong missing match, got %v, expected %v", exc.missing[0].MatchID, matchIDMissing)
  6218  	}
  6219  	if len(exc.extra) != 0 {
  6220  		t.Fatalf("found %d extra matches for trade %v, expected 0", len(exc.extra), oid)
  6221  	}
  6222  }
  6223  
  6224  func convertMsgLimitOrder(msgOrder *msgjson.LimitOrder) *order.LimitOrder {
  6225  	tif := order.ImmediateTiF
  6226  	if msgOrder.TiF == msgjson.StandingOrderNum {
  6227  		tif = order.StandingTiF
  6228  	}
  6229  	return &order.LimitOrder{
  6230  		P:     convertMsgPrefix(&msgOrder.Prefix, order.LimitOrderType),
  6231  		T:     convertMsgTrade(&msgOrder.Trade),
  6232  		Rate:  msgOrder.Rate,
  6233  		Force: tif,
  6234  	}
  6235  }
  6236  
  6237  func convertMsgMarketOrder(msgOrder *msgjson.MarketOrder) *order.MarketOrder {
  6238  	return &order.MarketOrder{
  6239  		P: convertMsgPrefix(&msgOrder.Prefix, order.MarketOrderType),
  6240  		T: convertMsgTrade(&msgOrder.Trade),
  6241  	}
  6242  }
  6243  
  6244  func convertMsgCancelOrder(msgOrder *msgjson.CancelOrder) *order.CancelOrder {
  6245  	var oid order.OrderID
  6246  	copy(oid[:], msgOrder.TargetID)
  6247  	return &order.CancelOrder{
  6248  		P:             convertMsgPrefix(&msgOrder.Prefix, order.CancelOrderType),
  6249  		TargetOrderID: oid,
  6250  	}
  6251  }
  6252  
  6253  func convertMsgPrefix(msgPrefix *msgjson.Prefix, oType order.OrderType) order.Prefix {
  6254  	var commit order.Commitment
  6255  	copy(commit[:], msgPrefix.Commit)
  6256  	var acctID account.AccountID
  6257  	copy(acctID[:], msgPrefix.AccountID)
  6258  	return order.Prefix{
  6259  		AccountID:  acctID,
  6260  		BaseAsset:  msgPrefix.Base,
  6261  		QuoteAsset: msgPrefix.Quote,
  6262  		OrderType:  oType,
  6263  		ClientTime: time.UnixMilli(int64(msgPrefix.ClientTime)),
  6264  		//ServerTime set in epoch queue processing pipeline.
  6265  		Commit: commit,
  6266  	}
  6267  }
  6268  
  6269  func convertMsgTrade(msgTrade *msgjson.Trade) order.Trade {
  6270  	coins := make([]order.CoinID, 0, len(msgTrade.Coins))
  6271  	for _, coin := range msgTrade.Coins {
  6272  		var b []byte = coin.ID
  6273  		coins = append(coins, b)
  6274  	}
  6275  	sell := true
  6276  	if msgTrade.Side == msgjson.BuyOrderNum {
  6277  		sell = false
  6278  	}
  6279  	return order.Trade{
  6280  		Coins:    coins,
  6281  		Sell:     sell,
  6282  		Quantity: msgTrade.Quantity,
  6283  		Address:  msgTrade.Address,
  6284  	}
  6285  }
  6286  
  6287  func orderResponse(msgID uint64, msgPrefix msgjson.Stampable, ord order.Order, badSig, noID, badID bool) *msgjson.Message {
  6288  	orderTime := time.Now()
  6289  	timeStamp := uint64(orderTime.UnixMilli())
  6290  	msgPrefix.Stamp(timeStamp)
  6291  	sign(tDexPriv, msgPrefix)
  6292  	if badSig {
  6293  		msgPrefix.SetSig(encode.RandomBytes(5))
  6294  	}
  6295  	ord.SetTime(orderTime)
  6296  	oid := ord.ID()
  6297  	oidB := oid[:]
  6298  	if noID {
  6299  		oidB = nil
  6300  	} else if badID {
  6301  		oidB = encode.RandomBytes(32)
  6302  	}
  6303  	resp, _ := msgjson.NewResponse(msgID, &msgjson.OrderResult{
  6304  		Sig:        msgPrefix.SigBytes(),
  6305  		OrderID:    oidB,
  6306  		ServerTime: timeStamp,
  6307  	}, nil)
  6308  	return resp
  6309  }
  6310  
  6311  func tMsgAudit(oid order.OrderID, mid order.MatchID, recipient string, val uint64, secretHash []byte) (*msgjson.Audit, *asset.AuditInfo) {
  6312  	auditID := encode.RandomBytes(36)
  6313  	auditContract := encode.RandomBytes(75)
  6314  	if secretHash == nil {
  6315  		secretHash = encode.RandomBytes(32)
  6316  	}
  6317  	auditStamp := uint64(time.Now().UnixMilli())
  6318  	audit := &msgjson.Audit{
  6319  		OrderID:  oid[:],
  6320  		MatchID:  mid[:],
  6321  		Time:     auditStamp,
  6322  		CoinID:   auditID,
  6323  		Contract: auditContract,
  6324  	}
  6325  	sign(tDexPriv, audit)
  6326  	auditCoin := &tCoin{id: auditID, val: val}
  6327  	auditInfo := &asset.AuditInfo{
  6328  		Recipient:  recipient,
  6329  		Coin:       auditCoin,
  6330  		Contract:   auditContract,
  6331  		SecretHash: secretHash,
  6332  	}
  6333  	return audit, auditInfo
  6334  }
  6335  
  6336  func TestHandleEpochOrderMsg(t *testing.T) {
  6337  	rig := newTestRig()
  6338  	defer rig.shutdown()
  6339  	ord := &order.LimitOrder{P: order.Prefix{ServerTime: time.Now()}}
  6340  	oid := ord.ID()
  6341  	payload := &msgjson.EpochOrderNote{
  6342  		BookOrderNote: msgjson.BookOrderNote{
  6343  			OrderNote: msgjson.OrderNote{
  6344  				MarketID: tDcrBtcMktName,
  6345  				OrderID:  oid.Bytes(),
  6346  			},
  6347  			TradeNote: msgjson.TradeNote{
  6348  				Side:     msgjson.BuyOrderNum,
  6349  				Rate:     4,
  6350  				Quantity: 10,
  6351  			},
  6352  		},
  6353  		Epoch: 1,
  6354  	}
  6355  
  6356  	req, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.EpochOrderRoute, payload)
  6357  
  6358  	// Ensure handling an epoch order associated with a non-existent orderbook
  6359  	// generates an error.
  6360  	err := handleEpochOrderMsg(rig.core, rig.dc, req)
  6361  	if err == nil {
  6362  		t.Fatal("[handleEpochOrderMsg] expected a non-existent orderbook error")
  6363  	}
  6364  
  6365  	rig.dc.books[tDcrBtcMktName] = newBookie(rig.dc, tUTXOAssetA.ID, tUTXOAssetB.ID, nil, tLogger)
  6366  
  6367  	err = handleEpochOrderMsg(rig.core, rig.dc, req)
  6368  	if err != nil {
  6369  		t.Fatalf("[handleEpochOrderMsg] unexpected error: %v", err)
  6370  	}
  6371  }
  6372  
  6373  func makeMatchProof(preimages []order.Preimage, commitments []order.Commitment) (msgjson.Bytes, msgjson.Bytes, error) {
  6374  	if len(preimages) != len(commitments) {
  6375  		return nil, nil, fmt.Errorf("expected equal number of preimages and commitments")
  6376  	}
  6377  
  6378  	sbuff := make([]byte, 0, len(preimages)*order.PreimageSize)
  6379  	cbuff := make([]byte, 0, len(commitments)*order.CommitmentSize)
  6380  	for i := 0; i < len(preimages); i++ {
  6381  		sbuff = append(sbuff, preimages[i][:]...)
  6382  		cbuff = append(cbuff, commitments[i][:]...)
  6383  	}
  6384  	seed := blake256.Sum256(sbuff)
  6385  	csum := blake256.Sum256(cbuff)
  6386  	return seed[:], csum[:], nil
  6387  }
  6388  
  6389  func TestHandleMatchProofMsg(t *testing.T) {
  6390  	rig := newTestRig()
  6391  	defer rig.shutdown()
  6392  	pimg := newPreimage()
  6393  	cmt := pimg.Commit()
  6394  
  6395  	seed, csum, err := makeMatchProof([]order.Preimage{pimg}, []order.Commitment{cmt})
  6396  	if err != nil {
  6397  		t.Fatalf("[makeMatchProof] unexpected error: %v", err)
  6398  	}
  6399  
  6400  	payload := &msgjson.MatchProofNote{
  6401  		MarketID:  tDcrBtcMktName,
  6402  		Epoch:     1,
  6403  		Preimages: []dex.Bytes{pimg[:]},
  6404  		CSum:      csum[:],
  6405  		Seed:      seed[:],
  6406  	}
  6407  
  6408  	eo := &msgjson.EpochOrderNote{
  6409  		BookOrderNote: msgjson.BookOrderNote{
  6410  			OrderNote: msgjson.OrderNote{
  6411  				MarketID: tDcrBtcMktName,
  6412  				OrderID:  encode.RandomBytes(order.OrderIDSize),
  6413  			},
  6414  		},
  6415  		Epoch:  1,
  6416  		Commit: cmt[:],
  6417  	}
  6418  
  6419  	req, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.MatchProofRoute, payload)
  6420  
  6421  	// Ensure match proof validation generates an error for a non-existent orderbook.
  6422  	err = handleMatchProofMsg(rig.core, rig.dc, req)
  6423  	if err == nil {
  6424  		t.Fatal("[handleMatchProofMsg] expected a non-existent orderbook error")
  6425  	}
  6426  
  6427  	rig.dc.books[tDcrBtcMktName] = newBookie(rig.dc, tUTXOAssetA.ID, tUTXOAssetB.ID, nil, tLogger)
  6428  
  6429  	err = rig.dc.books[tDcrBtcMktName].Enqueue(eo)
  6430  	if err != nil {
  6431  		t.Fatalf("[Enqueue] unexpected error: %v", err)
  6432  	}
  6433  
  6434  	err = handleMatchProofMsg(rig.core, rig.dc, req)
  6435  	if err != nil {
  6436  		t.Fatalf("[handleMatchProofMsg] unexpected error: %v", err)
  6437  	}
  6438  }
  6439  
  6440  func Test_marketTrades(t *testing.T) {
  6441  	mktID := "dcr_btc"
  6442  	dc := &dexConnection{
  6443  		trades: make(map[order.OrderID]*trackedTrade),
  6444  	}
  6445  
  6446  	preImg := newPreimage()
  6447  	activeOrd := &order.LimitOrder{P: order.Prefix{
  6448  		ServerTime: time.Now(),
  6449  		Commit:     preImg.Commit(),
  6450  	}}
  6451  	activeTracker := &trackedTrade{
  6452  		Order:  activeOrd,
  6453  		preImg: preImg,
  6454  		mktID:  mktID,
  6455  		dc:     dc,
  6456  		metaData: &db.OrderMetaData{
  6457  			Status: order.OrderStatusBooked,
  6458  		},
  6459  		matches: make(map[order.MatchID]*matchTracker),
  6460  	}
  6461  
  6462  	dc.trades[activeTracker.ID()] = activeTracker
  6463  
  6464  	preImg = newPreimage() // different oid
  6465  	inactiveOrd := &order.LimitOrder{P: order.Prefix{
  6466  		ServerTime: time.Now(),
  6467  		Commit:     preImg.Commit(),
  6468  	}}
  6469  	inactiveTracker := &trackedTrade{
  6470  		Order:  inactiveOrd,
  6471  		preImg: preImg,
  6472  		mktID:  mktID,
  6473  		dc:     dc,
  6474  		metaData: &db.OrderMetaData{
  6475  			Status: order.OrderStatusExecuted,
  6476  		},
  6477  		matches: make(map[order.MatchID]*matchTracker), // no matches
  6478  	}
  6479  
  6480  	dc.trades[inactiveTracker.ID()] = inactiveTracker
  6481  
  6482  	trades, _ := dc.marketTrades(mktID)
  6483  	if len(trades) != 1 {
  6484  		t.Fatalf("Expected only one trade from marketTrades, found %v", len(trades))
  6485  	}
  6486  	if trades[0].ID() != activeOrd.ID() {
  6487  		t.Errorf("Expected active order ID %v, got %v", activeOrd.ID(), trades[0].ID())
  6488  	}
  6489  }
  6490  
  6491  func TestLogout(t *testing.T) {
  6492  	rig := newTestRig()
  6493  	defer rig.shutdown()
  6494  	tCore := rig.core
  6495  
  6496  	dcrWallet, _ := newTWallet(tUTXOAssetA.ID)
  6497  	tCore.wallets[tUTXOAssetA.ID] = dcrWallet
  6498  
  6499  	btcWallet, _ := newTWallet(tUTXOAssetB.ID)
  6500  	tCore.wallets[tUTXOAssetB.ID] = btcWallet
  6501  
  6502  	ord := &order.LimitOrder{P: order.Prefix{ServerTime: time.Now()}}
  6503  	tracker := &trackedTrade{
  6504  		Order:  ord,
  6505  		preImg: newPreimage(),
  6506  		dc:     rig.dc,
  6507  		metaData: &db.OrderMetaData{
  6508  			Status: order.OrderStatusBooked,
  6509  		},
  6510  		matches: make(map[order.MatchID]*matchTracker),
  6511  	}
  6512  	rig.dc.trades[ord.ID()] = tracker
  6513  
  6514  	ensureErr := func(tag string) {
  6515  		t.Helper()
  6516  
  6517  		tCore.loginMtx.Lock()
  6518  		tCore.loggedIn = true
  6519  		tCore.loginMtx.Unlock()
  6520  
  6521  		err := tCore.Logout()
  6522  		if err == nil {
  6523  			t.Fatalf("%s: no error", tag)
  6524  		}
  6525  	}
  6526  
  6527  	// Active orders error.
  6528  	ensureErr("active orders")
  6529  
  6530  	tracker.metaData = &db.OrderMetaData{
  6531  		Status: order.OrderStatusExecuted,
  6532  	}
  6533  	mid := ordertest.RandomMatchID()
  6534  	tracker.matches[mid] = &matchTracker{
  6535  		MetaMatch: db.MetaMatch{
  6536  			MetaData: &db.MatchMetaData{},
  6537  			UserMatch: &order.UserMatch{
  6538  				OrderID: ord.ID(),
  6539  				MatchID: mid,
  6540  				Status:  order.NewlyMatched,
  6541  				Side:    order.Maker,
  6542  			},
  6543  		},
  6544  		prefix:          ord.Prefix(),
  6545  		trade:           ord.Trade(),
  6546  		counterConfirms: -1,
  6547  	}
  6548  	// Active orders with matches error.
  6549  	ensureErr("active orders matches")
  6550  	rig.dc.trades = nil
  6551  }
  6552  
  6553  func TestSetEpoch(t *testing.T) {
  6554  	rig := newTestRig()
  6555  	defer rig.shutdown()
  6556  	dc := rig.dc
  6557  	dc.books[tDcrBtcMktName] = newBookie(rig.dc, tUTXOAssetA.ID, tUTXOAssetB.ID, nil, tLogger)
  6558  
  6559  	mktEpoch := func() uint64 {
  6560  		dc.epochMtx.RLock()
  6561  		defer dc.epochMtx.RUnlock()
  6562  		return dc.epoch[tDcrBtcMktName]
  6563  	}
  6564  
  6565  	payload := &msgjson.MatchProofNote{
  6566  		MarketID: tDcrBtcMktName,
  6567  		Epoch:    1,
  6568  	}
  6569  	req, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.MatchProofRoute, payload)
  6570  	err := handleMatchProofMsg(rig.core, rig.dc, req)
  6571  	if err != nil {
  6572  		t.Fatalf("error advancing epoch: %v", err)
  6573  	}
  6574  	if mktEpoch() != 2 {
  6575  		t.Fatalf("expected epoch 2, got %d", mktEpoch())
  6576  	}
  6577  
  6578  	payload.Epoch = 0
  6579  	req, _ = msgjson.NewRequest(rig.dc.NextID(), msgjson.MatchProofRoute, payload)
  6580  	err = handleMatchProofMsg(rig.core, rig.dc, req)
  6581  	if err != nil {
  6582  		t.Fatalf("error handling match proof: %v", err)
  6583  	}
  6584  	if mktEpoch() != 2 {
  6585  		t.Fatalf("epoch changed, expected epoch 2, got %d", mktEpoch())
  6586  	}
  6587  }
  6588  
  6589  func makeLimitOrder(dc *dexConnection, sell bool, qty, rate uint64) (*order.LimitOrder, *db.MetaOrder, order.Preimage, string) {
  6590  	preImg := newPreimage()
  6591  	addr := ordertest.RandomAddress()
  6592  	lo := &order.LimitOrder{
  6593  		P: order.Prefix{
  6594  			AccountID:  dc.acct.ID(),
  6595  			BaseAsset:  tUTXOAssetA.ID,
  6596  			QuoteAsset: tUTXOAssetB.ID,
  6597  			OrderType:  order.LimitOrderType,
  6598  			ClientTime: time.Now(),
  6599  			ServerTime: time.Now().Add(time.Millisecond),
  6600  			Commit:     preImg.Commit(),
  6601  		},
  6602  		T: order.Trade{
  6603  			// Coins needed?
  6604  			Sell:     sell,
  6605  			Quantity: qty,
  6606  			Address:  addr,
  6607  		},
  6608  		Rate:  rate,
  6609  		Force: order.ImmediateTiF,
  6610  	}
  6611  	fromAsset, toAsset := tUTXOAssetB, tUTXOAssetA
  6612  	if sell {
  6613  		fromAsset, toAsset = tUTXOAssetA, tUTXOAssetB
  6614  	}
  6615  	dbOrder := &db.MetaOrder{
  6616  		MetaData: &db.OrderMetaData{
  6617  			Status: order.OrderStatusEpoch,
  6618  			Host:   dc.acct.host,
  6619  			Proof: db.OrderProof{
  6620  				Preimage: preImg[:],
  6621  			},
  6622  			MaxFeeRate:   tMaxFeeRate,
  6623  			EpochDur:     dc.marketEpochDuration(tDcrBtcMktName),
  6624  			FromSwapConf: fromAsset.SwapConf,
  6625  			ToSwapConf:   toAsset.SwapConf,
  6626  		},
  6627  		Order: lo,
  6628  	}
  6629  	return lo, dbOrder, preImg, addr
  6630  }
  6631  
  6632  func TestAddrHost(t *testing.T) {
  6633  	tests := []struct {
  6634  		name, addr, want string
  6635  		wantErr          bool
  6636  	}{{
  6637  		name: "scheme, host, and port",
  6638  		addr: "https://localhost:5758",
  6639  		want: "localhost:5758",
  6640  	}, {
  6641  		name: "scheme, ipv6 host, and port",
  6642  		addr: "https://[::1]:5758",
  6643  		want: "[::1]:5758",
  6644  	}, {
  6645  		name: "host and port",
  6646  		addr: "localhost:5758",
  6647  		want: "localhost:5758",
  6648  	}, {
  6649  		name: "just port",
  6650  		addr: ":5758",
  6651  		want: "localhost:5758",
  6652  	}, {
  6653  		name: "ip host and port",
  6654  		addr: "127.0.0.1:5758",
  6655  		want: "127.0.0.1:5758",
  6656  	}, {
  6657  		name: "just host",
  6658  		addr: "thatonedex.com",
  6659  		want: "thatonedex.com:7232",
  6660  	}, {
  6661  		name: "scheme and host",
  6662  		addr: "https://thatonedex.com",
  6663  		want: "thatonedex.com:7232",
  6664  	}, {
  6665  		name: "scheme, host, and path",
  6666  		addr: "https://thatonedex.com/any/path",
  6667  		want: "thatonedex.com:7232",
  6668  	}, {
  6669  		name: "ipv6 host",
  6670  		addr: "[1:2::]",
  6671  		want: "[1:2::]:7232",
  6672  	}, {
  6673  		name: "ipv6 host and port",
  6674  		addr: "[1:2::]:5758",
  6675  		want: "[1:2::]:5758",
  6676  	}, {
  6677  		name: "empty address",
  6678  		want: "localhost:7232",
  6679  	}, {
  6680  		name:    "invalid host",
  6681  		addr:    "https://\n:1234",
  6682  		wantErr: true,
  6683  	}, {
  6684  		name:    "invalid port",
  6685  		addr:    ":asdf",
  6686  		wantErr: true,
  6687  	}}
  6688  	for _, test := range tests {
  6689  		res, err := addrHost(test.addr)
  6690  		if res != test.want {
  6691  			t.Fatalf("wanted %s but got %s for test '%s'", test.want, res, test.name)
  6692  		}
  6693  		if test.wantErr {
  6694  			if err == nil {
  6695  				t.Fatalf("wanted error for test %s, but got none", test.name)
  6696  			}
  6697  			continue
  6698  		} else if err != nil {
  6699  			t.Fatalf("addrHost error for test %s: %v", test.name, err)
  6700  		}
  6701  		// Parsing results a second time should produce the same results.
  6702  		res, _ = addrHost(res)
  6703  		if res != test.want {
  6704  			t.Fatalf("wanted %s but got %s for test '%s'", test.want, res, test.name)
  6705  		}
  6706  	}
  6707  }
  6708  
  6709  func TestAssetBalance(t *testing.T) {
  6710  	rig := newTestRig()
  6711  	defer rig.shutdown()
  6712  	tCore := rig.core
  6713  
  6714  	wallet, tWallet := newTWallet(tUTXOAssetA.ID)
  6715  	tCore.wallets[tUTXOAssetA.ID] = wallet
  6716  	bal := &asset.Balance{
  6717  		Available: 4e7,
  6718  		Immature:  6e7,
  6719  		Locked:    2e8,
  6720  	}
  6721  	tWallet.bal = bal
  6722  	walletBal, err := tCore.AssetBalance(tUTXOAssetA.ID)
  6723  	if err != nil {
  6724  		t.Fatalf("error retrieving asset balance: %v", err)
  6725  	}
  6726  	dbtest.MustCompareAssetBalances(t, "zero-conf", bal, &walletBal.Balance.Balance)
  6727  	if walletBal.ContractLocked != 0 {
  6728  		t.Fatalf("contractlocked balance %d > expected value 0", walletBal.ContractLocked)
  6729  	}
  6730  }
  6731  
  6732  func TestAssetCounter(t *testing.T) {
  6733  	assets := make(assetMap)
  6734  	assets.count(1)
  6735  	if len(assets) != 1 {
  6736  		t.Fatalf("count not added")
  6737  	}
  6738  
  6739  	newCounts := assetMap{
  6740  		1: struct{}{},
  6741  		2: struct{}{},
  6742  	}
  6743  	assets.merge(newCounts)
  6744  	if len(assets) != 2 {
  6745  		t.Fatalf("counts not absorbed properly")
  6746  	}
  6747  }
  6748  
  6749  func TestHandleTradeSuspensionMsg(t *testing.T) {
  6750  	rig := newTestRig()
  6751  	defer rig.shutdown()
  6752  
  6753  	tCore := rig.core
  6754  	dc := rig.dc
  6755  	dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID)
  6756  	tCore.wallets[tUTXOAssetA.ID] = dcrWallet
  6757  	dcrWallet.Unlock(rig.crypter)
  6758  
  6759  	btcWallet, _ := newTWallet(tUTXOAssetB.ID)
  6760  	tCore.wallets[tUTXOAssetB.ID] = btcWallet
  6761  	btcWallet.Unlock(rig.crypter)
  6762  
  6763  	walletSet, _, _, _ := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, true)
  6764  
  6765  	rig.dc.books[tDcrBtcMktName] = newBookie(rig.dc, tUTXOAssetA.ID, tUTXOAssetB.ID, nil, tLogger)
  6766  
  6767  	addTracker := func(coins asset.Coins) *trackedTrade {
  6768  		lo, dbOrder, preImg, _ := makeLimitOrder(dc, true, 0, 0)
  6769  		oid := lo.ID()
  6770  		tracker := newTrackedTrade(dbOrder, preImg, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker,
  6771  			rig.db, rig.queue, walletSet, coins, rig.core.notify, rig.core.formatDetails)
  6772  		dc.trades[oid] = tracker
  6773  		return tracker
  6774  	}
  6775  
  6776  	// Make a trade that has a single funding coin, no change coin, and no
  6777  	// active matches.
  6778  	fundCoinDcrID := encode.RandomBytes(36)
  6779  	freshTracker := addTracker(asset.Coins{&tCoin{id: fundCoinDcrID}})
  6780  	freshTracker.metaData.Status = order.OrderStatusBooked // suspend with purge only purges book orders since epoch orders are always processed first
  6781  
  6782  	// Ensure a non-existent market cannot be suspended.
  6783  	payload := &msgjson.TradeSuspension{
  6784  		MarketID: "dcr_dcr",
  6785  	}
  6786  
  6787  	req, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.SuspensionRoute, payload)
  6788  	err := handleTradeSuspensionMsg(rig.core, rig.dc, req)
  6789  	if err == nil {
  6790  		t.Fatal("[handleTradeSuspensionMsg] expected a market ID not found error")
  6791  	}
  6792  
  6793  	newPayload := func() *msgjson.TradeSuspension {
  6794  		return &msgjson.TradeSuspension{
  6795  			MarketID:    tDcrBtcMktName,
  6796  			FinalEpoch:  100,
  6797  			SuspendTime: uint64(time.Now().Add(time.Millisecond * 20).UnixMilli()),
  6798  			Persist:     false, // Make sure the coins are returned.
  6799  		}
  6800  	}
  6801  
  6802  	// Suspend a running market.
  6803  	rig.dc.cfgMtx.Lock()
  6804  	mktConf := rig.dc.findMarketConfig(tDcrBtcMktName)
  6805  	mktConf.StartEpoch = 12
  6806  	rig.dc.cfgMtx.Unlock()
  6807  
  6808  	payload = newPayload()
  6809  	payload.SuspendTime = 0 // now
  6810  
  6811  	orderNotes, feedDone := orderNoteFeed(tCore)
  6812  	defer feedDone()
  6813  
  6814  	req, _ = msgjson.NewRequest(rig.dc.NextID(), msgjson.SuspensionRoute, payload)
  6815  	err = handleTradeSuspensionMsg(rig.core, rig.dc, req)
  6816  	if err != nil {
  6817  		t.Fatalf("[handleTradeSuspensionMsg] unexpected error: %v", err)
  6818  	}
  6819  
  6820  	verifyRevokeNotification(orderNotes, TopicOrderAutoRevoked, t)
  6821  
  6822  	// Check that the funding coin was returned. Use the tradeMtx for
  6823  	// synchronization.
  6824  	dc.tradeMtx.Lock()
  6825  	if len(tDcrWallet.returnedCoins) != 1 || !bytes.Equal(tDcrWallet.returnedCoins[0].ID(), fundCoinDcrID) {
  6826  		t.Fatalf("funding coin not returned")
  6827  	}
  6828  	dc.tradeMtx.Unlock()
  6829  
  6830  	// Make sure the change coin is returned for a trade with a change coin.
  6831  	delete(dc.trades, freshTracker.ID())
  6832  	swappedTracker := addTracker(nil)
  6833  	changeCoinID := encode.RandomBytes(36)
  6834  	swappedTracker.change = &tCoin{id: changeCoinID}
  6835  	swappedTracker.changeLocked = true
  6836  	swappedTracker.metaData.Status = order.OrderStatusBooked
  6837  	rig.dc.cfgMtx.Lock()
  6838  	mktConf.StartEpoch = 12 // make it appear running again first
  6839  	mktConf.FinalEpoch = 0
  6840  	mktConf.Persist = nil
  6841  	rig.dc.cfgMtx.Unlock()
  6842  	req, _ = msgjson.NewRequest(rig.dc.NextID(), msgjson.SuspensionRoute, payload)
  6843  	err = handleTradeSuspensionMsg(rig.core, rig.dc, req)
  6844  	if err != nil {
  6845  		t.Fatalf("[handleTradeSuspensionMsg] unexpected error: %v", err)
  6846  	}
  6847  	// Check that the funding coin was returned.
  6848  	dc.tradeMtx.Lock()
  6849  	if len(tDcrWallet.returnedCoins) != 1 || !bytes.Equal(tDcrWallet.returnedCoins[0].ID(), changeCoinID) {
  6850  		t.Fatalf("change coin not returned")
  6851  	}
  6852  	tDcrWallet.returnedCoins = nil
  6853  	dc.tradeMtx.Unlock()
  6854  
  6855  	// Make sure the coin isn't returned if there are unswapped matches.
  6856  	mid := ordertest.RandomMatchID()
  6857  	match := &matchTracker{
  6858  		MetaMatch: db.MetaMatch{
  6859  			UserMatch: &order.UserMatch{MatchID: mid}, // Default status = NewlyMatched
  6860  			MetaData:  &db.MatchMetaData{},
  6861  		},
  6862  		counterConfirms: -1,
  6863  	}
  6864  	swappedTracker.matches[mid] = match
  6865  	rig.dc.cfgMtx.Lock()
  6866  	mktConf.StartEpoch = 12 // make it appear running again first
  6867  	rig.dc.cfgMtx.Unlock()
  6868  	req, _ = msgjson.NewRequest(rig.dc.NextID(), msgjson.SuspensionRoute, payload)
  6869  	err = handleTradeSuspensionMsg(rig.core, rig.dc, req)
  6870  	if err != nil {
  6871  		t.Fatalf("[handleTradeSuspensionMsg] unexpected error: %v", err)
  6872  	}
  6873  	dc.tradeMtx.Lock()
  6874  	if tDcrWallet.returnedCoins != nil {
  6875  		t.Fatalf("change coin returned with active matches")
  6876  	}
  6877  	dc.tradeMtx.Unlock()
  6878  
  6879  	// Ensure trades for a suspended market generate an error.
  6880  	form := &TradeForm{
  6881  		Host:    tDexHost,
  6882  		IsLimit: true,
  6883  		Sell:    true,
  6884  		Base:    tUTXOAssetA.ID,
  6885  		Quote:   tUTXOAssetB.ID,
  6886  		Qty:     dcrBtcLotSize * 10,
  6887  		Rate:    dcrBtcRateStep * 1000,
  6888  		TifNow:  false,
  6889  	}
  6890  
  6891  	_, err = rig.core.Trade(tPW, form)
  6892  	if err == nil {
  6893  		t.Fatalf("expected a suspension market error")
  6894  	}
  6895  }
  6896  
  6897  func orderNoteFeed(tCore *Core) (orderNotes chan *OrderNote, done func()) {
  6898  	orderNotes = make(chan *OrderNote, 16)
  6899  
  6900  	ntfnFeed := tCore.NotificationFeed()
  6901  	feedDone := make(chan struct{})
  6902  	var wg sync.WaitGroup
  6903  	wg.Add(1)
  6904  	go func() {
  6905  		defer wg.Done()
  6906  		for {
  6907  			select {
  6908  			case n := <-ntfnFeed.C:
  6909  				if ordNote, ok := n.(*OrderNote); ok {
  6910  					orderNotes <- ordNote
  6911  				}
  6912  			case <-tCtx.Done():
  6913  				return
  6914  			case <-feedDone:
  6915  				return
  6916  			}
  6917  		}
  6918  	}()
  6919  
  6920  	done = func() {
  6921  		close(feedDone) // close first on return
  6922  		wg.Wait()
  6923  	}
  6924  	return orderNotes, done
  6925  }
  6926  
  6927  func verifyRevokeNotification(ch chan *OrderNote, expectedTopic Topic, t *testing.T) {
  6928  	t.Helper()
  6929  	select {
  6930  	case actualOrderNote := <-ch:
  6931  		if expectedTopic != actualOrderNote.TopicID {
  6932  			t.Fatalf("SubjectText mismatch. %s != %s", actualOrderNote.TopicID,
  6933  				expectedTopic)
  6934  		}
  6935  		return
  6936  	case <-tCtx.Done():
  6937  		return
  6938  	case <-time.After(time.Second):
  6939  		t.Fatal("timed out waiting for OrderNote notification")
  6940  		return
  6941  	}
  6942  }
  6943  
  6944  func TestHandleTradeResumptionMsg(t *testing.T) {
  6945  	rig := newTestRig()
  6946  	defer rig.shutdown()
  6947  
  6948  	tCore := rig.core
  6949  	dcrWallet, _ := newTWallet(tUTXOAssetA.ID)
  6950  	tCore.wallets[tUTXOAssetA.ID] = dcrWallet
  6951  	dcrWallet.Unlock(rig.crypter)
  6952  
  6953  	btcWallet, _ := newTWallet(tUTXOAssetB.ID)
  6954  	tCore.wallets[tUTXOAssetB.ID] = btcWallet
  6955  	btcWallet.Unlock(rig.crypter)
  6956  
  6957  	epochLen := rig.dc.marketConfig(tDcrBtcMktName).EpochLen
  6958  
  6959  	handleLimit := func(msg *msgjson.Message, f msgFunc) error {
  6960  		// Need to stamp and sign the message with the server's key.
  6961  		msgOrder := new(msgjson.LimitOrder)
  6962  		err := msg.Unmarshal(msgOrder)
  6963  		if err != nil {
  6964  			t.Fatalf("unmarshal error: %v", err)
  6965  		}
  6966  		lo := convertMsgLimitOrder(msgOrder)
  6967  		f(orderResponse(msg.ID, msgOrder, lo, false, false, false))
  6968  		return nil
  6969  	}
  6970  
  6971  	tradeForm := &TradeForm{
  6972  		Host:    tDexHost,
  6973  		IsLimit: true,
  6974  		Sell:    true,
  6975  		Base:    tUTXOAssetA.ID,
  6976  		Quote:   tUTXOAssetB.ID,
  6977  		Qty:     dcrBtcLotSize * 10,
  6978  		Rate:    dcrBtcRateStep * 1000,
  6979  		TifNow:  false,
  6980  	}
  6981  
  6982  	rig.ws.queueResponse(msgjson.LimitRoute, handleLimit)
  6983  
  6984  	// Ensure a non-existent market cannot be suspended.
  6985  	payload := &msgjson.TradeResumption{
  6986  		MarketID: "dcr_dcr",
  6987  	}
  6988  
  6989  	req, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.ResumptionRoute, payload)
  6990  	err := handleTradeResumptionMsg(rig.core, rig.dc, req)
  6991  	if err == nil {
  6992  		t.Fatal("[handleTradeResumptionMsg] expected a market ID not found error")
  6993  	}
  6994  
  6995  	var resumeTime uint64
  6996  	newPayload := func() *msgjson.TradeResumption {
  6997  		return &msgjson.TradeResumption{
  6998  			MarketID:   tDcrBtcMktName,
  6999  			ResumeTime: resumeTime, // set the time to test the scheduling notification case, zero it for immediate resume
  7000  			StartEpoch: resumeTime / epochLen,
  7001  		}
  7002  	}
  7003  
  7004  	// Notify of scheduled resume.
  7005  	rig.dc.cfgMtx.Lock()
  7006  	mktConf := rig.dc.findMarketConfig(tDcrBtcMktName)
  7007  	mktConf.StartEpoch = 12
  7008  	mktConf.FinalEpoch = mktConf.StartEpoch + 1 // long since closed
  7009  	rig.dc.cfgMtx.Unlock()
  7010  
  7011  	resumeTime = uint64(time.Now().Add(time.Hour).UnixMilli())
  7012  	payload = newPayload()
  7013  	req, _ = msgjson.NewRequest(rig.dc.NextID(), msgjson.ResumptionRoute, payload)
  7014  	err = handleTradeResumptionMsg(rig.core, rig.dc, req)
  7015  	if err != nil {
  7016  		t.Fatalf("[handleTradeResumptionMsg] unexpected error: %v", err)
  7017  	}
  7018  
  7019  	// Should be suspended still, no trades
  7020  	_, err = rig.core.Trade(tPW, tradeForm)
  7021  	if err == nil {
  7022  		t.Fatal("trade was accepted for suspended market")
  7023  	}
  7024  
  7025  	// Resume the market immediately.
  7026  	resumeTime = uint64(time.Now().UnixMilli())
  7027  	payload = newPayload()
  7028  	payload.ResumeTime = 0 // resume now, not scheduled
  7029  	req, _ = msgjson.NewRequest(rig.dc.NextID(), msgjson.ResumptionRoute, payload)
  7030  	err = handleTradeResumptionMsg(rig.core, rig.dc, req)
  7031  	if err != nil {
  7032  		t.Fatalf("[handleTradeResumptionMsg] unexpected error: %v", err)
  7033  	}
  7034  
  7035  	// Ensure trades for a resumed market are processed without error.
  7036  	_, err = rig.core.Trade(tPW, tradeForm)
  7037  	if err != nil {
  7038  		t.Fatalf("unexpected trade error %v", err)
  7039  	}
  7040  }
  7041  
  7042  func TestHandleNomatch(t *testing.T) {
  7043  	rig := newTestRig()
  7044  	defer rig.shutdown()
  7045  	tCore := rig.core
  7046  	dc := rig.dc
  7047  
  7048  	dcrWallet, _ := newTWallet(tUTXOAssetA.ID)
  7049  	tCore.wallets[tUTXOAssetA.ID] = dcrWallet
  7050  	btcWallet, tBtcWallet := newTWallet(tUTXOAssetB.ID)
  7051  	tCore.wallets[tUTXOAssetB.ID] = btcWallet
  7052  
  7053  	walletSet, _, _, err := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, true)
  7054  	if err != nil {
  7055  		t.Fatalf("walletSet error: %v", err)
  7056  	}
  7057  
  7058  	fundingCoins := asset.Coins{&tCoin{}}
  7059  
  7060  	// Four types of order to check
  7061  
  7062  	// 1. Immediate limit order
  7063  	loImmediate, dbOrder, preImgL, _ := makeLimitOrder(dc, true, dcrBtcLotSize*100, dcrBtcRateStep)
  7064  	loImmediate.Force = order.ImmediateTiF
  7065  	immediateOID := loImmediate.ID()
  7066  	immediateTracker := newTrackedTrade(dbOrder, preImgL, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker,
  7067  		rig.db, rig.queue, walletSet, fundingCoins, rig.core.notify, rig.core.formatDetails)
  7068  	dc.trades[immediateOID] = immediateTracker
  7069  
  7070  	// 2. Standing limit order
  7071  	loStanding, dbOrder, preImgL, _ := makeLimitOrder(dc, true, dcrBtcLotSize*100, dcrBtcRateStep)
  7072  	loStanding.Force = order.StandingTiF
  7073  	standingOID := loStanding.ID()
  7074  	standingTracker := newTrackedTrade(dbOrder, preImgL, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker,
  7075  		rig.db, rig.queue, walletSet, fundingCoins, rig.core.notify, rig.core.formatDetails)
  7076  	dc.trades[standingOID] = standingTracker
  7077  
  7078  	// 3. Cancel order.
  7079  	cancelOrder := &order.CancelOrder{
  7080  		P: order.Prefix{
  7081  			ServerTime: time.Now(),
  7082  		},
  7083  	}
  7084  	cancelOID := cancelOrder.ID()
  7085  	standingTracker.cancel = &trackedCancel{
  7086  		CancelOrder: *cancelOrder,
  7087  	}
  7088  	dc.registerCancelLink(cancelOID, standingOID)
  7089  
  7090  	// 4. Market order.
  7091  	loWillBeMarket, dbOrder, preImgL, _ := makeLimitOrder(dc, true, dcrBtcLotSize*100, dcrBtcRateStep)
  7092  	mktOrder := &order.MarketOrder{
  7093  		P: loWillBeMarket.P,
  7094  		T: *loWillBeMarket.Trade().Copy(),
  7095  	}
  7096  	dbOrder.Order = mktOrder
  7097  	marketOID := mktOrder.ID()
  7098  	marketTracker := newTrackedTrade(dbOrder, preImgL, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker,
  7099  		rig.db, rig.queue, walletSet, fundingCoins, rig.core.notify, rig.core.formatDetails)
  7100  	dc.trades[marketOID] = marketTracker
  7101  
  7102  	runNomatch := func(tag string, oid order.OrderID) {
  7103  		tracker, _ := dc.findOrder(oid)
  7104  		if tracker == nil {
  7105  			t.Fatalf("%s: order ID not found", tag)
  7106  		}
  7107  		payload := &msgjson.NoMatch{OrderID: oid[:]}
  7108  		req, _ := msgjson.NewRequest(dc.NextID(), msgjson.NoMatchRoute, payload)
  7109  		err := handleNoMatchRoute(tCore, dc, req)
  7110  		if err != nil {
  7111  			t.Fatalf("handleNoMatchRoute error: %v", err)
  7112  		}
  7113  	}
  7114  
  7115  	checkTradeStatus := func(tag string, oid order.OrderID, expStatus order.OrderStatus) {
  7116  		tracker, _ := dc.findOrder(oid)
  7117  		if tracker.metaData.Status != expStatus {
  7118  			t.Fatalf("%s: wrong status. expected %s, got %s", tag, expStatus, tracker.metaData.Status)
  7119  		}
  7120  		if rig.db.lastStatusID != oid {
  7121  			t.Fatalf("%s: order status not stored", tag)
  7122  		}
  7123  		if rig.db.lastStatus != expStatus {
  7124  			t.Fatalf("%s: wrong order status stored. expected %s, got %s", tag, expStatus, rig.db.lastStatus)
  7125  		}
  7126  		if expStatus == order.OrderStatusExecuted {
  7127  			if tBtcWallet.returnedAddr != tracker.Trade().Address {
  7128  				t.Fatalf("%s: redemption address not returned", tag)
  7129  			}
  7130  		}
  7131  	}
  7132  
  7133  	runNomatch("cancel", cancelOID)
  7134  	if rig.db.lastStatusID != cancelOID || rig.db.lastStatus != order.OrderStatusExecuted {
  7135  		t.Fatalf("cancel status not updated")
  7136  	}
  7137  	if rig.db.linkedFromID != standingOID || !rig.db.linkedToID.IsZero() {
  7138  		t.Fatalf("missed cancel not unlinked. wanted trade ID %s, got %s. wanted zeroed linked ID, got %s",
  7139  			standingOID, rig.db.linkedFromID, rig.db.linkedToID)
  7140  	}
  7141  
  7142  	runNomatch("standing limit", standingOID)
  7143  	checkTradeStatus("standing limit", standingOID, order.OrderStatusBooked)
  7144  
  7145  	runNomatch("immediate", immediateOID)
  7146  	checkTradeStatus("immediate", immediateOID, order.OrderStatusExecuted)
  7147  
  7148  	runNomatch("market", marketOID)
  7149  	checkTradeStatus("market", marketOID, order.OrderStatusExecuted)
  7150  
  7151  	// Unknown order should error.
  7152  	oid := ordertest.RandomOrderID()
  7153  	payload := &msgjson.NoMatch{OrderID: oid[:]}
  7154  	req, _ := msgjson.NewRequest(dc.NextID(), msgjson.NoMatchRoute, payload)
  7155  	err = handleNoMatchRoute(tCore, dc, req)
  7156  	if !errorHasCode(err, unknownOrderErr) {
  7157  		t.Fatalf("wrong error for unknown order ID: %v", err)
  7158  	}
  7159  }
  7160  
  7161  func TestWalletSettings(t *testing.T) {
  7162  	rig := newTestRig()
  7163  	defer rig.shutdown()
  7164  	tCore := rig.core
  7165  	rig.db.wallet = &db.Wallet{
  7166  		Settings: map[string]string{
  7167  			"abc": "123",
  7168  		},
  7169  	}
  7170  	var assetID uint32 = 54321
  7171  
  7172  	// wallet not found
  7173  	_, err := tCore.WalletSettings(assetID)
  7174  	if !errorHasCode(err, missingWalletErr) {
  7175  		t.Fatalf("wrong error for missing wallet: %v", err)
  7176  	}
  7177  
  7178  	tCore.wallets[assetID] = &xcWallet{}
  7179  
  7180  	// db error
  7181  	rig.db.walletErr = tErr
  7182  	_, err = tCore.WalletSettings(assetID)
  7183  	if !errorHasCode(err, dbErr) {
  7184  		t.Fatalf("wrong error when expected db error: %v", err)
  7185  	}
  7186  	rig.db.walletErr = nil
  7187  
  7188  	// success
  7189  	returnedSettings, err := tCore.WalletSettings(assetID)
  7190  	if err != nil {
  7191  		t.Fatalf("WalletSettings error: %v", err)
  7192  	}
  7193  
  7194  	if len(returnedSettings) != 1 || returnedSettings["abc"] != "123" {
  7195  		t.Fatalf("returned wallet settings are not correct: %v", returnedSettings)
  7196  	}
  7197  }
  7198  
  7199  func TestChangeAppPass(t *testing.T) {
  7200  	rig := newTestRig()
  7201  	defer rig.shutdown()
  7202  	// Use the smarter crypter.
  7203  	smartCrypter := newTCrypterSmart()
  7204  	rig.crypter = smartCrypter
  7205  	rig.core.newCrypter = func([]byte) encrypt.Crypter { return newTCrypterSmart() }
  7206  	rig.core.reCrypter = func([]byte, []byte) (encrypt.Crypter, error) { return rig.crypter, smartCrypter.recryptErr }
  7207  
  7208  	tCore := rig.core
  7209  	newTPW := []byte("apppass")
  7210  
  7211  	// App Password error
  7212  	rig.crypter.(*tCrypterSmart).recryptErr = tErr
  7213  	err := tCore.ChangeAppPass(tPW, newTPW)
  7214  	if !errorHasCode(err, authErr) {
  7215  		t.Fatalf("wrong error for password error: %v", err)
  7216  	}
  7217  	rig.crypter.(*tCrypterSmart).recryptErr = nil
  7218  
  7219  	oldCreds := tCore.credentials
  7220  
  7221  	rig.db.creds = nil
  7222  	err = tCore.ChangeAppPass(tPW, newTPW)
  7223  	if err != nil {
  7224  		t.Fatal(err)
  7225  	}
  7226  
  7227  	if bytes.Equal(oldCreds.OuterKeyParams, tCore.credentials.OuterKeyParams) {
  7228  		t.Fatalf("credentials not updated in Core")
  7229  	}
  7230  
  7231  	if rig.db.creds == nil || !bytes.Equal(tCore.credentials.OuterKeyParams, rig.db.creds.OuterKeyParams) {
  7232  		t.Fatalf("credentials not updated in DB")
  7233  	}
  7234  }
  7235  
  7236  func TestResetAppPass(t *testing.T) {
  7237  	rig := newTestRig()
  7238  	defer rig.shutdown()
  7239  	crypter := newTCrypterSmart()
  7240  	rig.crypter = crypter
  7241  	rig.core.newCrypter = func([]byte) encrypt.Crypter { return crypter }
  7242  	rig.core.reCrypter = func([]byte, []byte) (encrypt.Crypter, error) { return rig.crypter, crypter.recryptErr }
  7243  
  7244  	rig.core.credentials = nil
  7245  	rig.core.InitializeClient(tPW, nil)
  7246  
  7247  	tCore := rig.core
  7248  	seed, err := tCore.ExportSeed(tPW)
  7249  	if err != nil {
  7250  		t.Fatalf("seed export failed: %v", err)
  7251  	}
  7252  
  7253  	// Invalid seed error
  7254  	invalidSeed := seed[:24]
  7255  	err = tCore.ResetAppPass(tPW, invalidSeed)
  7256  	if !strings.Contains(err.Error(), "unabled to decode provided seed") {
  7257  		t.Fatalf("wrong error for invalid seed length: %v", err)
  7258  	}
  7259  
  7260  	// Want incorrect seed error.
  7261  	rig.crypter.(*tCrypterSmart).recryptErr = tErr
  7262  	// tCrypter is used to encode the orginal seed but we don't need it here, so
  7263  	// we need to add 8 bytes to commplete the expected seed lenght(64).
  7264  	err = tCore.ResetAppPass(tPW, seed+"blah")
  7265  	if !strings.Contains(err.Error(), "unabled to decode provided seed") {
  7266  		t.Fatalf("wrong error for incorrect seed: %v", err)
  7267  	}
  7268  
  7269  	// ok, no crypter error.
  7270  	rig.crypter.(*tCrypterSmart).recryptErr = nil
  7271  	err = tCore.ResetAppPass(tPW, seed)
  7272  	if err != nil {
  7273  		t.Fatalf("unexpected error: %v", err)
  7274  	}
  7275  }
  7276  
  7277  func TestReconfigureWallet(t *testing.T) {
  7278  	rig := newTestRig()
  7279  	defer rig.shutdown()
  7280  	tCore := rig.core
  7281  	rig.db.wallet = &db.Wallet{
  7282  		Settings: map[string]string{
  7283  			"abc": "123",
  7284  		},
  7285  	}
  7286  	const assetID uint32 = 54321
  7287  	xyzWallet, tXyzWallet := newTWallet(assetID)
  7288  	newSettings := map[string]string{
  7289  		"def": "456",
  7290  	}
  7291  
  7292  	form := &WalletForm{
  7293  		AssetID: assetID,
  7294  		Config:  newSettings,
  7295  		Type:    "type",
  7296  	}
  7297  	xyzWallet.walletType = "type"
  7298  
  7299  	// App Password error
  7300  	rig.crypter.(*tCrypter).recryptErr = tErr
  7301  	err := tCore.ReconfigureWallet(tPW, nil, form)
  7302  	if !errorHasCode(err, authErr) {
  7303  		t.Fatalf("wrong error for password error: %v", err)
  7304  	}
  7305  	rig.crypter.(*tCrypter).recryptErr = nil
  7306  
  7307  	// Missing wallet error
  7308  	err = tCore.ReconfigureWallet(tPW, nil, form)
  7309  	if !errorHasCode(err, assetSupportErr) {
  7310  		t.Fatalf("wrong error for missing wallet definition: %v", err)
  7311  	}
  7312  
  7313  	walletDef := &asset.WalletDefinition{
  7314  		Type:   "type",
  7315  		Seeded: true,
  7316  	}
  7317  	winfo := *tWalletInfo
  7318  	winfo.AvailableWallets = []*asset.WalletDefinition{walletDef}
  7319  
  7320  	assetDriver := &tCreator{
  7321  		tDriver: &tDriver{
  7322  			wallet: xyzWallet.Wallet,
  7323  			winfo:  &winfo,
  7324  		},
  7325  	}
  7326  	asset.Register(assetID, assetDriver)
  7327  	if err = xyzWallet.Connect(); err != nil {
  7328  		t.Fatal(err)
  7329  	}
  7330  	defer xyzWallet.Disconnect()
  7331  
  7332  	// Missing wallet error
  7333  	err = tCore.ReconfigureWallet(tPW, nil, form)
  7334  	if !errorHasCode(err, missingWalletErr) {
  7335  		t.Fatalf("wrong error for missing wallet: %v", err)
  7336  	}
  7337  
  7338  	tCore.wallets[assetID] = xyzWallet
  7339  
  7340  	// Errors for seeded wallets.
  7341  	walletDef.Seeded = true
  7342  	// Exists error
  7343  	assetDriver.existsErr = tErr
  7344  	err = tCore.ReconfigureWallet(tPW, nil, form)
  7345  	if !errorHasCode(err, existenceCheckErr) {
  7346  		t.Fatalf("wrong error when expecting existence check error: %v", err)
  7347  	}
  7348  	assetDriver.existsErr = nil
  7349  	// Create error
  7350  	assetDriver.doesntExist = true
  7351  	assetDriver.createErr = tErr
  7352  	err = tCore.ReconfigureWallet(tPW, nil, form)
  7353  	if !errorHasCode(err, createWalletErr) {
  7354  		t.Fatalf("wrong error when expecting wallet creation error error: %v", err)
  7355  	}
  7356  	assetDriver.createErr = nil
  7357  	walletDef.Seeded = false
  7358  
  7359  	// Connect error
  7360  	tXyzWallet.connectErr = tErr
  7361  	err = tCore.ReconfigureWallet(tPW, nil, form)
  7362  	if !errorHasCode(err, connectWalletErr) {
  7363  		t.Fatalf("wrong error when expecting connection error: %v", err)
  7364  	}
  7365  	tXyzWallet.connectErr = nil
  7366  
  7367  	// Unlock error
  7368  	tXyzWallet.Unlock(wPW)
  7369  	tXyzWallet.unlockErr = tErr
  7370  	err = tCore.ReconfigureWallet(tPW, nil, form)
  7371  	if !errorHasCode(err, walletAuthErr) {
  7372  		t.Fatalf("wrong error when expecting auth error: %v", err)
  7373  	}
  7374  	tXyzWallet.unlockErr = nil
  7375  
  7376  	// For the last success, make sure that we also clear any related
  7377  	// tickGovernors.
  7378  	abcWallet, _ := newTWallet(tUTXOAssetA.ID) // for to/baseWallet
  7379  	matchID := ordertest.RandomMatchID()
  7380  	match := &matchTracker{
  7381  		suspectSwap:  true,
  7382  		tickGovernor: time.NewTimer(time.Hour),
  7383  		MetaMatch: db.MetaMatch{
  7384  			MetaData: &db.MatchMetaData{
  7385  				Proof: db.MatchProof{
  7386  					ContractData: dex.Bytes{0},
  7387  				},
  7388  			},
  7389  			UserMatch: &order.UserMatch{
  7390  				MatchID: matchID,
  7391  			},
  7392  		},
  7393  	}
  7394  	tCore.conns[tDexHost].tradeMtx.Lock()
  7395  	tCore.conns[tDexHost].trades[order.OrderID{}] = &trackedTrade{
  7396  		Order: &order.LimitOrder{
  7397  			P: order.Prefix{
  7398  				BaseAsset:  assetID,
  7399  				ServerTime: time.Now(),
  7400  			},
  7401  		},
  7402  		wallets: &walletSet{
  7403  			fromWallet:  xyzWallet,
  7404  			quoteWallet: xyzWallet, // sell=false
  7405  			toWallet:    abcWallet,
  7406  			baseWallet:  abcWallet,
  7407  		},
  7408  		matches: map[order.MatchID]*matchTracker{
  7409  			{}: match,
  7410  		},
  7411  		metaData:    &db.OrderMetaData{},
  7412  		dc:          rig.dc,
  7413  		readyToTick: true, // prevent resume path
  7414  	}
  7415  	tCore.conns[tDexHost].tradeMtx.Unlock()
  7416  
  7417  	// Error checking if wallet owns address.
  7418  	tXyzWallet.ownsAddressErr = tErr
  7419  	err = tCore.ReconfigureWallet(tPW, nil, form)
  7420  	if !errorHasCode(err, walletErr) {
  7421  		t.Fatalf("wrong error when expecting ownsAddress wallet error: %v", err)
  7422  	}
  7423  	tXyzWallet.ownsAddressErr = nil
  7424  
  7425  	// Wallet doesn't own address.
  7426  	tXyzWallet.ownsAddress = false
  7427  	err = tCore.ReconfigureWallet(tPW, nil, form)
  7428  	if !errorHasCode(err, walletErr) {
  7429  		t.Fatalf("wrong error when expecting not owned wallet error: %v", err)
  7430  	}
  7431  
  7432  	// Leave the ownsAddress false, but swap out a LiveReconfigurer and ensure
  7433  	// the restart = false path passes.
  7434  	liveReconfigurer := &TLiveReconfigurer{TXCWallet: tXyzWallet}
  7435  	xyzWallet.Wallet = liveReconfigurer
  7436  	if err = tCore.ReconfigureWallet(tPW, nil, form); err != nil {
  7437  		t.Fatalf("ReconfigureWallet error for short path: %v", err)
  7438  	}
  7439  
  7440  	// But restart = true should still fail for live orders.
  7441  	liveReconfigurer.restart = true
  7442  	err = tCore.ReconfigureWallet(tPW, nil, form)
  7443  	if !errorHasCode(err, walletErr) {
  7444  		t.Fatalf("wrong error when expecting not owned wallet error: %v", err)
  7445  	}
  7446  	liveReconfigurer.restart = false
  7447  
  7448  	// OwnsAddress error
  7449  	liveReconfigurer.ownsAddressErr = tErr
  7450  	err = tCore.ReconfigureWallet(tPW, nil, form)
  7451  	if !errorHasCode(err, walletErr) {
  7452  		t.Fatalf("wrong error when expecting ownsAddress wallet error without restart: %v", err)
  7453  	}
  7454  	liveReconfigurer.ownsAddressErr = nil
  7455  
  7456  	// Refresh address error
  7457  	liveReconfigurer.addrErr = tErr
  7458  	err = tCore.ReconfigureWallet(tPW, nil, form)
  7459  	if !errorHasCode(err, newAddrErr) {
  7460  		t.Fatalf("wrong error when expecting address refresh error without restart: %v", err)
  7461  	}
  7462  	liveReconfigurer.addrErr = nil
  7463  
  7464  	// Password error for non-seeded wallet with password.
  7465  	// from above: walletDef.Seeded = false
  7466  	liveReconfigurer.unlockErr = tErr
  7467  	err = tCore.ReconfigureWallet(tPW, append(tPW, 5), form)
  7468  	if !errorHasCode(err, walletAuthErr) {
  7469  		t.Fatalf("wrong error when expecting new password error without restart: %v", err)
  7470  	}
  7471  	liveReconfigurer.unlockErr = nil
  7472  
  7473  	// DB error for restartless path.
  7474  	rig.db.updateWalletErr = tErr
  7475  	err = tCore.ReconfigureWallet(tPW, nil, form)
  7476  	if !errorHasCode(err, dbErr) {
  7477  		t.Fatalf("wrong error when db update error without restart: %v", err)
  7478  	}
  7479  	rig.db.updateWalletErr = nil
  7480  
  7481  	// End LiveReconfigurer tests.
  7482  	xyzWallet.Wallet = tXyzWallet
  7483  	tXyzWallet.ownsAddress = true
  7484  
  7485  	// Success updating settings.
  7486  	err = tCore.ReconfigureWallet(tPW, nil, form)
  7487  	if err != nil {
  7488  		t.Fatalf("ReconfigureWallet error: %v", err)
  7489  	}
  7490  
  7491  	settings := rig.db.wallet.Settings
  7492  	if len(settings) != 1 || settings["def"] != "456" {
  7493  		t.Fatalf("settings not stored")
  7494  	}
  7495  
  7496  	if match.tickGovernor != nil {
  7497  		t.Fatalf("tickGovernor not removed")
  7498  	}
  7499  
  7500  	// Success updating wallet PW.
  7501  	newWalletPW := []byte("password")
  7502  	err = tCore.ReconfigureWallet(tPW, newWalletPW, form)
  7503  	if err != nil {
  7504  		t.Fatalf("ReconfigureWallet error: %v", err)
  7505  	}
  7506  
  7507  	// Check that the xcWallet was updated.
  7508  	xyzWallet = tCore.wallets[assetID]
  7509  	decNewPW, _ := rig.crypter.Decrypt(xyzWallet.encPW())
  7510  	if !bytes.Equal(decNewPW, newWalletPW) {
  7511  		t.Fatalf("xcWallet encPW field not updated want: %x got: %x",
  7512  			newWalletPW, decNewPW)
  7513  	}
  7514  }
  7515  
  7516  func TestSetWalletPassword(t *testing.T) {
  7517  	rig := newTestRig()
  7518  	defer rig.shutdown()
  7519  	tCore := rig.core
  7520  	rig.db.wallet = &db.Wallet{
  7521  		EncryptedPW: []byte("abc"),
  7522  	}
  7523  	newPW := []byte("def")
  7524  	var assetID uint32 = 54321
  7525  
  7526  	// Nil password error
  7527  	err := tCore.SetWalletPassword(tPW, assetID, nil)
  7528  	if !errorHasCode(err, passwordErr) {
  7529  		t.Fatalf("wrong error for nil password error: %v", err)
  7530  	}
  7531  
  7532  	// Auth error
  7533  	rig.crypter.(*tCrypter).recryptErr = tErr
  7534  	err = tCore.SetWalletPassword(tPW, assetID, newPW)
  7535  	if !errorHasCode(err, authErr) {
  7536  		t.Fatalf("wrong error for auth error: %v", err)
  7537  	}
  7538  	rig.crypter.(*tCrypter).recryptErr = nil
  7539  
  7540  	// Missing wallet error
  7541  	err = tCore.SetWalletPassword(tPW, assetID, newPW)
  7542  	if !errorHasCode(err, missingWalletErr) {
  7543  		t.Fatalf("wrong error for missing wallet: %v", err)
  7544  	}
  7545  
  7546  	xyzWallet, tXyzWallet := newTWallet(assetID)
  7547  	tCore.wallets[assetID] = xyzWallet
  7548  
  7549  	// Connection error
  7550  	xyzWallet.hookedUp = false
  7551  	tXyzWallet.connectErr = tErr
  7552  	err = tCore.SetWalletPassword(tPW, assetID, newPW)
  7553  	if !errorHasCode(err, connectionErr) {
  7554  		t.Fatalf("wrong error for connection error: %v", err)
  7555  	}
  7556  	xyzWallet.hookedUp = true
  7557  	tXyzWallet.connectErr = nil
  7558  
  7559  	// Unlock error
  7560  	tXyzWallet.unlockErr = tErr
  7561  	err = tCore.SetWalletPassword(tPW, assetID, newPW)
  7562  	if !errorHasCode(err, authErr) {
  7563  		t.Fatalf("wrong error for auth error: %v", err)
  7564  	}
  7565  	tXyzWallet.unlockErr = nil
  7566  
  7567  	// SetWalletPassword db error
  7568  	rig.db.setWalletPwErr = tErr
  7569  	err = tCore.SetWalletPassword(tPW, assetID, newPW)
  7570  	if !errorHasCode(err, dbErr) {
  7571  		t.Fatalf("wrong error for missing wallet: %v", err)
  7572  	}
  7573  	rig.db.setWalletPwErr = nil
  7574  
  7575  	// Success
  7576  	err = tCore.SetWalletPassword(tPW, assetID, newPW)
  7577  	if err != nil {
  7578  		t.Fatalf("SetWalletPassword error: %v", err)
  7579  	}
  7580  
  7581  	// Check that the xcWallet was updated.
  7582  	decNewPW, _ := rig.crypter.Decrypt(xyzWallet.encPW())
  7583  	if !bytes.Equal(decNewPW, newPW) {
  7584  		t.Fatalf("xcWallet encPW field not updated")
  7585  	}
  7586  }
  7587  
  7588  func TestHandlePenaltyMsg(t *testing.T) {
  7589  	rig := newTestRig()
  7590  	defer rig.shutdown()
  7591  	tCore := rig.core
  7592  	dc := rig.dc
  7593  	penalty := &msgjson.Penalty{
  7594  		Rule:    account.Rule(1),
  7595  		Time:    uint64(1598929305),
  7596  		Details: "You may no longer trade. Leave your client running to finish pending trades.",
  7597  	}
  7598  	diffKey, _ := secp256k1.GeneratePrivateKey()
  7599  	noMatch, err := msgjson.NewNotification(msgjson.NoMatchRoute, "fake")
  7600  	if err != nil {
  7601  		t.Fatal(err)
  7602  	}
  7603  	tests := []struct {
  7604  		name    string
  7605  		key     *secp256k1.PrivateKey
  7606  		payload any
  7607  		wantErr bool
  7608  	}{{
  7609  		name:    "ok",
  7610  		key:     tDexPriv,
  7611  		payload: penalty,
  7612  	}, {
  7613  		name:    "bad note",
  7614  		key:     tDexPriv,
  7615  		payload: noMatch,
  7616  		wantErr: true,
  7617  	}, {
  7618  		name:    "wrong sig",
  7619  		key:     diffKey,
  7620  		payload: penalty,
  7621  		wantErr: true,
  7622  	}}
  7623  	for _, test := range tests {
  7624  		var err error
  7625  		var note *msgjson.Message
  7626  		switch v := test.payload.(type) {
  7627  		case *msgjson.Penalty:
  7628  			penaltyNote := &msgjson.PenaltyNote{
  7629  				Penalty: v,
  7630  			}
  7631  			sign(test.key, penaltyNote)
  7632  			note, err = msgjson.NewNotification(msgjson.PenaltyRoute, penaltyNote)
  7633  			if err != nil {
  7634  				t.Fatalf("error creating penalty notification: %v", err)
  7635  			}
  7636  		case *msgjson.Message:
  7637  			note = v
  7638  		default:
  7639  			t.Fatalf("unknown payload type: %T", v)
  7640  		}
  7641  
  7642  		err = handlePenaltyMsg(tCore, dc, note)
  7643  		if test.wantErr {
  7644  			if err == nil {
  7645  				t.Fatalf("expected error for test %s", test.name)
  7646  			}
  7647  			continue
  7648  		}
  7649  		if err != nil {
  7650  			t.Fatalf("%s: unexpected error: %v", test.name, err)
  7651  		}
  7652  	}
  7653  }
  7654  
  7655  func TestPreimageSync(t *testing.T) {
  7656  	rig := newTestRig()
  7657  	defer rig.shutdown()
  7658  	tCore := rig.core
  7659  	dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID)
  7660  	tCore.wallets[tUTXOAssetA.ID] = dcrWallet
  7661  	dcrWallet.Unlock(rig.crypter)
  7662  
  7663  	btcWallet, tBtcWallet := newTWallet(tUTXOAssetB.ID)
  7664  	tCore.wallets[tUTXOAssetB.ID] = btcWallet
  7665  	btcWallet.Unlock(rig.crypter)
  7666  
  7667  	var lots uint64 = 10
  7668  	qty := dcrBtcLotSize * lots
  7669  	rate := dcrBtcRateStep * 1000
  7670  
  7671  	form := &TradeForm{
  7672  		Host:    tDexHost,
  7673  		IsLimit: true,
  7674  		Sell:    true,
  7675  		Base:    tUTXOAssetA.ID,
  7676  		Quote:   tUTXOAssetB.ID,
  7677  		Qty:     qty,
  7678  		Rate:    rate,
  7679  		TifNow:  false,
  7680  	}
  7681  
  7682  	dcrCoin := &tCoin{
  7683  		id:  encode.RandomBytes(36),
  7684  		val: qty * 2,
  7685  	}
  7686  	tDcrWallet.fundingCoins = asset.Coins{dcrCoin}
  7687  	tDcrWallet.fundRedeemScripts = []dex.Bytes{nil}
  7688  
  7689  	btcVal := calc.BaseToQuote(rate, qty*2)
  7690  	btcCoin := &tCoin{
  7691  		id:  encode.RandomBytes(36),
  7692  		val: btcVal,
  7693  	}
  7694  	tBtcWallet.fundingCoins = asset.Coins{btcCoin}
  7695  	tBtcWallet.fundRedeemScripts = []dex.Bytes{nil}
  7696  
  7697  	limitRouteProcessing := make(chan order.OrderID)
  7698  	var commit order.Commitment
  7699  
  7700  	rig.ws.queueResponse(msgjson.LimitRoute, func(msg *msgjson.Message, f msgFunc) error {
  7701  		t.Helper()
  7702  		// Need to stamp and sign the message with the server's key.
  7703  		msgOrder := new(msgjson.LimitOrder)
  7704  		err := msg.Unmarshal(msgOrder)
  7705  		if err != nil {
  7706  			return fmt.Errorf("unmarshal error: %w", err)
  7707  		}
  7708  		lo := convertMsgLimitOrder(msgOrder)
  7709  		resp := orderResponse(msg.ID, msgOrder, lo, false, false, false)
  7710  		limitRouteProcessing <- lo.ID()
  7711  		commit = lo.Commit // accessed below only after errChan receive indicating Trade done
  7712  		f(resp)            // e.g. the UnmarshalJSON in sendRequest
  7713  		return nil
  7714  	})
  7715  
  7716  	errChan := make(chan error, 1)
  7717  	// Run the trade in a goroutine.
  7718  	go func() {
  7719  		_, err := tCore.Trade(tPW, form)
  7720  		errChan <- err
  7721  	}()
  7722  
  7723  	// Wait for the limit route to start processing. Then we have 100 ms to call
  7724  	// handlePreimageRequest to catch the early-preimage case.
  7725  	var oid order.OrderID
  7726  	select {
  7727  	case oid = <-limitRouteProcessing:
  7728  	case <-time.After(time.Second):
  7729  		t.Fatalf("limit route never hit")
  7730  	}
  7731  
  7732  	err := <-errChan
  7733  	if err != nil {
  7734  		t.Fatalf("trade error: %v", err)
  7735  	}
  7736  
  7737  	// So ideally, we're calling handlePreimageRequest about 100 ms before we
  7738  	// even have an order id back from the server. This shouldn't result in an
  7739  	// error.
  7740  	payload := &msgjson.PreimageRequest{
  7741  		OrderID:    oid[:],
  7742  		Commitment: commit[:],
  7743  	}
  7744  	req, _ := msgjson.NewRequest(rig.dc.NextID(), msgjson.PreimageRoute, payload)
  7745  	err = handlePreimageRequest(rig.core, rig.dc, req)
  7746  	if err != nil {
  7747  		t.Fatalf("early preimage request error: %v", err)
  7748  	}
  7749  }
  7750  
  7751  func TestAccelerateOrder(t *testing.T) {
  7752  	rig := newTestRig()
  7753  	defer rig.shutdown()
  7754  	tCore := rig.core
  7755  	dc := rig.dc
  7756  
  7757  	dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID)
  7758  	tDcrWallet.swapSize = tSwapSizeA
  7759  	tCore.wallets[tUTXOAssetA.ID] = dcrWallet
  7760  	btcWallet, tBtcWallet := newTWallet(tUTXOAssetB.ID)
  7761  	tBtcWallet.swapSize = tSwapSizeB
  7762  	tCore.wallets[tUTXOAssetB.ID] = btcWallet
  7763  
  7764  	buyWalletSet, _, _, _ := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, false)
  7765  	sellWalletSet, _, _, _ := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, false)
  7766  
  7767  	var newBaseFeeRate uint64 = 55
  7768  	var newQuoteFeeRate uint64 = 65
  7769  	feeRateSource := func(msg *msgjson.Message, f msgFunc) error {
  7770  		var resp *msgjson.Message
  7771  		if string(msg.Payload) == "42" {
  7772  			resp, _ = msgjson.NewResponse(msg.ID, newBaseFeeRate, nil)
  7773  		} else {
  7774  			resp, _ = msgjson.NewResponse(msg.ID, newQuoteFeeRate, nil)
  7775  		}
  7776  		f(resp)
  7777  		return nil
  7778  	}
  7779  
  7780  	type testMatch struct {
  7781  		status   order.MatchStatus
  7782  		quantity uint64
  7783  		rate     uint64
  7784  		side     order.MatchSide
  7785  	}
  7786  
  7787  	tests := []struct {
  7788  		name                       string
  7789  		orderQuantity              uint64
  7790  		orderFilled                uint64
  7791  		orderStatus                order.OrderStatus
  7792  		rate                       uint64
  7793  		sell                       bool
  7794  		previousAccelerations      []order.CoinID
  7795  		matches                    []testMatch
  7796  		expectRequiredForRemaining uint64
  7797  		expectError                bool
  7798  		orderIDIncorrectLength     bool
  7799  		nonActiveOrderID           bool
  7800  		accelerateOrderError       bool
  7801  		nilChangeCoin              bool
  7802  		nilNewChangeCoin           bool
  7803  	}{
  7804  		{
  7805  			name:                       "ok",
  7806  			orderQuantity:              3 * dcrBtcLotSize,
  7807  			orderFilled:                dcrBtcLotSize,
  7808  			previousAccelerations:      []order.CoinID{encode.RandomBytes(32)},
  7809  			orderStatus:                order.OrderStatusExecuted,
  7810  			rate:                       dcrBtcRateStep * 10,
  7811  			expectRequiredForRemaining: 2*tMaxFeeRate*tSwapSizeB + calc.BaseToQuote(dcrBtcRateStep*10, 2*dcrBtcLotSize),
  7812  			matches: []testMatch{
  7813  				{
  7814  					side:     order.Maker,
  7815  					status:   order.TakerSwapCast,
  7816  					quantity: dcrBtcLotSize,
  7817  					rate:     dcrBtcRateStep * 10,
  7818  				},
  7819  			},
  7820  		},
  7821  		{
  7822  			name:                       "ok - unswapped match, buy",
  7823  			orderQuantity:              8 * dcrBtcLotSize,
  7824  			orderFilled:                5 * dcrBtcLotSize,
  7825  			orderStatus:                order.OrderStatusExecuted,
  7826  			previousAccelerations:      []order.CoinID{encode.RandomBytes(32), encode.RandomBytes(32)},
  7827  			rate:                       dcrBtcRateStep * 10,
  7828  			expectRequiredForRemaining: 4*tMaxFeeRate*tSwapSizeB + calc.BaseToQuote(dcrBtcRateStep*10, 5*dcrBtcLotSize),
  7829  			matches: []testMatch{
  7830  				{
  7831  					side:     order.Maker,
  7832  					status:   order.TakerSwapCast,
  7833  					quantity: dcrBtcLotSize,
  7834  					rate:     dcrBtcRateStep * 10,
  7835  				},
  7836  				{
  7837  					side:     order.Taker,
  7838  					status:   order.TakerSwapCast,
  7839  					quantity: 2 * dcrBtcLotSize,
  7840  					rate:     dcrBtcRateStep * 10,
  7841  				},
  7842  				{
  7843  					side:     order.Taker,
  7844  					status:   order.MakerSwapCast,
  7845  					quantity: 2 * dcrBtcLotSize,
  7846  					rate:     dcrBtcRateStep * 10,
  7847  				},
  7848  			},
  7849  		},
  7850  		{
  7851  			name:                       "ok - unswapped match, sell",
  7852  			sell:                       true,
  7853  			previousAccelerations:      []order.CoinID{encode.RandomBytes(32), encode.RandomBytes(32)},
  7854  			orderQuantity:              8 * dcrBtcLotSize,
  7855  			orderFilled:                5 * dcrBtcLotSize,
  7856  			orderStatus:                order.OrderStatusExecuted,
  7857  			rate:                       dcrBtcRateStep * 10,
  7858  			expectRequiredForRemaining: 4*tMaxFeeRate*tSwapSizeB + 5*dcrBtcLotSize,
  7859  			matches: []testMatch{
  7860  				{
  7861  					side:     order.Maker,
  7862  					status:   order.TakerSwapCast,
  7863  					quantity: dcrBtcLotSize,
  7864  					rate:     dcrBtcRateStep * 10,
  7865  				},
  7866  				{
  7867  					side:     order.Taker,
  7868  					status:   order.TakerSwapCast,
  7869  					quantity: 2 * dcrBtcLotSize,
  7870  					rate:     dcrBtcRateStep * 10,
  7871  				},
  7872  				{
  7873  					side:     order.Taker,
  7874  					status:   order.MakerSwapCast,
  7875  					quantity: 2 * dcrBtcLotSize,
  7876  					rate:     dcrBtcRateStep * 10,
  7877  				},
  7878  			},
  7879  		},
  7880  		{
  7881  			name: "10 previous accelerations",
  7882  			sell: true,
  7883  			previousAccelerations: []order.CoinID{encode.RandomBytes(32), encode.RandomBytes(32),
  7884  				encode.RandomBytes(32), encode.RandomBytes(32),
  7885  				encode.RandomBytes(32), encode.RandomBytes(32),
  7886  				encode.RandomBytes(32), encode.RandomBytes(32),
  7887  				encode.RandomBytes(32), encode.RandomBytes(32)},
  7888  			orderQuantity: 8 * dcrBtcLotSize,
  7889  			orderFilled:   5 * dcrBtcLotSize,
  7890  			orderStatus:   order.OrderStatusExecuted,
  7891  			rate:          dcrBtcRateStep * 10,
  7892  			matches: []testMatch{
  7893  				{
  7894  					side:     order.Maker,
  7895  					status:   order.TakerSwapCast,
  7896  					quantity: dcrBtcLotSize,
  7897  					rate:     dcrBtcRateStep * 10,
  7898  				},
  7899  			},
  7900  			expectError: true,
  7901  		},
  7902  		{
  7903  			name:          "no matches",
  7904  			orderQuantity: 3 * dcrBtcLotSize,
  7905  			orderFilled:   dcrBtcLotSize,
  7906  			orderStatus:   order.OrderStatusExecuted,
  7907  			rate:          dcrBtcRateStep * 10,
  7908  			matches:       []testMatch{},
  7909  			expectError:   true,
  7910  		},
  7911  		{
  7912  			name:          "no swap coins",
  7913  			orderQuantity: 3 * dcrBtcLotSize,
  7914  			orderFilled:   dcrBtcLotSize,
  7915  			orderStatus:   order.OrderStatusExecuted,
  7916  			rate:          dcrBtcRateStep * 10,
  7917  			matches: []testMatch{{
  7918  				side:     order.Taker,
  7919  				status:   order.MakerSwapCast,
  7920  				quantity: 2 * dcrBtcLotSize,
  7921  				rate:     dcrBtcRateStep * 10,
  7922  			}},
  7923  			expectError: true,
  7924  		},
  7925  		{
  7926  			name:          "incorrect length order id",
  7927  			orderQuantity: 3 * dcrBtcLotSize,
  7928  			orderFilled:   dcrBtcLotSize,
  7929  			orderStatus:   order.OrderStatusExecuted,
  7930  			rate:          dcrBtcRateStep * 10,
  7931  			matches: []testMatch{
  7932  				{
  7933  					side:     order.Maker,
  7934  					status:   order.TakerSwapCast,
  7935  					quantity: dcrBtcLotSize,
  7936  					rate:     dcrBtcRateStep * 10,
  7937  				},
  7938  			},
  7939  			orderIDIncorrectLength: true,
  7940  			expectError:            true,
  7941  		},
  7942  		{
  7943  			name:          "incorrect length order id",
  7944  			orderQuantity: 3 * dcrBtcLotSize,
  7945  			orderFilled:   dcrBtcLotSize,
  7946  			orderStatus:   order.OrderStatusExecuted,
  7947  			rate:          dcrBtcRateStep * 10,
  7948  			matches: []testMatch{
  7949  				{
  7950  					side:     order.Maker,
  7951  					status:   order.TakerSwapCast,
  7952  					quantity: dcrBtcLotSize,
  7953  					rate:     dcrBtcRateStep * 10,
  7954  				},
  7955  			},
  7956  			nonActiveOrderID: true,
  7957  			expectError:      true,
  7958  		},
  7959  		{
  7960  			name:          "accelerate order err",
  7961  			orderQuantity: 3 * dcrBtcLotSize,
  7962  			orderFilled:   dcrBtcLotSize,
  7963  			orderStatus:   order.OrderStatusExecuted,
  7964  			rate:          dcrBtcRateStep * 10,
  7965  			matches: []testMatch{
  7966  				{
  7967  					side:     order.Maker,
  7968  					status:   order.TakerSwapCast,
  7969  					quantity: dcrBtcLotSize,
  7970  					rate:     dcrBtcRateStep * 10,
  7971  				},
  7972  			},
  7973  			accelerateOrderError: true,
  7974  			expectError:          true,
  7975  		},
  7976  		{
  7977  			name:          "nil change coin",
  7978  			orderQuantity: 3 * dcrBtcLotSize,
  7979  			orderFilled:   dcrBtcLotSize,
  7980  			orderStatus:   order.OrderStatusExecuted,
  7981  			rate:          dcrBtcRateStep * 10,
  7982  			matches: []testMatch{
  7983  				{
  7984  					side:     order.Maker,
  7985  					status:   order.TakerSwapCast,
  7986  					quantity: dcrBtcLotSize,
  7987  					rate:     dcrBtcRateStep * 10,
  7988  				},
  7989  			},
  7990  			nilChangeCoin: true,
  7991  			expectError:   true,
  7992  		},
  7993  		{
  7994  			name:                       "nil new change coin",
  7995  			orderQuantity:              3 * dcrBtcLotSize,
  7996  			orderFilled:                dcrBtcLotSize,
  7997  			orderStatus:                order.OrderStatusExecuted,
  7998  			rate:                       dcrBtcRateStep * 10,
  7999  			expectRequiredForRemaining: 2*tMaxFeeRate*tSwapSizeB + calc.BaseToQuote(dcrBtcRateStep*10, 2*dcrBtcLotSize),
  8000  			matches: []testMatch{
  8001  				{
  8002  					side:     order.Maker,
  8003  					status:   order.TakerSwapCast,
  8004  					quantity: dcrBtcLotSize,
  8005  					rate:     dcrBtcRateStep * 10,
  8006  				},
  8007  			},
  8008  			nilNewChangeCoin: true,
  8009  		},
  8010  	}
  8011  
  8012  	for _, test := range tests {
  8013  		tBtcWallet.accelerateOrderErr = nil
  8014  		lo, dbOrder, preImg, addr := makeLimitOrder(dc, test.sell, test.orderQuantity, test.rate)
  8015  		dbOrder.MetaData.Status = test.orderStatus // so there is no order_status request for this
  8016  		oid := lo.ID()
  8017  		var walletSet *walletSet
  8018  		if test.sell {
  8019  			walletSet = sellWalletSet
  8020  		} else {
  8021  			walletSet = buyWalletSet
  8022  		}
  8023  		trade := newTrackedTrade(dbOrder, preImg, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker,
  8024  			rig.db, rig.queue, walletSet, nil, rig.core.notify, rig.core.formatDetails)
  8025  		dc.trades[trade.ID()] = trade
  8026  		trade.Trade().AddFill(test.orderFilled)
  8027  
  8028  		trade.metaData.ChangeCoin = encode.RandomBytes(32)
  8029  		originalChangeCoin := trade.metaData.ChangeCoin
  8030  		trade.metaData.AccelerationCoins = test.previousAccelerations
  8031  		newChangeCoinID := dex.Bytes(encode.RandomBytes(32))
  8032  		if test.nilNewChangeCoin {
  8033  			tBtcWallet.newChangeCoinID = nil
  8034  		} else {
  8035  			tBtcWallet.newChangeCoinID = &newChangeCoinID
  8036  		}
  8037  		tBtcWallet.newAccelerationTxID = hex.EncodeToString(encode.RandomBytes(32))
  8038  		trade.matches = make(map[order.MatchID]*matchTracker)
  8039  		expectedSwapCoins := make([]order.CoinID, 0, len(test.matches))
  8040  		for _, testMatch := range test.matches {
  8041  			matchID := ordertest.RandomMatchID()
  8042  			match := &matchTracker{
  8043  				MetaMatch: db.MetaMatch{
  8044  					MetaData: &db.MatchMetaData{
  8045  						Proof: db.MatchProof{
  8046  							MakerSwap: encode.RandomBytes(32),
  8047  							TakerSwap: encode.RandomBytes(32),
  8048  						},
  8049  					},
  8050  					UserMatch: &order.UserMatch{
  8051  						MatchID:  matchID,
  8052  						Address:  addr,
  8053  						Side:     testMatch.side,
  8054  						Status:   testMatch.status,
  8055  						Quantity: testMatch.quantity,
  8056  						Rate:     testMatch.rate,
  8057  					},
  8058  				},
  8059  			}
  8060  			if testMatch.side == order.Maker && testMatch.status >= order.MakerSwapCast {
  8061  				expectedSwapCoins = append(expectedSwapCoins, match.MetaData.Proof.MakerSwap)
  8062  			}
  8063  			if testMatch.side == order.Taker && testMatch.status >= order.TakerSwapCast {
  8064  				expectedSwapCoins = append(expectedSwapCoins, match.MetaData.Proof.TakerSwap)
  8065  			}
  8066  			trade.matches[matchID] = match
  8067  		}
  8068  		orderIDBytes := oid.Bytes()
  8069  		if test.orderIDIncorrectLength {
  8070  			orderIDBytes = encode.RandomBytes(31)
  8071  		}
  8072  		if test.nonActiveOrderID {
  8073  			orderIDBytes = encode.RandomBytes(32)
  8074  		}
  8075  		if test.accelerateOrderError {
  8076  			tBtcWallet.accelerateOrderErr = errors.New("")
  8077  		}
  8078  		if test.nilChangeCoin {
  8079  			trade.metaData.ChangeCoin = nil
  8080  		}
  8081  
  8082  		checkCommonCallValues := func() {
  8083  			t.Helper()
  8084  			swapCoins := tBtcWallet.accelerationParams.swapCoins
  8085  			if len(swapCoins) != len(expectedSwapCoins) {
  8086  				t.Fatalf("expected %d swap coins but got %d", len(expectedSwapCoins), len(swapCoins))
  8087  			}
  8088  
  8089  			sort.Slice(swapCoins, func(i, j int) bool { return bytes.Compare(swapCoins[i], swapCoins[j]) > 0 })
  8090  			sort.Slice(expectedSwapCoins, func(i, j int) bool { return bytes.Compare(expectedSwapCoins[i], expectedSwapCoins[j]) > 0 })
  8091  
  8092  			for i := range swapCoins {
  8093  				if !bytes.Equal(swapCoins[i], expectedSwapCoins[i]) {
  8094  					t.Fatalf("expected swap coins not the same as actual")
  8095  				}
  8096  			}
  8097  
  8098  			changeCoin := tBtcWallet.accelerationParams.changeCoin
  8099  			if !bytes.Equal(changeCoin, originalChangeCoin) {
  8100  				t.Fatalf("change coin not same as expected %x - %x", changeCoin, trade.metaData.ChangeCoin)
  8101  			}
  8102  
  8103  			accelerationCoins := tBtcWallet.accelerationParams.accelerationCoins
  8104  			if len(accelerationCoins) != len(test.previousAccelerations) {
  8105  				t.Fatalf("expected 1 acceleration tx but got %v", len(accelerationCoins))
  8106  			}
  8107  			for i := range accelerationCoins {
  8108  				if !bytes.Equal(accelerationCoins[i], test.previousAccelerations[i]) {
  8109  					t.Fatalf("expected acceleration coin not the same as actual")
  8110  				}
  8111  			}
  8112  		}
  8113  
  8114  		checkRequiredForRemainingSwaps := func() {
  8115  			t.Helper()
  8116  			if tBtcWallet.accelerationParams.requiredForRemainingSwaps != test.expectRequiredForRemaining {
  8117  				t.Fatalf("expected requiredForRemainingSwaps %d, but got %d", test.expectRequiredForRemaining,
  8118  					tBtcWallet.accelerationParams.requiredForRemainingSwaps)
  8119  			}
  8120  		}
  8121  
  8122  		testAccelerateOrder := func() {
  8123  			newFeeRate := rand.Uint64()
  8124  			txID, err := tCore.AccelerateOrder(tPW, orderIDBytes, newFeeRate)
  8125  			if test.expectError {
  8126  				if err == nil {
  8127  					t.Fatalf("expected error, but did not get")
  8128  				}
  8129  				return
  8130  			}
  8131  			if err != nil {
  8132  				t.Fatalf("unexpected error: %v", err)
  8133  			}
  8134  
  8135  			checkCommonCallValues()
  8136  			checkRequiredForRemainingSwaps()
  8137  
  8138  			if test.nilNewChangeCoin {
  8139  				if tBtcWallet.newChangeCoinID != nil {
  8140  					t.Fatalf("expected coin on order to be nil, but got %x", tBtcWallet.newChangeCoinID)
  8141  				}
  8142  			} else {
  8143  				if !bytes.Equal(trade.metaData.ChangeCoin, *tBtcWallet.newChangeCoinID) {
  8144  					t.Fatalf("change coin on trade was not updated to return value from AccelerateOrder")
  8145  				}
  8146  				if !bytes.Equal(trade.metaData.AccelerationCoins[len(trade.metaData.AccelerationCoins)-1], *tBtcWallet.newChangeCoinID) {
  8147  					t.Fatalf("new acceleration transaction id was not added to the trade")
  8148  				}
  8149  
  8150  				var inCoinsList bool
  8151  				for _, coin := range trade.coins {
  8152  					if bytes.Equal(coin.ID(), *tBtcWallet.newChangeCoinID) {
  8153  						inCoinsList = true
  8154  					}
  8155  				}
  8156  				if !inCoinsList {
  8157  					t.Fatalf("new change coin must be added to the trade.coins slice")
  8158  				}
  8159  			}
  8160  			if txID != tBtcWallet.newAccelerationTxID {
  8161  				t.Fatalf("new acceleration transaction id was not returned from AccelerateOrder")
  8162  			}
  8163  			if newFeeRate != tBtcWallet.accelerationParams.newFeeRate {
  8164  				t.Fatalf("%s: expected new fee rate %d, but got %d", test.name,
  8165  					newFeeRate, tBtcWallet.accelerationParams.newFeeRate)
  8166  			}
  8167  		}
  8168  
  8169  		testPreAccelerate := func() {
  8170  			rig.ws.queueResponse(msgjson.FeeRateRoute, feeRateSource)
  8171  			tBtcWallet.preAccelerateSwapRate = rand.Uint64()
  8172  			tBtcWallet.preAccelerateSuggestedRange = asset.XYRange{
  8173  				Start: asset.XYRangePoint{
  8174  					Label: "startLabel",
  8175  					X:     rand.Float64(),
  8176  					Y:     rand.Float64(),
  8177  				},
  8178  				End: asset.XYRangePoint{
  8179  					Label: "endLabel",
  8180  					X:     rand.Float64(),
  8181  					Y:     rand.Float64(),
  8182  				},
  8183  				XUnit: "x",
  8184  				YUnit: "y",
  8185  			}
  8186  
  8187  			preAccelerate, err := tCore.PreAccelerateOrder(orderIDBytes)
  8188  			if test.expectError {
  8189  				if err == nil {
  8190  					t.Fatalf("expected error, but did not get")
  8191  				}
  8192  				return
  8193  			}
  8194  			if err != nil {
  8195  				t.Fatalf("%s: unexpected error: %v", test.name, err)
  8196  			}
  8197  
  8198  			checkCommonCallValues()
  8199  			checkRequiredForRemainingSwaps()
  8200  
  8201  			if !test.sell && preAccelerate.SuggestedRate != newQuoteFeeRate {
  8202  				t.Fatalf("%s: expected fee suggestion to be %d, but got %d",
  8203  					test.name, newQuoteFeeRate, preAccelerate.SuggestedRate)
  8204  			}
  8205  			if test.sell && preAccelerate.SuggestedRate != newBaseFeeRate {
  8206  				t.Fatalf("%s: expected fee suggestion to be %d, but got %d",
  8207  					test.name, newBaseFeeRate, preAccelerate.SuggestedRate)
  8208  			}
  8209  			if preAccelerate.SwapRate != tBtcWallet.preAccelerateSwapRate {
  8210  				t.Fatalf("%s: expected pre accelerate swap rate %d, but got %d",
  8211  					test.name, tBtcWallet.preAccelerateSwapRate, preAccelerate.SwapRate)
  8212  			}
  8213  			if !reflect.DeepEqual(preAccelerate.SuggestedRange,
  8214  				tBtcWallet.preAccelerateSuggestedRange) {
  8215  				t.Fatalf("%s: PreAccelerate suggested range not same as expected",
  8216  					test.name)
  8217  			}
  8218  		}
  8219  
  8220  		testMaxAcceleration := func() {
  8221  			t.Helper()
  8222  			tBtcWallet.accelerationEstimate = rand.Uint64()
  8223  			newFeeRate := rand.Uint64()
  8224  			estimate, err := tCore.AccelerationEstimate(orderIDBytes, newFeeRate)
  8225  			if test.expectError {
  8226  				if err == nil {
  8227  					t.Fatalf("expected error, but did not get")
  8228  				}
  8229  				return
  8230  			}
  8231  			if err != nil {
  8232  				t.Fatalf("%s: unexpected error: %v", test.name, err)
  8233  			}
  8234  
  8235  			checkCommonCallValues()
  8236  			checkRequiredForRemainingSwaps()
  8237  
  8238  			if newFeeRate != tBtcWallet.accelerationParams.newFeeRate {
  8239  				t.Fatalf("%s: expected new fee rate %d, but got %d", test.name,
  8240  					newFeeRate, tBtcWallet.accelerationParams.newFeeRate)
  8241  			}
  8242  			if estimate != tBtcWallet.accelerationEstimate {
  8243  				t.Fatalf("%s: expected acceleration estimate %d, but got %d",
  8244  					test.name, tBtcWallet.accelerationEstimate, estimate)
  8245  			}
  8246  		}
  8247  
  8248  		testPreAccelerate()
  8249  		testMaxAcceleration()
  8250  		testAccelerateOrder()
  8251  	}
  8252  }
  8253  
  8254  func TestMatchStatusResolution(t *testing.T) {
  8255  	rig := newTestRig()
  8256  	defer rig.shutdown()
  8257  	tCore := rig.core
  8258  	dc := rig.dc
  8259  
  8260  	dcrWallet, _ := newTWallet(tUTXOAssetA.ID)
  8261  	tCore.wallets[tUTXOAssetA.ID] = dcrWallet
  8262  	btcWallet, tBtcWallet := newTWallet(tUTXOAssetB.ID)
  8263  	tCore.wallets[tUTXOAssetB.ID] = btcWallet
  8264  	walletSet, _, _, _ := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, true)
  8265  
  8266  	qty := 3 * dcrBtcLotSize
  8267  	secret := encode.RandomBytes(32)
  8268  	secretHash := sha256.Sum256(secret)
  8269  
  8270  	lo, dbOrder, preImg, addr := makeLimitOrder(dc, true, qty, dcrBtcRateStep*10)
  8271  	dbOrder.MetaData.Status = order.OrderStatusExecuted // so there is no order_status request for this
  8272  	oid := lo.ID()
  8273  	trade := newTrackedTrade(dbOrder, preImg, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker,
  8274  		rig.db, rig.queue, walletSet, nil, rig.core.notify, rig.core.formatDetails)
  8275  
  8276  	dc.trades[trade.ID()] = trade
  8277  	matchID := ordertest.RandomMatchID()
  8278  	matchTime := time.Now()
  8279  	match := &matchTracker{
  8280  		MetaMatch: db.MetaMatch{
  8281  			MetaData: &db.MatchMetaData{},
  8282  			UserMatch: &order.UserMatch{
  8283  				MatchID: matchID,
  8284  				Address: addr,
  8285  			},
  8286  		},
  8287  	}
  8288  	trade.matches[matchID] = match
  8289  
  8290  	// oid order.OrderID, mid order.MatchID, recipient string, val uint64, secretHash []byte
  8291  	_, auditInfo := tMsgAudit(oid, matchID, addr, qty, secretHash[:])
  8292  	tBtcWallet.auditInfo = auditInfo
  8293  
  8294  	connectMatches := func(status order.MatchStatus) []*msgjson.Match {
  8295  		return []*msgjson.Match{
  8296  			{
  8297  				OrderID: oid[:],
  8298  				MatchID: matchID[:],
  8299  				Status:  uint8(status),
  8300  				Side:    uint8(match.Side),
  8301  			},
  8302  		}
  8303  
  8304  	}
  8305  
  8306  	tBytes := encode.RandomBytes(2)
  8307  	tCoinID := encode.RandomBytes(36)
  8308  	tTxData := encode.RandomBytes(1)
  8309  
  8310  	setAuthSigs := func(status order.MatchStatus) {
  8311  		isMaker := match.Side == order.Maker
  8312  		match.MetaData.Proof.Auth = db.MatchAuth{}
  8313  		auth := &match.MetaData.Proof.Auth
  8314  		auth.MatchStamp = uint64(matchTime.UnixMilli())
  8315  		if status >= order.MakerSwapCast {
  8316  			if isMaker {
  8317  				auth.InitSig = tBytes
  8318  			} else {
  8319  				auth.AuditSig = tBytes
  8320  			}
  8321  		}
  8322  		if status >= order.TakerSwapCast {
  8323  			if isMaker {
  8324  				auth.AuditSig = tBytes
  8325  			} else {
  8326  				auth.InitSig = tBytes
  8327  			}
  8328  		}
  8329  		if status >= order.MakerRedeemed {
  8330  			if isMaker {
  8331  				auth.RedeemSig = tBytes
  8332  			} else {
  8333  				auth.RedemptionSig = tBytes
  8334  			}
  8335  		}
  8336  		if status >= order.MatchComplete {
  8337  			if isMaker {
  8338  				auth.RedemptionSig = tBytes
  8339  			} else {
  8340  				auth.RedeemSig = tBytes
  8341  			}
  8342  		}
  8343  	}
  8344  
  8345  	// Call setProof before setAuthSigs
  8346  	setProof := func(status order.MatchStatus) {
  8347  		isMaker := match.Side == order.Maker
  8348  		match.Status = status
  8349  		match.MetaData.Proof = db.MatchProof{}
  8350  		proof := &match.MetaData.Proof
  8351  
  8352  		if isMaker {
  8353  			auditInfo.Expiration = matchTime.Add(trade.lockTimeTaker)
  8354  		} else {
  8355  			auditInfo.Expiration = matchTime.Add(trade.lockTimeMaker)
  8356  		}
  8357  
  8358  		if status >= order.MakerSwapCast {
  8359  			proof.MakerSwap = tCoinID
  8360  			proof.SecretHash = secretHash[:]
  8361  			if isMaker {
  8362  				proof.ContractData = tBytes
  8363  				proof.Secret = secret
  8364  			} else {
  8365  				proof.CounterContract = tBytes
  8366  			}
  8367  		}
  8368  		if status >= order.TakerSwapCast {
  8369  			proof.TakerSwap = tCoinID
  8370  			if isMaker {
  8371  				proof.CounterContract = tBytes
  8372  			} else {
  8373  				proof.ContractData = tBytes
  8374  			}
  8375  		}
  8376  		if status >= order.MakerRedeemed {
  8377  			proof.MakerRedeem = tCoinID
  8378  			if !isMaker {
  8379  				proof.Secret = secret
  8380  			}
  8381  		}
  8382  		if status >= order.MatchComplete {
  8383  			proof.TakerRedeem = tCoinID
  8384  		}
  8385  	}
  8386  
  8387  	setLocalMatchStatus := func(proofStatus, authStatus order.MatchStatus) {
  8388  		setProof(proofStatus)
  8389  		setAuthSigs(authStatus)
  8390  	}
  8391  
  8392  	var tMatchResults *msgjson.MatchStatusResult
  8393  	setMatchResults := func(status order.MatchStatus) *msgjson.MatchStatusResult {
  8394  		tMatchResults = &msgjson.MatchStatusResult{
  8395  			MatchID: matchID[:],
  8396  			Status:  uint8(status),
  8397  			Active:  status != order.MatchComplete,
  8398  		}
  8399  		if status >= order.MakerSwapCast {
  8400  			tMatchResults.MakerContract = tBytes
  8401  			tMatchResults.MakerSwap = tCoinID
  8402  		}
  8403  		if status == order.MakerSwapCast || status == order.TakerSwapCast {
  8404  			tMatchResults.TakerTxData = tTxData
  8405  		}
  8406  		if status >= order.TakerSwapCast {
  8407  			tMatchResults.TakerContract = tBytes
  8408  			tMatchResults.TakerSwap = tCoinID
  8409  		}
  8410  		if status >= order.MakerRedeemed {
  8411  			tMatchResults.MakerRedeem = tCoinID
  8412  			tMatchResults.Secret = secret
  8413  		}
  8414  		if status >= order.MatchComplete {
  8415  			tMatchResults.TakerRedeem = tCoinID
  8416  		}
  8417  		return tMatchResults
  8418  	}
  8419  
  8420  	type test struct {
  8421  		ours, servers      order.MatchStatus
  8422  		side               order.MatchSide
  8423  		tweaker            func()
  8424  		countStatusUpdates int
  8425  	}
  8426  
  8427  	testName := func(tt *test) string {
  8428  		return fmt.Sprintf("%s / %s (%s)", tt.ours, tt.servers, tt.side)
  8429  	}
  8430  
  8431  	runTest := func(tt *test) order.MatchStatus {
  8432  		match.Side = tt.side
  8433  		setLocalMatchStatus(tt.ours, tt.servers)
  8434  		setMatchResults(tt.servers)
  8435  		if tt.tweaker != nil {
  8436  			tt.tweaker()
  8437  		}
  8438  		rig.queueConnect(nil, connectMatches(tt.servers), nil)
  8439  		rig.ws.queueResponse(msgjson.MatchStatusRoute, func(msg *msgjson.Message, f msgFunc) error {
  8440  			resp, _ := msgjson.NewResponse(msg.ID, []*msgjson.MatchStatusResult{tMatchResults}, nil)
  8441  			f(resp)
  8442  			return nil
  8443  		})
  8444  		if tt.countStatusUpdates > 0 {
  8445  			rig.db.updateMatchChan = make(chan order.MatchStatus, tt.countStatusUpdates)
  8446  		}
  8447  		if err := tCore.authDEX(dc); err != nil {
  8448  			t.Fatalf("unexpected authDEX error: %v", err)
  8449  		}
  8450  		for i := 0; i < tt.countStatusUpdates; i++ {
  8451  			<-rig.db.updateMatchChan
  8452  		}
  8453  		rig.db.updateMatchChan = nil
  8454  		trade.mtx.Lock()
  8455  		newStatus := match.Status
  8456  		trade.mtx.Unlock()
  8457  		return newStatus
  8458  	}
  8459  
  8460  	// forwardResolvers are recoverable status combos where the server is ahead
  8461  	// of us.
  8462  	forwardResolvers := []*test{
  8463  		{
  8464  			ours:               order.NewlyMatched,
  8465  			servers:            order.MakerSwapCast,
  8466  			side:               order.Taker,
  8467  			countStatusUpdates: 2,
  8468  		},
  8469  		{
  8470  			ours:               order.MakerSwapCast,
  8471  			servers:            order.TakerSwapCast,
  8472  			side:               order.Maker,
  8473  			countStatusUpdates: 2,
  8474  		},
  8475  		{
  8476  			ours:    order.TakerSwapCast,
  8477  			servers: order.MakerRedeemed,
  8478  			side:    order.Taker,
  8479  		},
  8480  		{
  8481  			ours:    order.MakerRedeemed,
  8482  			servers: order.MatchComplete,
  8483  			side:    order.Maker,
  8484  		},
  8485  	}
  8486  
  8487  	// Check that all of the forwardResolvers update the match status.
  8488  	for _, tt := range forwardResolvers {
  8489  		newStatus := runTest(tt)
  8490  		if newStatus == tt.ours {
  8491  			t.Fatalf("(%s) status not updated for forward resolution path", testName(tt))
  8492  		}
  8493  		if match.MetaData.Proof.SelfRevoked {
  8494  			t.Fatalf("(%s) match self-revoked during forward resolution", testName(tt))
  8495  		}
  8496  	}
  8497  
  8498  	// backwardsResolvers are recoverable status mismatches where we are ahead
  8499  	// of the server but can be resolved by deferring to resendPendingRequests.
  8500  	backWardsResolvers := []*test{
  8501  		{
  8502  			ours:    order.MakerSwapCast,
  8503  			servers: order.NewlyMatched,
  8504  			side:    order.Maker,
  8505  		},
  8506  		{
  8507  			ours:    order.TakerSwapCast,
  8508  			servers: order.MakerSwapCast,
  8509  			side:    order.Taker,
  8510  		},
  8511  		{
  8512  			ours:    order.MakerRedeemed,
  8513  			servers: order.TakerSwapCast,
  8514  			side:    order.Maker,
  8515  		},
  8516  		{
  8517  			ours:    order.MatchComplete,
  8518  			servers: order.MakerRedeemed,
  8519  			side:    order.Taker,
  8520  		},
  8521  	}
  8522  
  8523  	// Backwards resolvers won't update the match status, but also won't revoke
  8524  	// the match.
  8525  	for _, tt := range backWardsResolvers {
  8526  		newStatus := runTest(tt)
  8527  		if newStatus != tt.ours {
  8528  			t.Fatalf("(%s) status changed for backwards resolution path", testName(tt))
  8529  		}
  8530  		if match.MetaData.Proof.SelfRevoked {
  8531  			t.Fatalf("(%s) match self-revoked during backwards resolution", testName(tt))
  8532  		}
  8533  	}
  8534  
  8535  	// nonsense are status combos that make no sense, so should always result
  8536  	// in a self-revocation.
  8537  	nonsense := []*test{
  8538  		{ // Server has our info before us
  8539  			ours:    order.NewlyMatched,
  8540  			servers: order.MakerSwapCast,
  8541  			side:    order.Maker,
  8542  		},
  8543  		{ // Two steps apart
  8544  			ours:    order.NewlyMatched,
  8545  			servers: order.TakerSwapCast,
  8546  			side:    order.Maker,
  8547  		},
  8548  		{ // Server didn't send contract
  8549  			ours:    order.NewlyMatched,
  8550  			servers: order.MakerSwapCast,
  8551  			side:    order.Taker,
  8552  			tweaker: func() {
  8553  				tMatchResults.MakerContract = nil
  8554  			},
  8555  		},
  8556  		{ // Server didn't send coin ID.
  8557  			ours:    order.NewlyMatched,
  8558  			servers: order.MakerSwapCast,
  8559  			side:    order.Taker,
  8560  			tweaker: func() {
  8561  				tMatchResults.MakerSwap = nil
  8562  			},
  8563  		},
  8564  		{ // Audit failed.
  8565  			ours:    order.NewlyMatched,
  8566  			servers: order.MakerSwapCast,
  8567  			side:    order.Taker,
  8568  			tweaker: func() {
  8569  				auditInfo.Expiration = matchTime
  8570  			},
  8571  			countStatusUpdates: 2, // async auditContract -> revoke and db update
  8572  		},
  8573  		{ // Server has our info before us
  8574  			ours:    order.MakerSwapCast,
  8575  			servers: order.TakerSwapCast,
  8576  			side:    order.Taker,
  8577  		},
  8578  		{ // Server has our info before us
  8579  			ours:    order.MakerSwapCast,
  8580  			servers: order.TakerSwapCast,
  8581  			side:    order.Taker,
  8582  		},
  8583  		{ // Server didn't send contract
  8584  			ours:    order.MakerSwapCast,
  8585  			servers: order.TakerSwapCast,
  8586  			side:    order.Maker,
  8587  			tweaker: func() {
  8588  				tMatchResults.TakerContract = nil
  8589  			},
  8590  		},
  8591  		{ // Server didn't send coin ID.
  8592  			ours:    order.MakerSwapCast,
  8593  			servers: order.TakerSwapCast,
  8594  			side:    order.Maker,
  8595  			tweaker: func() {
  8596  				tMatchResults.TakerSwap = nil
  8597  			},
  8598  		},
  8599  		{ // Audit failed.
  8600  			ours:    order.MakerSwapCast,
  8601  			servers: order.TakerSwapCast,
  8602  			side:    order.Maker,
  8603  			tweaker: func() {
  8604  				auditInfo.Expiration = matchTime
  8605  			},
  8606  			countStatusUpdates: 2, // async auditContract -> revoke and db update
  8607  		},
  8608  		{ // Taker has counter-party info the server doesn't.
  8609  			ours:    order.MakerSwapCast,
  8610  			servers: order.NewlyMatched,
  8611  			side:    order.Taker,
  8612  		},
  8613  		{ // Maker has a server ack, but they say they don't have the data.
  8614  			ours:    order.MakerSwapCast,
  8615  			servers: order.NewlyMatched,
  8616  			side:    order.Maker,
  8617  			tweaker: func() {
  8618  				match.MetaData.Proof.Auth.InitSig = tBytes
  8619  			},
  8620  		},
  8621  		{ // Maker has counter-party info the server doesn't.
  8622  			ours:    order.TakerSwapCast,
  8623  			servers: order.MakerSwapCast,
  8624  			side:    order.Maker,
  8625  		},
  8626  		{ // Taker has a server ack, but they say they don't have the data.
  8627  			ours:    order.TakerSwapCast,
  8628  			servers: order.MakerSwapCast,
  8629  			side:    order.Taker,
  8630  			tweaker: func() {
  8631  				match.MetaData.Proof.Auth.InitSig = tBytes
  8632  			},
  8633  		},
  8634  		{ // Server has redeem info before us.
  8635  			ours:    order.TakerSwapCast,
  8636  			servers: order.MakerRedeemed,
  8637  			side:    order.Maker,
  8638  		},
  8639  		{ // Server didn't provide redemption coin ID.
  8640  			ours:    order.TakerSwapCast,
  8641  			servers: order.MakerRedeemed,
  8642  			side:    order.Taker,
  8643  			tweaker: func() {
  8644  				tMatchResults.MakerRedeem = nil
  8645  			},
  8646  		},
  8647  		{ // Server didn't provide secret.
  8648  			ours:    order.TakerSwapCast,
  8649  			servers: order.MakerRedeemed,
  8650  			side:    order.Taker,
  8651  			tweaker: func() {
  8652  				tMatchResults.Secret = nil
  8653  			},
  8654  		},
  8655  		{ // Server has our redemption data before us.
  8656  			ours:    order.MakerRedeemed,
  8657  			servers: order.MatchComplete,
  8658  			side:    order.Taker,
  8659  		},
  8660  		{ // We have data before the server.
  8661  			ours:    order.MakerRedeemed,
  8662  			servers: order.TakerSwapCast,
  8663  			side:    order.Taker,
  8664  		},
  8665  		{ // We have a server ack, but they say they don't have the data.
  8666  			ours:    order.MakerRedeemed,
  8667  			servers: order.TakerSwapCast,
  8668  			side:    order.Maker,
  8669  			tweaker: func() {
  8670  				match.MetaData.Proof.Auth.RedeemSig = tBytes
  8671  			},
  8672  		},
  8673  		{ // We have data before the server.
  8674  			ours:    order.MatchComplete,
  8675  			servers: order.MakerSwapCast,
  8676  			side:    order.Maker,
  8677  		},
  8678  		{ // We have a server ack, but they say they don't have the data.
  8679  			ours:    order.MatchComplete,
  8680  			servers: order.MakerSwapCast,
  8681  			side:    order.Taker,
  8682  			tweaker: func() {
  8683  				match.MetaData.Proof.Auth.RedeemSig = tBytes
  8684  			},
  8685  		},
  8686  	}
  8687  
  8688  	for _, tt := range nonsense {
  8689  		runTest(tt)
  8690  		if !match.MetaData.Proof.SelfRevoked {
  8691  			t.Fatalf("(%s) match not self-revoked during nonsense resolution", testName(tt))
  8692  		}
  8693  	}
  8694  
  8695  	// Run two matches for the same order.
  8696  	match2ID := ordertest.RandomMatchID()
  8697  	match2 := &matchTracker{
  8698  		MetaMatch: db.MetaMatch{
  8699  			MetaData: &db.MatchMetaData{},
  8700  			UserMatch: &order.UserMatch{
  8701  				MatchID: match2ID,
  8702  				Address: addr,
  8703  			},
  8704  		},
  8705  	}
  8706  	trade.matches[match2ID] = match2
  8707  	setAuthSigs(order.NewlyMatched)
  8708  	setProof(order.NewlyMatched)
  8709  	match2.Side = order.Taker
  8710  	match2.MetaData.Proof = match.MetaData.Proof
  8711  
  8712  	srvMatches := connectMatches(order.MakerSwapCast)
  8713  	srvMatches = append(srvMatches, &msgjson.Match{OrderID: oid[:],
  8714  		MatchID: match2ID[:],
  8715  		Status:  uint8(order.MakerSwapCast),
  8716  		Side:    uint8(order.Taker),
  8717  	})
  8718  
  8719  	res1 := setMatchResults(order.MakerSwapCast)
  8720  	res2 := setMatchResults(order.MakerSwapCast)
  8721  	res2.MatchID = match2ID[:]
  8722  
  8723  	rig.queueConnect(nil, srvMatches, nil)
  8724  	rig.ws.queueResponse(msgjson.MatchStatusRoute, func(msg *msgjson.Message, f msgFunc) error {
  8725  		resp, _ := msgjson.NewResponse(msg.ID, []*msgjson.MatchStatusResult{res1, res2}, nil)
  8726  		f(resp)
  8727  		return nil
  8728  	})
  8729  	// 2 matches resolved via contract audit: 2 synchronous updates, 2 async
  8730  	rig.db.updateMatchChan = make(chan order.MatchStatus, 4)
  8731  	tCore.authDEX(dc)
  8732  	for i := 0; i < 4; i++ {
  8733  		<-rig.db.updateMatchChan
  8734  	}
  8735  	trade.mtx.Lock()
  8736  	newStatus1 := match.Status
  8737  	newStatus2 := match2.Status
  8738  	trade.mtx.Unlock()
  8739  	if newStatus1 != order.MakerSwapCast {
  8740  		t.Fatalf("wrong status for match 1: %s", newStatus1)
  8741  	}
  8742  	if newStatus2 != order.MakerSwapCast {
  8743  		t.Fatalf("wrong status for match 2: %s", newStatus2)
  8744  	}
  8745  }
  8746  
  8747  func TestConfirmRedemption(t *testing.T) {
  8748  	rig := newTestRig()
  8749  	defer rig.shutdown()
  8750  	dc := rig.dc
  8751  	tCore := rig.core
  8752  
  8753  	dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID)
  8754  	tCore.wallets[tUTXOAssetA.ID] = dcrWallet
  8755  	btcWallet, tBtcWallet := newTWallet(tUTXOAssetB.ID)
  8756  	tCore.wallets[tUTXOAssetB.ID] = btcWallet
  8757  	walletSet, _, _, _ := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, true)
  8758  
  8759  	lo, dbOrder, preImg, addr := makeLimitOrder(dc, true, 0, 0)
  8760  	oid := lo.ID()
  8761  	tracker := newTrackedTrade(dbOrder, preImg, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker,
  8762  		rig.db, rig.queue, walletSet, nil, rig.core.notify, rig.core.formatDetails)
  8763  	dc.trades[oid] = tracker
  8764  
  8765  	tBytes := encode.RandomBytes(2)
  8766  	tCoinID := encode.RandomBytes(36)
  8767  	tUpdatedCoinID := encode.RandomBytes(36)
  8768  	secret := encode.RandomBytes(32)
  8769  	secretHash := sha256.Sum256(secret)
  8770  
  8771  	var match *matchTracker
  8772  
  8773  	tBtcWallet.redeemCoins = []dex.Bytes{tUpdatedCoinID}
  8774  
  8775  	ourContract := encode.RandomBytes(90)
  8776  	setupMatch := func(status order.MatchStatus, side order.MatchSide) {
  8777  		matchID := ordertest.RandomMatchID()
  8778  		_, auditInfo := tMsgAudit(oid, matchID, addr, 0, secretHash[:])
  8779  		matchTime := time.Now()
  8780  		match = &matchTracker{
  8781  			counterSwap: auditInfo,
  8782  			MetaMatch: db.MetaMatch{
  8783  				MetaData: &db.MatchMetaData{},
  8784  				UserMatch: &order.UserMatch{
  8785  					MatchID: matchID,
  8786  					Address: addr,
  8787  					Side:    side,
  8788  					Status:  status,
  8789  				},
  8790  			},
  8791  		}
  8792  		tracker.matches = map[order.MatchID]*matchTracker{matchID: match}
  8793  
  8794  		isMaker := match.Side == order.Maker
  8795  		proof := &match.MetaData.Proof
  8796  		proof.Auth.InitSig = []byte{1, 2, 3, 4}
  8797  		// Assume our redeem was accepted, if we sent one.
  8798  		if isMaker {
  8799  			auditInfo.Expiration = matchTime.Add(tracker.lockTimeTaker)
  8800  			if status >= order.MakerRedeemed {
  8801  				match.MetaData.Proof.Auth.RedeemSig = []byte{0}
  8802  			}
  8803  		} else {
  8804  			auditInfo.Expiration = matchTime.Add(tracker.lockTimeMaker)
  8805  			if status >= order.MatchComplete {
  8806  				match.MetaData.Proof.Auth.RedeemSig = []byte{0}
  8807  			}
  8808  		}
  8809  
  8810  		if status >= order.MakerSwapCast {
  8811  			proof.MakerSwap = tCoinID
  8812  			proof.SecretHash = secretHash[:]
  8813  			if isMaker {
  8814  				proof.ContractData = ourContract
  8815  				proof.Secret = secret
  8816  			} else {
  8817  				proof.CounterContract = tBytes
  8818  			}
  8819  		}
  8820  		if status >= order.TakerSwapCast {
  8821  			proof.TakerSwap = tCoinID
  8822  			if isMaker {
  8823  				proof.CounterContract = tBytes
  8824  			} else {
  8825  				proof.ContractData = ourContract
  8826  			}
  8827  		}
  8828  		if status >= order.MakerRedeemed {
  8829  			proof.MakerRedeem = tCoinID
  8830  			if !isMaker {
  8831  				proof.Secret = secret
  8832  			}
  8833  		}
  8834  		if status >= order.MatchComplete {
  8835  			proof.TakerRedeem = tCoinID
  8836  		}
  8837  	}
  8838  
  8839  	type note struct {
  8840  		severity db.Severity
  8841  		topic    db.Topic
  8842  	}
  8843  
  8844  	tests := []struct {
  8845  		name                    string
  8846  		matchStatus             order.MatchStatus
  8847  		matchSide               order.MatchSide
  8848  		expectedNotifications   []*note
  8849  		confirmRedemptionResult *asset.ConfirmRedemptionStatus
  8850  		confirmRedemptionErr    error
  8851  
  8852  		expectConfirmRedemptionCalled bool
  8853  		expectedStatus                order.MatchStatus
  8854  		expectTicksDelayed            bool
  8855  	}{
  8856  		{
  8857  			name:        "maker, makerRedeemed, confirmedRedemption",
  8858  			matchStatus: order.MakerRedeemed,
  8859  			matchSide:   order.Maker,
  8860  			expectedNotifications: []*note{
  8861  				{
  8862  					severity: db.Success,
  8863  					topic:    TopicRedemptionConfirmed,
  8864  				},
  8865  			},
  8866  			confirmRedemptionResult: &asset.ConfirmRedemptionStatus{
  8867  				Confs:  10,
  8868  				Req:    10,
  8869  				CoinID: tCoinID,
  8870  			},
  8871  			expectConfirmRedemptionCalled: true,
  8872  			expectedStatus:                order.MatchConfirmed,
  8873  		},
  8874  		{
  8875  			name:        "maker, makerRedeemed, confirmedRedemption, more confs than required",
  8876  			matchStatus: order.MakerRedeemed,
  8877  			matchSide:   order.Maker,
  8878  			expectedNotifications: []*note{
  8879  				{
  8880  					severity: db.Success,
  8881  					topic:    TopicRedemptionConfirmed,
  8882  				},
  8883  			},
  8884  			confirmRedemptionResult: &asset.ConfirmRedemptionStatus{
  8885  				Confs:  15,
  8886  				Req:    10,
  8887  				CoinID: tCoinID,
  8888  			},
  8889  			expectConfirmRedemptionCalled: true,
  8890  			expectedStatus:                order.MatchConfirmed,
  8891  		},
  8892  		{
  8893  			name:        "taker, matchComplete, confirmedRedemption",
  8894  			matchStatus: order.MatchComplete,
  8895  			matchSide:   order.Taker,
  8896  			expectedNotifications: []*note{
  8897  				{
  8898  					severity: db.Success,
  8899  					topic:    TopicRedemptionConfirmed,
  8900  				},
  8901  			},
  8902  			confirmRedemptionResult: &asset.ConfirmRedemptionStatus{
  8903  				Confs:  10,
  8904  				Req:    10,
  8905  				CoinID: tCoinID,
  8906  			},
  8907  			expectConfirmRedemptionCalled: true,
  8908  			expectedStatus:                order.MatchConfirmed,
  8909  		},
  8910  		{
  8911  			name:        "maker, makerRedeemed, incomplete",
  8912  			matchStatus: order.MakerRedeemed,
  8913  			matchSide:   order.Maker,
  8914  			expectedNotifications: []*note{
  8915  				{
  8916  					severity: db.Data,
  8917  					topic:    TopicConfirms,
  8918  				},
  8919  			},
  8920  			confirmRedemptionResult: &asset.ConfirmRedemptionStatus{
  8921  				Confs:  5,
  8922  				Req:    10,
  8923  				CoinID: tCoinID,
  8924  			},
  8925  			expectConfirmRedemptionCalled: true,
  8926  			expectedStatus:                order.MakerRedeemed,
  8927  		},
  8928  		{
  8929  			name:        "maker, makerRedeemed, replacedTx",
  8930  			matchStatus: order.MakerRedeemed,
  8931  			matchSide:   order.Maker,
  8932  			expectedNotifications: []*note{
  8933  				{
  8934  					severity: db.WarningLevel,
  8935  					topic:    TopicRedemptionResubmitted,
  8936  				},
  8937  				{
  8938  					severity: db.Data,
  8939  					topic:    TopicConfirms,
  8940  				},
  8941  			},
  8942  			confirmRedemptionResult: &asset.ConfirmRedemptionStatus{
  8943  				Confs:  0,
  8944  				Req:    10,
  8945  				CoinID: tUpdatedCoinID,
  8946  			},
  8947  			expectConfirmRedemptionCalled: true,
  8948  			expectedStatus:                order.MakerRedeemed,
  8949  		},
  8950  		{
  8951  			name:        "taker, matchComplete, replacedTx",
  8952  			matchStatus: order.MatchComplete,
  8953  			matchSide:   order.Taker,
  8954  			expectedNotifications: []*note{
  8955  				{
  8956  					severity: db.WarningLevel,
  8957  					topic:    TopicRedemptionResubmitted,
  8958  				},
  8959  				{
  8960  					severity: db.Data,
  8961  					topic:    TopicConfirms,
  8962  				},
  8963  			},
  8964  			confirmRedemptionResult: &asset.ConfirmRedemptionStatus{
  8965  				Confs:  0,
  8966  				Req:    10,
  8967  				CoinID: tUpdatedCoinID,
  8968  			},
  8969  			expectConfirmRedemptionCalled: true,
  8970  			expectedStatus:                order.MatchComplete,
  8971  		},
  8972  		{
  8973  			// This case could happen if the dex was shut down right after
  8974  			// a resubmission.
  8975  			name:        "taker, matchComplete, replacedTx and already confirmed",
  8976  			matchStatus: order.MatchComplete,
  8977  			matchSide:   order.Taker,
  8978  			expectedNotifications: []*note{
  8979  				{
  8980  					severity: db.WarningLevel,
  8981  					topic:    TopicRedemptionResubmitted,
  8982  				},
  8983  				{
  8984  					severity: db.Success,
  8985  					topic:    TopicRedemptionConfirmed,
  8986  				},
  8987  			},
  8988  			confirmRedemptionResult: &asset.ConfirmRedemptionStatus{
  8989  				Confs:  10,
  8990  				Req:    10,
  8991  				CoinID: tUpdatedCoinID,
  8992  			},
  8993  			expectConfirmRedemptionCalled: true,
  8994  			expectedStatus:                order.MatchConfirmed,
  8995  		},
  8996  		{
  8997  			name:                          "maker, makerRedeemed, error",
  8998  			matchStatus:                   order.MakerRedeemed,
  8999  			matchSide:                     order.Maker,
  9000  			confirmRedemptionErr:          errors.New("err"),
  9001  			expectedStatus:                order.MakerRedeemed,
  9002  			expectTicksDelayed:            true,
  9003  			expectConfirmRedemptionCalled: true,
  9004  		},
  9005  		{
  9006  			name:                 "maker, makerRedeemed, swap refunded error",
  9007  			matchStatus:          order.MakerRedeemed,
  9008  			matchSide:            order.Maker,
  9009  			confirmRedemptionErr: asset.ErrSwapRefunded,
  9010  			expectedStatus:       order.MatchConfirmed,
  9011  			expectedNotifications: []*note{
  9012  				{
  9013  					severity: db.ErrorLevel,
  9014  					topic:    TopicSwapRefunded,
  9015  				},
  9016  			},
  9017  			expectConfirmRedemptionCalled: true,
  9018  		},
  9019  		{
  9020  			name:                 "taker, takerRedeemed, redemption tx rejected error",
  9021  			matchStatus:          order.MatchComplete,
  9022  			matchSide:            order.Taker,
  9023  			confirmRedemptionErr: asset.ErrTxRejected,
  9024  			expectedStatus:       order.MatchComplete,
  9025  			expectedNotifications: []*note{
  9026  				{
  9027  					severity: db.Data,
  9028  					topic:    TopicRedeemRejected,
  9029  				},
  9030  			},
  9031  			expectConfirmRedemptionCalled: true,
  9032  		},
  9033  		{
  9034  			name:                          "maker, makerRedeemed, redemption tx lost",
  9035  			matchStatus:                   order.MakerRedeemed,
  9036  			matchSide:                     order.Maker,
  9037  			confirmRedemptionErr:          asset.ErrTxLost,
  9038  			expectedStatus:                order.TakerSwapCast,
  9039  			expectConfirmRedemptionCalled: true,
  9040  		},
  9041  		{
  9042  			name:                          "taker, takerRedeemed, redemption tx lost",
  9043  			matchStatus:                   order.MatchComplete,
  9044  			matchSide:                     order.Taker,
  9045  			confirmRedemptionErr:          asset.ErrTxLost,
  9046  			expectedStatus:                order.MakerRedeemed,
  9047  			expectConfirmRedemptionCalled: true,
  9048  		},
  9049  		{
  9050  			name:                          "maker, matchConfirmed",
  9051  			matchStatus:                   order.MatchConfirmed,
  9052  			matchSide:                     order.Maker,
  9053  			expectedStatus:                order.MatchConfirmed,
  9054  			expectedNotifications:         []*note{},
  9055  			expectConfirmRedemptionCalled: false,
  9056  		},
  9057  		{
  9058  			name:                          "maker, TakerSwapCast",
  9059  			matchStatus:                   order.TakerSwapCast,
  9060  			matchSide:                     order.Maker,
  9061  			expectedStatus:                order.TakerSwapCast,
  9062  			expectedNotifications:         []*note{},
  9063  			expectConfirmRedemptionCalled: false,
  9064  		},
  9065  		{
  9066  			name:                          "taker, TakerSwapCast",
  9067  			matchStatus:                   order.TakerSwapCast,
  9068  			matchSide:                     order.Taker,
  9069  			expectedStatus:                order.TakerSwapCast,
  9070  			expectedNotifications:         []*note{},
  9071  			expectConfirmRedemptionCalled: false,
  9072  		},
  9073  	}
  9074  
  9075  	notificationFeed := tCore.NotificationFeed()
  9076  
  9077  	for _, test := range tests {
  9078  		tracker.mtx.Lock()
  9079  		setupMatch(test.matchStatus, test.matchSide)
  9080  		tracker.mtx.Unlock()
  9081  
  9082  		tBtcWallet.confirmRedemptionResult = test.confirmRedemptionResult
  9083  		tBtcWallet.confirmRedemptionErr = test.confirmRedemptionErr
  9084  		tBtcWallet.confirmRedemptionCalled = false
  9085  
  9086  		tCore.tickAsset(dc, tUTXOAssetB.ID)
  9087  
  9088  		if tBtcWallet.confirmRedemptionCalled != test.expectConfirmRedemptionCalled {
  9089  			t.Fatalf("%s: expected confirm redemption to be called=%v but got=%v",
  9090  				test.name, test.expectConfirmRedemptionCalled, tBtcWallet.confirmRedemptionCalled)
  9091  		}
  9092  
  9093  		for _, expectedNotification := range test.expectedNotifications {
  9094  			var n Notification
  9095  		out:
  9096  			for {
  9097  				select {
  9098  				case n = <-notificationFeed.C:
  9099  					if n.Topic() == expectedNotification.topic {
  9100  						break out
  9101  					}
  9102  				case <-time.After(60 * time.Second):
  9103  					t.Fatalf("%s: did not receive expected notification", test.name)
  9104  				}
  9105  			}
  9106  
  9107  			if n.Severity() != expectedNotification.severity {
  9108  				t.Fatalf("%s: expected severity %v, got %v",
  9109  					test.name, expectedNotification.severity, n.Severity())
  9110  			}
  9111  		}
  9112  
  9113  		tracker.mtx.RLock()
  9114  		if test.confirmRedemptionResult != nil {
  9115  			var redeemCoin order.CoinID
  9116  			if test.matchSide == order.Maker {
  9117  				redeemCoin = match.MetaData.Proof.MakerRedeem
  9118  			} else {
  9119  				redeemCoin = match.MetaData.Proof.TakerRedeem
  9120  			}
  9121  			if !bytes.Equal(redeemCoin, test.confirmRedemptionResult.CoinID) {
  9122  				t.Fatalf("%s: expected coin %v != actual %v", test.name, test.confirmRedemptionResult.CoinID, redeemCoin)
  9123  			}
  9124  			if test.confirmRedemptionResult.Confs >= test.confirmRedemptionResult.Req {
  9125  				if len(tDcrWallet.returnedContracts) != 1 || !bytes.Equal(ourContract, tDcrWallet.returnedContracts[0]) {
  9126  					t.Fatalf("%s: refund address not returned", test.name)
  9127  				}
  9128  			}
  9129  		}
  9130  
  9131  		ticksDelayed := match.tickGovernor != nil
  9132  		if ticksDelayed != test.expectTicksDelayed {
  9133  			t.Fatalf("%s: expected ticks delayed %v but got %v", test.name, test.expectTicksDelayed, ticksDelayed)
  9134  		}
  9135  
  9136  		if match.Status != test.expectedStatus {
  9137  			t.Fatalf("%s: expected status %v but got %v", test.name, test.expectedStatus, match.Status)
  9138  		}
  9139  		tracker.mtx.RUnlock()
  9140  	}
  9141  }
  9142  
  9143  func TestMaxSwapsRedeemsInTx(t *testing.T) {
  9144  	rig := newTestRig()
  9145  	defer rig.shutdown()
  9146  	dc := rig.dc
  9147  	tCore := rig.core
  9148  
  9149  	dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID)
  9150  	tCore.wallets[tUTXOAssetA.ID] = dcrWallet
  9151  	btcWallet, tBtcWallet := newTWallet(tUTXOAssetB.ID)
  9152  	tCore.wallets[tUTXOAssetB.ID] = btcWallet
  9153  	walletSet, _, _, _ := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, true)
  9154  
  9155  	tDcrWallet.info.MaxSwapsInTx = 4
  9156  	tBtcWallet.info.MaxRedeemsInTx = 4
  9157  
  9158  	lo, dbOrder, preImg, _ := makeLimitOrder(dc, true, 0, 0)
  9159  	oid := lo.ID()
  9160  	tracker := newTrackedTrade(dbOrder, preImg, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker,
  9161  		rig.db, rig.queue, walletSet, nil, rig.core.notify, rig.core.formatDetails)
  9162  	dc.trades[oid] = tracker
  9163  
  9164  	newMatch := func(side order.MatchSide, status order.MatchStatus) *matchTracker {
  9165  		return &matchTracker{
  9166  			prefix: lo.Prefix(),
  9167  			trade:  lo.Trade(),
  9168  			MetaMatch: db.MetaMatch{
  9169  				MetaData: &db.MatchMetaData{
  9170  					Proof: db.MatchProof{
  9171  						Auth: db.MatchAuth{
  9172  							MatchStamp: uint64(time.Now().UnixMilli()),
  9173  							AuditStamp: uint64(time.Now().UnixMilli()),
  9174  						},
  9175  					},
  9176  				},
  9177  				UserMatch: &order.UserMatch{
  9178  					MatchID:     ordertest.RandomMatchID(),
  9179  					Side:        side,
  9180  					Address:     ordertest.RandomAddress(),
  9181  					Status:      status,
  9182  					FeeRateSwap: tMaxFeeRate,
  9183  				},
  9184  			},
  9185  		}
  9186  	}
  9187  
  9188  	swapabbleMatches := func(num int) map[order.MatchID]*matchTracker {
  9189  		matches := make(map[order.MatchID]*matchTracker, num)
  9190  		for i := 0; i < num; i++ {
  9191  			m := newMatch(order.Maker, order.NewlyMatched)
  9192  			matches[m.MatchID] = m
  9193  		}
  9194  		return matches
  9195  	}
  9196  
  9197  	redeemableMatches := func(num int) map[order.MatchID]*matchTracker {
  9198  		matches := make(map[order.MatchID]*matchTracker, num)
  9199  		for i := 0; i < num; i++ {
  9200  			m := newMatch(order.Taker, order.MakerRedeemed)
  9201  			matches[m.MatchID] = m
  9202  			rig.ws.queueResponse(msgjson.RedeemRoute, redeemAcker)
  9203  		}
  9204  		return matches
  9205  	}
  9206  
  9207  	checkNumSwaps := func(expected []int, wallet *TXCWallet) {
  9208  		t.Helper()
  9209  		for i := range expected {
  9210  			if expected[i] != len(wallet.lastSwaps[i].Contracts) {
  9211  				t.Fatalf("expected %d swaps but got %d", expected[i], len(wallet.lastSwaps[i].Contracts))
  9212  			}
  9213  		}
  9214  	}
  9215  
  9216  	checkNumRedeems := func(expected []int, wallet *TXCWallet) {
  9217  		t.Helper()
  9218  		for i := range expected {
  9219  			if expected[i] != len(wallet.lastRedeems[i].Redemptions) {
  9220  				t.Fatalf("expected %d swaps but got %d", expected[i], len(wallet.lastRedeems[i].Redemptions))
  9221  			}
  9222  		}
  9223  	}
  9224  
  9225  	populateRedeemCoins := func(num int, wallet *TXCWallet) {
  9226  		wallet.redeemCoins = make([]dex.Bytes, num)
  9227  		for i := 0; i < num; i++ {
  9228  			wallet.redeemCoins = append(wallet.redeemCoins, encode.RandomBytes(32))
  9229  		}
  9230  	}
  9231  
  9232  	// Test Swaps
  9233  	expected := []int{4, 4, 4, 4, 4, 2}
  9234  	tracker.matches = swapabbleMatches(22)
  9235  	tCore.tick(tracker)
  9236  	checkNumSwaps(expected, tDcrWallet)
  9237  
  9238  	tDcrWallet.lastSwaps = make([]*asset.Swaps, 0)
  9239  	expected = []int{3}
  9240  	tracker.matches = swapabbleMatches(3)
  9241  	tCore.tick(tracker)
  9242  	checkNumSwaps(expected, tDcrWallet)
  9243  
  9244  	tDcrWallet.lastSwaps = make([]*asset.Swaps, 0)
  9245  	expected = []int{4}
  9246  	tracker.matches = swapabbleMatches(4)
  9247  	tCore.tick(tracker)
  9248  	checkNumSwaps(expected, tDcrWallet)
  9249  
  9250  	// Test Redeems
  9251  	expected = []int{4, 4, 4, 4, 4, 2}
  9252  	tracker.matches = redeemableMatches(22)
  9253  	populateRedeemCoins(22, tBtcWallet)
  9254  	tCore.tick(tracker)
  9255  	checkNumRedeems(expected, tBtcWallet)
  9256  
  9257  	tBtcWallet.lastRedeems = make([]*asset.RedeemForm, 0)
  9258  	expected = []int{3}
  9259  	tracker.matches = redeemableMatches(3)
  9260  	populateRedeemCoins(3, tBtcWallet)
  9261  	tCore.tick(tracker)
  9262  	checkNumRedeems(expected, tBtcWallet)
  9263  
  9264  	tBtcWallet.lastRedeems = make([]*asset.RedeemForm, 0)
  9265  	expected = []int{4}
  9266  	tracker.matches = redeemableMatches(4)
  9267  	populateRedeemCoins(4, tBtcWallet)
  9268  	tCore.tick(tracker)
  9269  	checkNumRedeems(expected, tBtcWallet)
  9270  }
  9271  
  9272  func TestSuspectTrades(t *testing.T) {
  9273  	rig := newTestRig()
  9274  	defer rig.shutdown()
  9275  	dc := rig.dc
  9276  	tCore := rig.core
  9277  
  9278  	dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID)
  9279  	tCore.wallets[tUTXOAssetA.ID] = dcrWallet
  9280  	btcWallet, tBtcWallet := newTWallet(tUTXOAssetB.ID)
  9281  	tCore.wallets[tUTXOAssetB.ID] = btcWallet
  9282  	walletSet, _, _, _ := tCore.walletSet(dc, tUTXOAssetA.ID, tUTXOAssetB.ID, true)
  9283  
  9284  	lo, dbOrder, preImg, addr := makeLimitOrder(dc, true, 0, 0)
  9285  	oid := lo.ID()
  9286  	tracker := newTrackedTrade(dbOrder, preImg, dc, rig.core.lockTimeTaker, rig.core.lockTimeMaker,
  9287  		rig.db, rig.queue, walletSet, nil, rig.core.notify, rig.core.formatDetails)
  9288  	dc.trades[oid] = tracker
  9289  
  9290  	newMatch := func(side order.MatchSide, status order.MatchStatus) *matchTracker {
  9291  		return &matchTracker{
  9292  			prefix: lo.Prefix(),
  9293  			trade:  lo.Trade(),
  9294  			MetaMatch: db.MetaMatch{
  9295  				MetaData: &db.MatchMetaData{
  9296  					Proof: db.MatchProof{
  9297  						Auth: db.MatchAuth{
  9298  							MatchStamp: uint64(time.Now().UnixMilli()),
  9299  							AuditStamp: uint64(time.Now().UnixMilli()),
  9300  						},
  9301  					},
  9302  				},
  9303  				UserMatch: &order.UserMatch{
  9304  					MatchID:     ordertest.RandomMatchID(),
  9305  					Side:        side,
  9306  					Address:     ordertest.RandomAddress(),
  9307  					Status:      status,
  9308  					FeeRateSwap: tMaxFeeRate,
  9309  				},
  9310  			},
  9311  		}
  9312  	}
  9313  
  9314  	var swappableMatch1, swappableMatch2 *matchTracker
  9315  	setSwaps := func() {
  9316  		swappableMatch1 = newMatch(order.Maker, order.NewlyMatched)
  9317  		swappableMatch2 = newMatch(order.Taker, order.MakerSwapCast)
  9318  
  9319  		// Set counterswaps for both swaps.
  9320  		// Set valid wallet auditInfo for swappableMatch2, taker will repeat audit before swapping.
  9321  		auditQty := calc.BaseToQuote(swappableMatch2.Rate, swappableMatch2.Quantity)
  9322  		_, auditInfo := tMsgAudit(oid, swappableMatch2.MatchID, addr, auditQty, encode.RandomBytes(32))
  9323  		auditInfo.Expiration = encode.DropMilliseconds(swappableMatch2.matchTime().Add(tracker.lockTimeMaker))
  9324  		tBtcWallet.setConfs(auditInfo.Coin.ID(), tUTXOAssetA.SwapConf, nil)
  9325  		tBtcWallet.auditInfo = auditInfo
  9326  		swappableMatch2.counterSwap = auditInfo
  9327  
  9328  		_, auditInfo = tMsgAudit(oid, swappableMatch1.MatchID, ordertest.RandomAddress(), 1, encode.RandomBytes(32))
  9329  		tBtcWallet.setConfs(auditInfo.Coin.ID(), tUTXOAssetA.SwapConf, nil)
  9330  		swappableMatch1.counterSwap = auditInfo
  9331  
  9332  		tDcrWallet.swapCounter = 0
  9333  		tracker.matches = map[order.MatchID]*matchTracker{
  9334  			swappableMatch1.MatchID: swappableMatch1,
  9335  			swappableMatch2.MatchID: swappableMatch2,
  9336  		}
  9337  	}
  9338  	setSwaps()
  9339  
  9340  	// Initial success
  9341  	_, err := tCore.tick(tracker)
  9342  	if err != nil {
  9343  		t.Fatalf("swap tick error: %v", err)
  9344  	}
  9345  
  9346  	setSwaps()
  9347  	tDcrWallet.swapErr = tErr
  9348  	_, err = tCore.tick(tracker)
  9349  	if err == nil || !strings.Contains(err.Error(), "error sending dcr swap transaction") {
  9350  		t.Fatalf("swap error not propagated, err = %v", err)
  9351  	}
  9352  	if tDcrWallet.swapCounter != 1 {
  9353  		t.Fatalf("never swapped")
  9354  	}
  9355  
  9356  	// Both matches should be marked as suspect and have tickGovernors in place.
  9357  	tracker.mtx.Lock()
  9358  	for i, m := range []*matchTracker{swappableMatch1, swappableMatch2} {
  9359  		if !m.suspectSwap {
  9360  			t.Fatalf("swappable match %d not suspect after failed swap", i+1)
  9361  		}
  9362  		if m.tickGovernor == nil {
  9363  			t.Fatalf("swappable match %d has no tick meterer set", i+1)
  9364  		}
  9365  	}
  9366  	tracker.mtx.Unlock()
  9367  
  9368  	// Ticking right away again should do nothing.
  9369  	tDcrWallet.swapErr = nil
  9370  	_, err = tCore.tick(tracker)
  9371  	if err != nil {
  9372  		t.Fatalf("tick error during metered swap tick: %v", err)
  9373  	}
  9374  	if tDcrWallet.swapCounter != 1 {
  9375  		t.Fatalf("swapped during metered tick")
  9376  	}
  9377  
  9378  	// But once the tickGovernors expire, we should succeed with two separate
  9379  	// requests.
  9380  	tracker.mtx.Lock()
  9381  	swappableMatch1.tickGovernor = nil
  9382  	swappableMatch2.tickGovernor = nil
  9383  	tracker.mtx.Unlock()
  9384  	_, err = tCore.tick(tracker)
  9385  	if err != nil {
  9386  		t.Fatalf("tick error while swapping suspect matches: %v", err)
  9387  	}
  9388  	if tDcrWallet.swapCounter != 3 {
  9389  		t.Fatalf("suspect swap matches not run or not run separately. expected 2 new calls to Swap, got %d", tDcrWallet.swapCounter-1)
  9390  	}
  9391  
  9392  	var redeemableMatch1, redeemableMatch2 *matchTracker
  9393  	setRedeems := func() {
  9394  		redeemableMatch1 = newMatch(order.Maker, order.TakerSwapCast)
  9395  		redeemableMatch2 = newMatch(order.Taker, order.MakerRedeemed)
  9396  
  9397  		// Set valid wallet auditInfo for redeemableMatch1, maker will repeat audit before redeeming.
  9398  		auditQty := calc.BaseToQuote(redeemableMatch1.Rate, redeemableMatch1.Quantity)
  9399  		_, auditInfo := tMsgAudit(oid, redeemableMatch1.MatchID, addr, auditQty, encode.RandomBytes(32))
  9400  		auditInfo.Expiration = encode.DropMilliseconds(redeemableMatch1.matchTime().Add(tracker.lockTimeTaker))
  9401  		tBtcWallet.setConfs(auditInfo.Coin.ID(), tUTXOAssetB.SwapConf, nil)
  9402  		tBtcWallet.auditInfo = auditInfo
  9403  		redeemableMatch1.counterSwap = auditInfo
  9404  		redeemableMatch1.MetaData.Proof.SecretHash = auditInfo.SecretHash
  9405  
  9406  		tBtcWallet.redeemCounter = 0
  9407  		tracker.matches = map[order.MatchID]*matchTracker{
  9408  			redeemableMatch1.MatchID: redeemableMatch1,
  9409  			redeemableMatch2.MatchID: redeemableMatch2,
  9410  		}
  9411  		rig.ws.queueResponse(msgjson.RedeemRoute, redeemAcker)
  9412  		rig.ws.queueResponse(msgjson.RedeemRoute, redeemAcker)
  9413  	}
  9414  	setRedeems()
  9415  
  9416  	// Initial success
  9417  	tBtcWallet.redeemCoins = []dex.Bytes{encode.RandomBytes(36), encode.RandomBytes(36)}
  9418  	_, err = tCore.tick(tracker)
  9419  	if err != nil {
  9420  		t.Fatalf("redeem tick error: %v", err)
  9421  	}
  9422  	if tBtcWallet.redeemCounter != 1 {
  9423  		t.Fatalf("never redeemed")
  9424  	}
  9425  
  9426  	setRedeems()
  9427  	tBtcWallet.redeemErr = tErr
  9428  	_, err = tCore.tick(tracker)
  9429  	if err == nil || !strings.Contains(err.Error(), "error sending redeem transaction") {
  9430  		t.Fatalf("redeem error not propagated. err = %v", err)
  9431  	}
  9432  	if tBtcWallet.redeemCounter != 1 {
  9433  		t.Fatalf("never redeemed")
  9434  	}
  9435  
  9436  	// Both matches should be marked as suspect and have tickGovernors in place.
  9437  	tracker.mtx.Lock()
  9438  	for i, m := range []*matchTracker{redeemableMatch1, redeemableMatch2} {
  9439  		if !m.suspectRedeem {
  9440  			t.Fatalf("redeemable match %d not suspect after failed swap", i+1)
  9441  		}
  9442  		if m.tickGovernor == nil {
  9443  			t.Fatalf("redeemable match %d has no tick meterer set", i+1)
  9444  		}
  9445  	}
  9446  	tracker.mtx.Unlock()
  9447  
  9448  	// Ticking right away again should do nothing.
  9449  	tBtcWallet.redeemErr = nil
  9450  	_, err = tCore.tick(tracker)
  9451  	if err != nil {
  9452  		t.Fatalf("tick error during metered redeem tick: %v", err)
  9453  	}
  9454  	if tBtcWallet.redeemCounter != 1 {
  9455  		t.Fatalf("redeemed during metered tick %d", tBtcWallet.redeemCounter)
  9456  	}
  9457  
  9458  	// But once the tickGovernors expire, we should succeed with two separate
  9459  	// requests.
  9460  	tracker.mtx.Lock()
  9461  	redeemableMatch1.tickGovernor = nil
  9462  	redeemableMatch2.tickGovernor = nil
  9463  	tracker.mtx.Unlock()
  9464  	_, err = tCore.tick(tracker)
  9465  	if err != nil {
  9466  		t.Fatalf("tick error while redeeming suspect matches: %v", err)
  9467  	}
  9468  	if tBtcWallet.redeemCounter != 3 {
  9469  		t.Fatalf("suspect redeem matches not run or not run separately. expected 2 new calls to Redeem, got %d", tBtcWallet.redeemCounter-1)
  9470  	}
  9471  }
  9472  
  9473  func TestWalletSyncing(t *testing.T) {
  9474  	rig := newTestRig()
  9475  	defer rig.shutdown()
  9476  	tCore := rig.core
  9477  
  9478  	noteFeed := tCore.NotificationFeed()
  9479  	dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID)
  9480  	dcrWallet.syncStatus.Synced = false
  9481  	dcrWallet.syncStatus.Blocks = 0
  9482  	dcrWallet.hookedUp = false
  9483  	// Connect with tCore.connectWallet below.
  9484  
  9485  	tStart := time.Now()
  9486  	testDuration := 100 * time.Millisecond
  9487  	syncTickerPeriod = 10 * time.Millisecond
  9488  
  9489  	tDcrWallet.syncStatus = func() (bool, float32, error) {
  9490  		progress := float32(float64(time.Since(tStart)) / float64(testDuration))
  9491  		if progress >= 1 {
  9492  			return true, 1, nil
  9493  		}
  9494  		return false, progress, nil
  9495  	}
  9496  
  9497  	_, err := tCore.connectWallet(dcrWallet)
  9498  	if err != nil {
  9499  		t.Fatalf("connectWallet error: %v", err)
  9500  	}
  9501  
  9502  	timeout := time.NewTimer(time.Second)
  9503  	defer timeout.Stop()
  9504  	var progressNotes int
  9505  out:
  9506  	for {
  9507  		select {
  9508  		case note := <-noteFeed.C:
  9509  			syncNote, ok := note.(*WalletSyncNote)
  9510  			if !ok {
  9511  				continue
  9512  			}
  9513  			progressNotes++
  9514  			if syncNote.SyncStatus.Synced {
  9515  				break out
  9516  			}
  9517  		case <-timeout.C:
  9518  			t.Fatalf("timed out waiting for synced wallet note. Received %d progress notes", progressNotes)
  9519  		}
  9520  	}
  9521  	// By the time we've got 10th note it should signal that the wallet has been
  9522  	// synced (due to how we've set up testDuration and syncTickerPeriod values).
  9523  	if progressNotes > 10 {
  9524  		t.Fatalf("expected 10 progress notes at most, got %d", progressNotes)
  9525  	}
  9526  }
  9527  
  9528  func TestParseCert(t *testing.T) {
  9529  	byteCert := []byte{0x0a, 0x0b}
  9530  	cert, err := parseCert("anyhost", []byte{0x0a, 0x0b}, dex.Mainnet)
  9531  	if err != nil {
  9532  		t.Fatalf("byte cert error: %v", err)
  9533  	}
  9534  	if !bytes.Equal(cert, byteCert) {
  9535  		t.Fatalf("byte cert note returned unmodified. expected %x, got %x", byteCert, cert)
  9536  	}
  9537  	byteCert = []byte{0x05, 0x06}
  9538  	certFile, _ := os.CreateTemp("", "dumbcert")
  9539  	defer os.Remove(certFile.Name())
  9540  	certFile.Write(byteCert)
  9541  	certFile.Close()
  9542  	cert, err = parseCert("anyhost", certFile.Name(), dex.Mainnet)
  9543  	if err != nil {
  9544  		t.Fatalf("file cert error: %v", err)
  9545  	}
  9546  	if !bytes.Equal(cert, byteCert) {
  9547  		t.Fatalf("byte cert note returned unmodified. expected %x, got %x", byteCert, cert)
  9548  	}
  9549  	_, err = parseCert("bison.exchange:17232", []byte(nil), dex.Testnet)
  9550  	if err != nil {
  9551  		t.Fatalf("CertStore cert error: %v", err)
  9552  	}
  9553  }
  9554  
  9555  func TestPreOrder(t *testing.T) {
  9556  	rig := newTestRig()
  9557  	defer rig.shutdown()
  9558  	tCore := rig.core
  9559  	dc := rig.dc
  9560  
  9561  	btcWallet, tBtcWallet := newTWallet(tUTXOAssetB.ID)
  9562  	tCore.wallets[tUTXOAssetB.ID] = btcWallet
  9563  	dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID)
  9564  	tCore.wallets[tUTXOAssetA.ID] = dcrWallet
  9565  
  9566  	var rate uint64 = 1e8
  9567  	quoteConvertedLotSize := calc.BaseToQuote(rate, dcrBtcLotSize)
  9568  
  9569  	book := newBookie(rig.dc, tUTXOAssetA.ID, tUTXOAssetB.ID, nil, tLogger)
  9570  	dc.books[tDcrBtcMktName] = book
  9571  
  9572  	sellNote := &msgjson.BookOrderNote{
  9573  		OrderNote: msgjson.OrderNote{
  9574  			OrderID: encode.RandomBytes(32),
  9575  		},
  9576  		TradeNote: msgjson.TradeNote{
  9577  			Side:     msgjson.SellOrderNum,
  9578  			Quantity: quoteConvertedLotSize * 10,
  9579  			Time:     uint64(time.Now().Unix()),
  9580  			Rate:     rate,
  9581  		},
  9582  	}
  9583  
  9584  	buyNote := *sellNote
  9585  	buyNote.TradeNote.Quantity = dcrBtcLotSize * 10
  9586  	buyNote.TradeNote.Side = msgjson.BuyOrderNum
  9587  
  9588  	var baseFeeRate uint64 = 5
  9589  	var quoteFeeRate uint64 = 10
  9590  
  9591  	err := book.Sync(&msgjson.OrderBook{
  9592  		MarketID:     tDcrBtcMktName,
  9593  		Seq:          1,
  9594  		Epoch:        1,
  9595  		Orders:       []*msgjson.BookOrderNote{sellNote, &buyNote},
  9596  		BaseFeeRate:  baseFeeRate,
  9597  		QuoteFeeRate: quoteFeeRate,
  9598  	})
  9599  	if err != nil {
  9600  		t.Fatalf("Sync error: %v", err)
  9601  	}
  9602  
  9603  	preSwap := &asset.PreSwap{
  9604  		Estimate: &asset.SwapEstimate{
  9605  			MaxFees:            1001,
  9606  			Lots:               5,
  9607  			RealisticBestCase:  15,
  9608  			RealisticWorstCase: 20,
  9609  		},
  9610  	}
  9611  
  9612  	tBtcWallet.preSwap = preSwap
  9613  
  9614  	preRedeem := &asset.PreRedeem{
  9615  		Estimate: &asset.RedeemEstimate{
  9616  			RealisticBestCase:  15,
  9617  			RealisticWorstCase: 20,
  9618  		},
  9619  	}
  9620  
  9621  	tDcrWallet.preRedeem = preRedeem
  9622  
  9623  	form := &TradeForm{
  9624  		Host: tDexHost,
  9625  		Sell: false,
  9626  		// IsLimit: true,
  9627  		Base:  tUTXOAssetA.ID,
  9628  		Quote: tUTXOAssetB.ID,
  9629  		Qty:   quoteConvertedLotSize * 5,
  9630  		Rate:  rate,
  9631  	}
  9632  	preOrder, err := tCore.PreOrder(form)
  9633  	if err != nil {
  9634  		t.Fatalf("PreOrder market buy error: %v", err)
  9635  	}
  9636  
  9637  	compUint64 := func(tag string, a, b uint64) {
  9638  		t.Helper()
  9639  		if a != b {
  9640  			t.Fatalf("%s: %d != %d", tag, a, b)
  9641  		}
  9642  	}
  9643  
  9644  	est1, est2 := preSwap.Estimate, preOrder.Swap.Estimate
  9645  	compUint64("MaxFees", est1.MaxFees, est2.MaxFees)
  9646  	compUint64("RealisticWorstCase", est1.RealisticWorstCase, est2.RealisticWorstCase)
  9647  	compUint64("RealisticBestCase", est1.RealisticBestCase, est2.RealisticBestCase)
  9648  	// This is a buy order, so the from asset is the quote asset.
  9649  	compUint64("PreOrder.FeeSuggestion.quote", quoteFeeRate, tBtcWallet.preSwapForm.FeeSuggestion)
  9650  	compUint64("PreOrder.FeeSuggestion.base", baseFeeRate, tDcrWallet.preRedeemForm.FeeSuggestion)
  9651  
  9652  	// Missing book is an error
  9653  	delete(dc.books, tDcrBtcMktName)
  9654  	_, err = tCore.PreOrder(form)
  9655  	if err == nil {
  9656  		t.Fatalf("no error for market order with missing book")
  9657  	}
  9658  	dc.books[tDcrBtcMktName] = book
  9659  
  9660  	// Exercise the market sell path too.
  9661  	form.Sell = true
  9662  	_, err = tCore.PreOrder(form)
  9663  	if err != nil {
  9664  		t.Fatalf("PreOrder market sell error: %v", err)
  9665  	}
  9666  
  9667  	// Market orders have to have a market to make estimates.
  9668  	book.Unbook(&msgjson.UnbookOrderNote{
  9669  		MarketID: tDcrBtcMktName,
  9670  		OrderID:  sellNote.OrderID,
  9671  	})
  9672  	book.Unbook(&msgjson.UnbookOrderNote{
  9673  		MarketID: tDcrBtcMktName,
  9674  		OrderID:  buyNote.OrderID,
  9675  	})
  9676  	_, err = tCore.PreOrder(form)
  9677  	if err == nil {
  9678  		t.Fatalf("no error for market order with empty market")
  9679  	}
  9680  
  9681  	// Limit orders have no such restriction.
  9682  	form.IsLimit = true
  9683  	_, err = tCore.PreOrder(form)
  9684  	if err != nil {
  9685  		t.Fatalf("PreOrder limit sell error: %v", err)
  9686  	}
  9687  
  9688  	var newBaseFeeRate uint64 = 55
  9689  	var newQuoteFeeRate uint64 = 65
  9690  	feeRateSource := func(msg *msgjson.Message, f msgFunc) error {
  9691  		var resp *msgjson.Message
  9692  		if string(msg.Payload) == "42" {
  9693  			resp, _ = msgjson.NewResponse(msg.ID, newBaseFeeRate, nil)
  9694  		} else {
  9695  			resp, _ = msgjson.NewResponse(msg.ID, newQuoteFeeRate, nil)
  9696  		}
  9697  		f(resp)
  9698  		return nil
  9699  	}
  9700  
  9701  	// Removing the book should cause us to
  9702  	delete(dc.books, tDcrBtcMktName)
  9703  	rig.ws.queueResponse(msgjson.FeeRateRoute, feeRateSource)
  9704  	rig.ws.queueResponse(msgjson.FeeRateRoute, feeRateSource)
  9705  
  9706  	_, err = tCore.PreOrder(form)
  9707  	if err != nil {
  9708  		t.Fatalf("PreOrder limit sell error #2: %v", err)
  9709  	}
  9710  	// sell order now, so from asset is base asset
  9711  	compUint64("PreOrder.FeeSuggestion quote asset from server", newQuoteFeeRate, tBtcWallet.preRedeemForm.FeeSuggestion)
  9712  	compUint64("PreOrder.FeeSuggestion base asset from server", newBaseFeeRate, tDcrWallet.preSwapForm.FeeSuggestion)
  9713  	dc.books[tDcrBtcMktName] = book
  9714  
  9715  	// no DEX
  9716  	delete(tCore.conns, dc.acct.host)
  9717  	_, err = tCore.PreOrder(form)
  9718  	if err == nil {
  9719  		t.Fatalf("no error for unknown DEX")
  9720  	}
  9721  	tCore.conns[dc.acct.host] = dc
  9722  
  9723  	// no wallet
  9724  	delete(tCore.wallets, tUTXOAssetA.ID)
  9725  	_, err = tCore.PreOrder(form)
  9726  	if err == nil {
  9727  		t.Fatalf("no error for missing wallet")
  9728  	}
  9729  	tCore.wallets[tUTXOAssetA.ID] = dcrWallet
  9730  
  9731  	// base wallet not connected
  9732  	dcrWallet.hookedUp = false
  9733  	tDcrWallet.connectErr = tErr
  9734  	_, err = tCore.PreOrder(form)
  9735  	if err == nil {
  9736  		t.Fatalf("no error for unconnected base wallet")
  9737  	}
  9738  	dcrWallet.hookedUp = true
  9739  	tDcrWallet.connectErr = nil
  9740  
  9741  	// quote wallet not connected
  9742  	btcWallet.hookedUp = false
  9743  	tBtcWallet.connectErr = tErr
  9744  	_, err = tCore.PreOrder(form)
  9745  	if err == nil {
  9746  		t.Fatalf("no error for unconnected quote wallet")
  9747  	}
  9748  	btcWallet.hookedUp = true
  9749  	tBtcWallet.connectErr = nil
  9750  
  9751  	// success again
  9752  	_, err = tCore.PreOrder(form)
  9753  	if err != nil {
  9754  		t.Fatalf("PreOrder error after fixing everything: %v", err)
  9755  	}
  9756  }
  9757  
  9758  func TestRefreshServerConfig(t *testing.T) {
  9759  	rig := newTestRig()
  9760  	defer rig.shutdown()
  9761  
  9762  	// Add an API version to supportedAPIVers to use in tests.
  9763  	const newAPIVer = ^uint16(0) - 1
  9764  	supportedAPIVers = append(supportedAPIVers, int32(newAPIVer))
  9765  
  9766  	queueConfig := func(err *msgjson.Error, apiVer uint16) {
  9767  		rig.ws.queueResponse(msgjson.ConfigRoute, func(msg *msgjson.Message, f msgFunc) error {
  9768  			cfg := *rig.dc.cfg
  9769  			cfg.APIVersion = apiVer
  9770  			resp, _ := msgjson.NewResponse(msg.ID, cfg, err)
  9771  			f(resp)
  9772  			return nil
  9773  		})
  9774  	}
  9775  	tests := []struct {
  9776  		name       string
  9777  		configErr  *msgjson.Error
  9778  		gotAPIVer  uint16
  9779  		marketBase uint32
  9780  		wantErr    bool
  9781  	}{{
  9782  		name:       "ok",
  9783  		marketBase: tUTXOAssetA.ID,
  9784  		gotAPIVer:  newAPIVer,
  9785  	}, {
  9786  		name:      "unable to fetch config",
  9787  		configErr: new(msgjson.Error),
  9788  		wantErr:   true,
  9789  	}, {
  9790  		name:       "api not in wanted versions",
  9791  		gotAPIVer:  ^uint16(0),
  9792  		marketBase: tUTXOAssetA.ID,
  9793  		wantErr:    true,
  9794  	}, {
  9795  		name:       "generate maps failure",
  9796  		marketBase: ^uint32(0),
  9797  		gotAPIVer:  newAPIVer,
  9798  		wantErr:    true,
  9799  	}}
  9800  
  9801  	for _, test := range tests {
  9802  		rig.dc.cfg.Markets[0].Base = test.marketBase
  9803  		queueConfig(test.configErr, test.gotAPIVer)
  9804  		_, err := rig.dc.refreshServerConfig()
  9805  		if test.wantErr {
  9806  			if err == nil {
  9807  				t.Fatalf("expected error for test %q", test.name)
  9808  			}
  9809  			continue
  9810  		}
  9811  		if err != nil {
  9812  			t.Fatalf("unexpected error for test %q: %v", test.name, err)
  9813  		}
  9814  	}
  9815  }
  9816  
  9817  func TestCredentialHandling(t *testing.T) {
  9818  	rig := newTestRig()
  9819  	defer rig.shutdown()
  9820  	tCore := rig.core
  9821  
  9822  	clearCreds := func() {
  9823  		tCore.credentials = nil
  9824  		rig.db.creds = nil
  9825  	}
  9826  
  9827  	clearCreds()
  9828  	tCore.newCrypter = encrypt.NewCrypter
  9829  	tCore.reCrypter = encrypt.Deserialize
  9830  
  9831  	_, err := tCore.InitializeClient(tPW, nil)
  9832  	if err != nil {
  9833  		t.Fatalf("InitializeClient error: %v", err)
  9834  	}
  9835  
  9836  	// Since the actual encrypt package crypter is now used instead of the dummy
  9837  	// tCrypter, the acct.encKey should be updated to reflect acct.privKey.
  9838  	// Although the test does not rely on this, we should keep the dexAccount
  9839  	// self-consistent and avoid confusing messages in the test log.
  9840  	err = rig.resetAcctEncKey(tPW)
  9841  	if err != nil {
  9842  		t.Fatalf("InitializeClient error: %v", err)
  9843  	}
  9844  
  9845  	tCore.Logout()
  9846  
  9847  	err = tCore.Login(tPW)
  9848  	if err != nil {
  9849  		t.Fatalf("Login error: %v", err)
  9850  	}
  9851  	// NOTE: a warning note is expected. "Wallet connection warning - Incomplete
  9852  	// registration detected for somedex.tld:7232, but failed to connect to the
  9853  	// Decred wallet"
  9854  }
  9855  
  9856  func TestCoreAssetSeedAndPass(t *testing.T) {
  9857  	// This test ensures the derived wallet seed and password are deterministic
  9858  	// and depend on both asset ID and app seed.
  9859  
  9860  	// NOTE: the blake256 hash of an empty slice is:
  9861  	// []byte{0x71, 0x6f, 0x6e, 0x86, 0x3f, 0x74, 0x4b, 0x9a, 0xc2, 0x2c, 0x97, 0xec, 0x7b, 0x76, 0xea, 0x5f,
  9862  	//        0x59, 0x8, 0xbc, 0x5b, 0x2f, 0x67, 0xc6, 0x15, 0x10, 0xbf, 0xc4, 0x75, 0x13, 0x84, 0xea, 0x7a}
  9863  	// The above was very briefly the password for all seeded wallets, not released.
  9864  
  9865  	tests := []struct {
  9866  		name     string
  9867  		appSeed  []byte
  9868  		assetID  uint32
  9869  		wantSeed []byte
  9870  		wantPass []byte
  9871  	}{
  9872  		{
  9873  			name:    "base",
  9874  			appSeed: []byte{1, 2, 3},
  9875  			assetID: 2,
  9876  			wantSeed: []byte{
  9877  				0xac, 0x61, 0xb1, 0xbc, 0x77, 0xd0, 0xa6, 0xd5, 0xd2, 0xb5, 0xc9, 0x77, 0x91, 0xd6, 0x4a, 0xaf,
  9878  				0x4a, 0xa3, 0x47, 0xb7, 0xb, 0x85, 0xe, 0x82, 0x1c, 0x79, 0xab, 0xc0, 0x86, 0x50, 0xee, 0xda},
  9879  			wantPass: []byte{
  9880  				0xd8, 0xf0, 0x27, 0x4d, 0xbc, 0x56, 0xb0, 0x74, 0x1e, 0x20, 0x3b, 0x98, 0xe9, 0xaa, 0x5c, 0xba,
  9881  				0x13, 0xfd, 0x60, 0x3b, 0x83, 0x76, 0x2e, 0x4b, 0x5d, 0x6d, 0x19, 0x57, 0x89, 0xe2, 0x8b, 0xc7},
  9882  		},
  9883  		{
  9884  			name:    "change app seed",
  9885  			appSeed: []byte{2, 2, 3},
  9886  			assetID: 2,
  9887  			wantSeed: []byte{
  9888  				0xf, 0xc9, 0xf, 0xa8, 0xb3, 0xe9, 0x31, 0x2a, 0xba, 0xf1, 0xda, 0x70, 0x41, 0x81, 0x49, 0xed,
  9889  				0xad, 0x47, 0x9, 0xcd, 0xe2, 0x17, 0x14, 0xd, 0x63, 0x49, 0x8a, 0xd8, 0xff, 0x1f, 0x3e, 0x8b},
  9890  			wantPass: []byte{
  9891  				0x78, 0x21, 0x72, 0x59, 0xbe, 0x39, 0xea, 0x54, 0x10, 0x46, 0x7d, 0x7e, 0xa, 0x95, 0xc4, 0xa0,
  9892  				0xd8, 0x73, 0xce, 0x1, 0xb2, 0x49, 0x98, 0x6c, 0x68, 0xc5, 0x69, 0x69, 0xa7, 0x13, 0xc1, 0xce},
  9893  		},
  9894  		{
  9895  			name:    "change asset ID",
  9896  			appSeed: []byte{1, 2, 3},
  9897  			assetID: 0,
  9898  			wantSeed: []byte{
  9899  				0xe1, 0xad, 0x62, 0xe4, 0x60, 0xfd, 0x75, 0x91, 0x3d, 0x41, 0x2e, 0x8e, 0xc5, 0x72, 0xd4, 0xa2,
  9900  				0x39, 0x2d, 0x32, 0x86, 0xf0, 0x6b, 0xf7, 0xdf, 0x48, 0xcc, 0x57, 0xb1, 0x4b, 0x7b, 0xc6, 0xce},
  9901  			wantPass: []byte{
  9902  				0x52, 0xba, 0x59, 0x21, 0xd3, 0xc5, 0x6b, 0x2, 0x2c, 0x12, 0xc1, 0x98, 0xdc, 0x84, 0xed, 0x68,
  9903  				0x6, 0x35, 0xa6, 0x25, 0xd0, 0xc4, 0x49, 0x5a, 0x13, 0xc3, 0x12, 0xfb, 0xeb, 0xb3, 0x61, 0x88},
  9904  		},
  9905  	}
  9906  	for _, tt := range tests {
  9907  		t.Run(tt.name, func(t *testing.T) {
  9908  			seed, pass := AssetSeedAndPass(tt.assetID, tt.appSeed)
  9909  			if !bytes.Equal(pass, tt.wantPass) {
  9910  				t.Errorf("pass not as expected, got %#v", pass)
  9911  			}
  9912  			if !bytes.Equal(seed, tt.wantSeed) {
  9913  				t.Errorf("seed not as expected, got %#v", seed)
  9914  			}
  9915  		})
  9916  	}
  9917  }
  9918  
  9919  var randU32 = func() uint32 { return uint32(rand.Int31()) }
  9920  
  9921  func randOrderForMarket(base, quote uint32) order.Order {
  9922  	switch rand.Intn(3) {
  9923  	case 0:
  9924  		o, _ := ordertest.RandomCancelOrder()
  9925  		o.BaseAsset = base
  9926  		o.QuoteAsset = quote
  9927  		return o
  9928  	case 1:
  9929  		o, _ := ordertest.RandomMarketOrder()
  9930  		o.BaseAsset = base
  9931  		o.QuoteAsset = quote
  9932  		return o
  9933  	default:
  9934  		o, _ := ordertest.RandomLimitOrder()
  9935  		o.BaseAsset = base
  9936  		o.QuoteAsset = quote
  9937  		return o
  9938  	}
  9939  }
  9940  
  9941  func randBytes(n int) []byte {
  9942  	b := make([]byte, n)
  9943  	rand.Read(b)
  9944  	return b
  9945  }
  9946  
  9947  func TestDeleteOrderFn(t *testing.T) {
  9948  	rig := newTestRig()
  9949  	defer rig.shutdown()
  9950  	tCore := rig.core
  9951  
  9952  	randomOdrs := func() []*db.MetaOrder {
  9953  		acct1 := dbtest.RandomAccountInfo()
  9954  		acct2 := dbtest.RandomAccountInfo()
  9955  		base1, quote1 := tUTXOAssetA.ID, tUTXOAssetB.ID
  9956  		base2, quote2 := tACCTAsset.ID, tUTXOAssetA.ID
  9957  		n := rand.Intn(9) + 1
  9958  		orders := make([]*db.MetaOrder, n)
  9959  		for i := 0; i < n; i++ {
  9960  			acct := acct1
  9961  			base, quote := base1, quote1
  9962  			if i%2 == 1 {
  9963  				acct = acct2
  9964  				base, quote = base2, quote2
  9965  			}
  9966  			ord := randOrderForMarket(base, quote)
  9967  			orders[i] = &db.MetaOrder{
  9968  				MetaData: &db.OrderMetaData{
  9969  					Status:             order.OrderStatus(rand.Intn(5) + 1),
  9970  					Host:               acct.Host,
  9971  					Proof:              db.OrderProof{DEXSig: randBytes(73)},
  9972  					SwapFeesPaid:       rand.Uint64(),
  9973  					RedemptionFeesPaid: rand.Uint64(),
  9974  					MaxFeeRate:         rand.Uint64(),
  9975  				},
  9976  				Order: ord,
  9977  			}
  9978  		}
  9979  		return orders
  9980  	}
  9981  
  9982  	ordersFile, err := os.CreateTemp("", "delete_archives_test_orders")
  9983  	if err != nil {
  9984  		t.Fatal(err)
  9985  	}
  9986  	ordersFileName := ordersFile.Name()
  9987  	ordersFile.Close()
  9988  	os.Remove(ordersFileName)
  9989  
  9990  	tests := []struct {
  9991  		name, ordersFileStr string
  9992  		wantErr             bool
  9993  	}{{
  9994  		name:          "ok orders and file save",
  9995  		ordersFileStr: ordersFileName,
  9996  	}, {
  9997  		name:          "bad file (already closed)",
  9998  		ordersFileStr: ordersFileName,
  9999  		wantErr:       true,
 10000  	}}
 10001  
 10002  	for _, test := range tests {
 10003  		perOrdFn, cleanupFn, err := tCore.deleteOrderFn(test.ordersFileStr)
 10004  		if test.wantErr {
 10005  			if err != nil {
 10006  				continue
 10007  			}
 10008  			t.Fatalf("%q: expected error", test.name)
 10009  		}
 10010  		if err != nil {
 10011  			t.Fatalf("%q: unexpected failure: %v", test.name, err)
 10012  		}
 10013  		for _, o := range randomOdrs() {
 10014  			err = perOrdFn(o)
 10015  			if err != nil {
 10016  				t.Fatalf("%q: unexpected failure: %v", test.name, err)
 10017  			}
 10018  		}
 10019  		cleanupFn()
 10020  	}
 10021  
 10022  	b, err := os.ReadFile(ordersFileName)
 10023  	if err != nil {
 10024  		t.Fatalf("unable to read file: %s", ordersFileName)
 10025  	}
 10026  	fmt.Println(string(b))
 10027  	os.Remove(ordersFileName)
 10028  }
 10029  
 10030  func TestDeleteMatchFn(t *testing.T) {
 10031  	randomMtchs := func() []*db.MetaMatch {
 10032  		base, quote := tUTXOAssetA.ID, tUTXOAssetB.ID
 10033  		acct := dbtest.RandomAccountInfo()
 10034  		n := rand.Intn(9) + 1
 10035  		metaMatches := make([]*db.MetaMatch, 0, n)
 10036  		for i := 0; i < n; i++ {
 10037  			m := &db.MetaMatch{
 10038  				MetaData: &db.MatchMetaData{
 10039  					Proof: *dbtest.RandomMatchProof(0.5),
 10040  					DEX:   acct.Host,
 10041  					Base:  base,
 10042  					Quote: quote,
 10043  					Stamp: rand.Uint64(),
 10044  				},
 10045  				UserMatch: ordertest.RandomUserMatch(),
 10046  			}
 10047  			if i%2 == 1 {
 10048  				m.Status = order.MatchStatus(rand.Intn(4))
 10049  			} else {
 10050  				m.Status = order.MatchComplete              // inactive
 10051  				m.MetaData.Proof.Auth.RedeemSig = []byte{0} // redeemSig required for MatchComplete to be considered inactive
 10052  			}
 10053  			metaMatches = append(metaMatches, m)
 10054  		}
 10055  		return metaMatches
 10056  	}
 10057  
 10058  	matchesFile, err := os.CreateTemp("", "delete_archives_test_matches")
 10059  	if err != nil {
 10060  		t.Fatal(err)
 10061  	}
 10062  	matchesFileName := matchesFile.Name()
 10063  	matchesFile.Close()
 10064  	os.Remove(matchesFileName)
 10065  
 10066  	tests := []struct {
 10067  		name, matchesFileStr string
 10068  		wantErr              bool
 10069  	}{{
 10070  		name:           "ok matches and file save",
 10071  		matchesFileStr: matchesFileName,
 10072  	}, {
 10073  		name:           "bad file (already closed)",
 10074  		matchesFileStr: matchesFileName,
 10075  		wantErr:        true,
 10076  	}}
 10077  
 10078  	for _, test := range tests {
 10079  		perMatchFn, cleanupFn, err := deleteMatchFn(test.matchesFileStr)
 10080  		if test.wantErr {
 10081  			if err != nil {
 10082  				continue
 10083  			}
 10084  			t.Fatalf("%q: expected error", test.name)
 10085  		}
 10086  		if err != nil {
 10087  			t.Fatalf("%q: unexpected failure: %v", test.name, err)
 10088  		}
 10089  		for _, m := range randomMtchs() {
 10090  			err = perMatchFn(m, true)
 10091  			if err != nil {
 10092  				t.Fatalf("%q: unexpected failure: %v", test.name, err)
 10093  			}
 10094  		}
 10095  		cleanupFn()
 10096  	}
 10097  
 10098  	b, err := os.ReadFile(matchesFileName)
 10099  	if err != nil {
 10100  		t.Fatalf("unable to read file: %s", matchesFileName)
 10101  	}
 10102  	fmt.Println(string(b))
 10103  	os.Remove(matchesFileName)
 10104  }
 10105  
 10106  func TestDeleteArchivedRecords(t *testing.T) {
 10107  	rig := newTestRig()
 10108  	defer rig.shutdown()
 10109  	tCore := rig.core
 10110  	tdb := tCore.db.(*TDB)
 10111  
 10112  	tempFile := func(suffix string) (path string) {
 10113  		matchesFile, err := os.CreateTemp("", suffix+"delete_archives_test_matches")
 10114  		if err != nil {
 10115  			t.Fatal(err)
 10116  		}
 10117  		matchesFileName := matchesFile.Name()
 10118  		matchesFile.Close()
 10119  		os.Remove(matchesFileName)
 10120  		return matchesFileName
 10121  	}
 10122  
 10123  	tests := []struct {
 10124  		name                                              string
 10125  		olderThan                                         *time.Time
 10126  		matchesFileStr, ordersFileStr                     string
 10127  		archivedMatches, archivedOrders                   int
 10128  		deleteInactiveOrdersErr, deleteInactiveMatchesErr error
 10129  		wantErr                                           bool
 10130  	}{{
 10131  		name:            "ok no order or file save",
 10132  		archivedMatches: 12,
 10133  		archivedOrders:  24,
 10134  	}, {
 10135  		name:            "ok orders and file save",
 10136  		ordersFileStr:   tempFile("abc"),
 10137  		matchesFileStr:  tempFile("123"),
 10138  		archivedMatches: 34,
 10139  		archivedOrders:  67,
 10140  	}, {
 10141  		name:                    "orders save error",
 10142  		ordersFileStr:           tempFile("abc"),
 10143  		deleteInactiveOrdersErr: errors.New(""),
 10144  		wantErr:                 true,
 10145  	}, {
 10146  		name:                     "matches save error",
 10147  		matchesFileStr:           tempFile("123"),
 10148  		deleteInactiveMatchesErr: errors.New(""),
 10149  		wantErr:                  true,
 10150  	}}
 10151  
 10152  	for _, test := range tests {
 10153  		tdb.archivedMatches = test.archivedMatches
 10154  		tdb.archivedOrders = test.archivedOrders
 10155  		tdb.deleteInactiveOrdersErr = test.deleteInactiveOrdersErr
 10156  		tdb.deleteInactiveMatchesErr = test.deleteInactiveMatchesErr
 10157  		nRecordsDeleted, err := tCore.DeleteArchivedRecords(test.olderThan, test.matchesFileStr, test.ordersFileStr)
 10158  		if test.wantErr {
 10159  			if err != nil {
 10160  				continue
 10161  			}
 10162  			t.Fatalf("%q: expected error", test.name)
 10163  		}
 10164  		if err != nil {
 10165  			t.Fatalf("%q: unexpected failure: %v", test.name, err)
 10166  		}
 10167  		expectedRecords := test.archivedMatches + test.archivedOrders
 10168  		if nRecordsDeleted != expectedRecords {
 10169  			t.Fatalf("%s: Expected %d deleted records, got %d", test.name, expectedRecords, nRecordsDeleted)
 10170  		}
 10171  	}
 10172  }
 10173  
 10174  func TestLCM(t *testing.T) {
 10175  	tests := []struct {
 10176  		name                                  string
 10177  		a, b, wantDenom, wantMultA, wantMultB uint64
 10178  	}{{
 10179  		name:      "ok 5 and 10",
 10180  		a:         5,
 10181  		b:         10,
 10182  		wantDenom: 10,
 10183  		wantMultA: 2,
 10184  		wantMultB: 1,
 10185  	}, {
 10186  		name:      "ok 3 and 7",
 10187  		a:         3,
 10188  		b:         7,
 10189  		wantDenom: 21,
 10190  		wantMultA: 7,
 10191  		wantMultB: 3,
 10192  	}, {
 10193  		name:      "ok 6 and 34",
 10194  		a:         34,
 10195  		b:         6,
 10196  		wantDenom: 102,
 10197  		wantMultA: 3,
 10198  		wantMultB: 17,
 10199  	}}
 10200  
 10201  	for _, test := range tests {
 10202  		denom, multA, multB := lcm(test.a, test.b)
 10203  		if denom != test.wantDenom || multA != test.wantMultA || multB != test.wantMultB {
 10204  			t.Fatalf("%q: expected %d %d %d but got %d %d %d", test.name,
 10205  				test.wantDenom, test.wantMultA, test.wantMultB, denom, multA, multB)
 10206  		}
 10207  	}
 10208  }
 10209  
 10210  func TestToggleRateSourceStatus(t *testing.T) {
 10211  	rig := newTestRig()
 10212  	defer rig.shutdown()
 10213  	tCore := rig.core
 10214  
 10215  	tests := []struct {
 10216  		name, source  string
 10217  		wantErr, init bool
 10218  	}{{
 10219  		name:    "Invalid rate source",
 10220  		source:  "binance",
 10221  		wantErr: true,
 10222  	}, {
 10223  		name:    "ok valid source",
 10224  		source:  messari,
 10225  		wantErr: false,
 10226  	}, {
 10227  		name:    "ok already disabled/not initialized || enabled",
 10228  		source:  messari,
 10229  		wantErr: false,
 10230  	}}
 10231  
 10232  	// Test disabling fiat rate source.
 10233  	for _, test := range tests {
 10234  		err := tCore.ToggleRateSourceStatus(test.source, true)
 10235  		if test.wantErr != (err != nil) {
 10236  			t.Fatalf("%s: wantErr = %t, err = %v", test.name, test.wantErr, err)
 10237  		}
 10238  	}
 10239  
 10240  	// Test enabling fiat rate source.
 10241  	for _, test := range tests {
 10242  		if test.init {
 10243  			tCore.fiatRateSources[test.source] = newCommonRateSource(tFetcher)
 10244  		}
 10245  		err := tCore.ToggleRateSourceStatus(test.source, false)
 10246  		if test.wantErr != (err != nil) {
 10247  			t.Fatalf("%s: wantErr = %t, err = %v", test.name, test.wantErr, err)
 10248  		}
 10249  	}
 10250  }
 10251  
 10252  func TestFiatRateSources(t *testing.T) {
 10253  	rig := newTestRig()
 10254  	defer rig.shutdown()
 10255  	tCore := rig.core
 10256  	supportedFetchers := len(fiatRateFetchers)
 10257  	rateSources := tCore.FiatRateSources()
 10258  	if len(rateSources) != supportedFetchers {
 10259  		t.Fatalf("Expected %d number of fiat rate source/fetchers", supportedFetchers)
 10260  	}
 10261  }
 10262  
 10263  func TestFiatConversions(t *testing.T) {
 10264  	rig := newTestRig()
 10265  	defer rig.shutdown()
 10266  	tCore := rig.core
 10267  
 10268  	// No fiat rate source initialized
 10269  	fiatRates := tCore.fiatConversions()
 10270  	if len(fiatRates) != 0 {
 10271  		t.Fatal("Unexpected asset rate values.")
 10272  	}
 10273  
 10274  	// Initialize fiat rate sources.
 10275  	for token := range fiatRateFetchers {
 10276  		tCore.fiatRateSources[token] = newCommonRateSource(tFetcher)
 10277  	}
 10278  
 10279  	// Fetch fiat rates.
 10280  	tCore.wg.Add(1)
 10281  	go func() {
 10282  		defer tCore.wg.Done()
 10283  		tCore.refreshFiatRates(tCtx)
 10284  	}()
 10285  	tCore.wg.Wait()
 10286  
 10287  	// Expects assets fiat rate values.
 10288  	fiatRates = tCore.fiatConversions()
 10289  	if len(fiatRates) != 2 {
 10290  		t.Fatal("Expected assets fiat rate for two assets")
 10291  	}
 10292  
 10293  	// fiat rates for assets can expire, and fiat rate fetchers can be
 10294  	// removed if expired.
 10295  	for token, source := range tCore.fiatRateSources {
 10296  		source.fiatRates[tUTXOAssetA.ID].lastUpdate = time.Now().Add(-time.Minute)
 10297  		source.fiatRates[tUTXOAssetB.ID].lastUpdate = time.Now().Add(-time.Minute)
 10298  		if source.isExpired(55 * time.Second) {
 10299  			delete(tCore.fiatRateSources, token)
 10300  		}
 10301  	}
 10302  
 10303  	fiatRates = tCore.fiatConversions()
 10304  	if len(fiatRates) != 0 {
 10305  		t.Fatal("Unexpected assets fiat rate values, expected to ignore expired fiat rates.")
 10306  	}
 10307  
 10308  	if len(tCore.fiatRateSources) != 0 {
 10309  		t.Fatal("Expected fiat conversion to be disabled, all rate source data has expired.")
 10310  	}
 10311  }
 10312  
 10313  func TestValidateAddress(t *testing.T) {
 10314  	rig := newTestRig()
 10315  	defer rig.shutdown()
 10316  	tCore := rig.core
 10317  
 10318  	wallet, tWallet := newTWallet(tUTXOAssetA.ID)
 10319  	tCore.wallets[tUTXOAssetA.ID] = wallet
 10320  
 10321  	tests := []struct {
 10322  		name              string
 10323  		addr              string
 10324  		wantValidAddr     bool
 10325  		wantMissingWallet bool
 10326  		wantErr           bool
 10327  	}{{
 10328  		name:          "valid address",
 10329  		addr:          "randomvalidaddress",
 10330  		wantValidAddr: true,
 10331  	}, {
 10332  		name: "invalid address",
 10333  		addr: "",
 10334  	}, {
 10335  		name:              "wallet not found",
 10336  		addr:              "randomaddr",
 10337  		wantMissingWallet: true,
 10338  		wantErr:           true,
 10339  	}}
 10340  	for _, test := range tests {
 10341  		tWallet.validAddr = test.wantValidAddr
 10342  		if test.wantMissingWallet {
 10343  			tCore.wallets = make(map[uint32]*xcWallet)
 10344  		}
 10345  		valid, err := tCore.ValidateAddress(test.addr, tUTXOAssetA.ID)
 10346  		if test.wantErr {
 10347  			if err != nil {
 10348  				continue
 10349  			}
 10350  			t.Fatalf("%s: expected error", test.name)
 10351  		}
 10352  		if test.wantValidAddr != valid {
 10353  			t.Fatalf("Got wrong response for address validation, got %v expected %v", valid, test.wantValidAddr)
 10354  		}
 10355  	}
 10356  }
 10357  
 10358  func TestEstimateSendTxFee(t *testing.T) {
 10359  	rig := newTestRig()
 10360  	defer rig.shutdown()
 10361  	tCore := rig.core
 10362  
 10363  	tests := []struct {
 10364  		name              string
 10365  		asset             uint32
 10366  		estFee            uint64
 10367  		value             uint64
 10368  		subtract          bool
 10369  		wantMissingWallet bool
 10370  		wantErr           bool
 10371  	}{{
 10372  		name:     "ok",
 10373  		asset:    tUTXOAssetA.ID,
 10374  		subtract: true,
 10375  		estFee:   1e8,
 10376  		value:    1e8,
 10377  	}, {
 10378  		name:     "zero amount",
 10379  		asset:    tACCTAsset.ID,
 10380  		subtract: true,
 10381  		wantErr:  true,
 10382  	}, {
 10383  		name:     "subtract true and not withdrawer",
 10384  		asset:    tACCTAsset.ID,
 10385  		subtract: true,
 10386  		wantErr:  true,
 10387  		value:    1e8,
 10388  	}, {
 10389  		name:              "wallet not found",
 10390  		asset:             tUTXOAssetA.ID,
 10391  		wantErr:           true,
 10392  		wantMissingWallet: true,
 10393  		value:             1e8,
 10394  	}}
 10395  
 10396  	for _, test := range tests {
 10397  		wallet, tWallet := newTWallet(test.asset)
 10398  		tCore.wallets[test.asset] = wallet
 10399  		if test.wantMissingWallet {
 10400  			delete(tCore.wallets, test.asset)
 10401  		}
 10402  
 10403  		tWallet.estFee = test.estFee
 10404  
 10405  		tWallet.estFeeErr = nil
 10406  		if test.wantErr {
 10407  			tWallet.estFeeErr = tErr
 10408  		}
 10409  		estimate, _, err := tCore.EstimateSendTxFee("addr", test.asset, test.value, test.subtract, false)
 10410  		if test.wantErr {
 10411  			if err != nil {
 10412  				continue
 10413  			}
 10414  			t.Fatalf("%s: expected error", test.name)
 10415  		}
 10416  		if err != nil {
 10417  			t.Fatalf("%s: unexpected error: %v", test.name, err)
 10418  		}
 10419  
 10420  		if estimate != test.estFee {
 10421  			t.Fatalf("%s: expected fee %v, got %v", test.name, test.estFee, estimate)
 10422  		}
 10423  		if !test.wantErr && err != nil {
 10424  			t.Fatalf("%s: unexpected error", test.name)
 10425  		}
 10426  	}
 10427  }
 10428  
 10429  type TDynamicSwapper struct {
 10430  	*TXCWallet
 10431  	tfpPaid         uint64
 10432  	tfpSecretHashes [][]byte
 10433  	tfpErr          error
 10434  }
 10435  
 10436  func (dtfc *TDynamicSwapper) DynamicSwapFeesPaid(ctx context.Context, coinID, contractData dex.Bytes) (uint64, [][]byte, error) {
 10437  	return dtfc.tfpPaid, dtfc.tfpSecretHashes, dtfc.tfpErr
 10438  }
 10439  func (dtfc *TDynamicSwapper) DynamicRedemptionFeesPaid(ctx context.Context, coinID, contractData dex.Bytes) (uint64, [][]byte, error) {
 10440  	return dtfc.tfpPaid, dtfc.tfpSecretHashes, dtfc.tfpErr
 10441  }
 10442  
 10443  var _ asset.DynamicSwapper = (*TDynamicSwapper)(nil)
 10444  
 10445  func TestUpdateFeesPaid(t *testing.T) {
 10446  	ctx, cancel := context.WithCancel(context.Background())
 10447  	defer cancel()
 10448  	tests := []struct {
 10449  		name                   string
 10450  		paid                   uint64
 10451  		init, swapWallets      bool
 10452  		tfpErr, updateOrderErr error
 10453  	}{{
 10454  		name: "ok init",
 10455  		paid: 1,
 10456  		init: true,
 10457  	}, {
 10458  		name:        "ok redeem",
 10459  		paid:        1,
 10460  		swapWallets: true,
 10461  	}, {
 10462  		name: "not dynamic",
 10463  	}, {
 10464  		name:   "TransactionFeesPaid error other than coin not found",
 10465  		init:   true,
 10466  		tfpErr: errors.New("other error"),
 10467  	}}
 10468  	for _, test := range tests {
 10469  		acctWallet, tWallet := newTWallet(tACCTAsset.ID)
 10470  		dynamicFeeChecker := &TDynamicSwapper{TXCWallet: tWallet}
 10471  		acctWallet.Wallet = dynamicFeeChecker
 10472  		tWallet.confs["00"] = 10
 10473  
 10474  		utxoWallet, _ := newTWallet(tUTXOAssetA.ID)
 10475  
 10476  		wallets := &walletSet{
 10477  			fromWallet:  acctWallet,
 10478  			toWallet:    utxoWallet,
 10479  			baseWallet:  acctWallet,
 10480  			quoteWallet: utxoWallet,
 10481  		}
 10482  		if test.swapWallets {
 10483  			wallets.fromWallet, wallets.toWallet = wallets.toWallet, wallets.fromWallet
 10484  			wallets.baseWallet, wallets.quoteWallet = wallets.quoteWallet, wallets.baseWallet
 10485  		}
 10486  
 10487  		dc := &dexConnection{
 10488  			acct: tNewAccount(&tCrypter{}),
 10489  			log:  tLogger,
 10490  		}
 10491  		lo, _, _, _ := makeLimitOrder(dc, true, 0, 0)
 10492  		tracker := &trackedTrade{
 10493  			wallets:  wallets,
 10494  			dc:       dc,
 10495  			metaData: new(db.OrderMetaData),
 10496  			db:       new(TDB),
 10497  			Order:    lo,
 10498  			notify:   func(Notification) {},
 10499  		}
 10500  		tracker.SetTime(time.Now())
 10501  		dynamicFeeChecker.tfpPaid = 1
 10502  		dynamicFeeChecker.tfpErr = test.tfpErr
 10503  		dynamicFeeChecker.tfpSecretHashes = [][]byte{{0}}
 10504  		matchID := ordertest.RandomMatchID()
 10505  		match := &matchTracker{
 10506  			MetaMatch: db.MetaMatch{
 10507  				UserMatch: &order.UserMatch{MatchID: matchID},
 10508  				MetaData: &db.MatchMetaData{
 10509  					Proof: db.MatchProof{
 10510  						TakerSwap:   []byte{0},
 10511  						MakerSwap:   []byte{0},
 10512  						MakerRedeem: []byte{0},
 10513  						TakerRedeem: []byte{0},
 10514  						SecretHash:  []byte{0},
 10515  					},
 10516  				},
 10517  			},
 10518  		}
 10519  		tracker.updateDynamicSwapOrRedemptionFeesPaid(ctx, match, test.init)
 10520  		got := tracker.metaData.SwapFeesPaid
 10521  		if !test.init {
 10522  			got = tracker.metaData.RedemptionFeesPaid
 10523  		}
 10524  		if got != test.paid {
 10525  			t.Fatalf("%s: want %d but got %d fees paid", test.name, test.paid, got)
 10526  		}
 10527  	}
 10528  }
 10529  
 10530  func TestUpdateBondOptions(t *testing.T) {
 10531  	const feeRate = 50
 10532  
 10533  	rig := newTestRig()
 10534  	defer rig.shutdown()
 10535  	acct := rig.dc.acct
 10536  	acct.isAuthed = true
 10537  
 10538  	dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID)
 10539  	dcrWallet.Wallet = &TFeeRater{tDcrWallet, feeRate}
 10540  	rig.core.wallets[tUTXOAssetA.ID] = dcrWallet
 10541  	bondFeeBuffer := tDcrWallet.BondsFeeBuffer(feeRate)
 10542  
 10543  	bondAsset := dcrBondAsset
 10544  	var wrongBondAssetID uint32 = 0
 10545  	var targetTier uint64 = 1
 10546  	var targetTierZero uint64 = 0
 10547  	defaultMaxBondedAmt := maxBondedMult * bondAsset.Amt * targetTier
 10548  	tooLowMaxBonded := defaultMaxBondedAmt - 1
 10549  	// Double because we will reserve for the bond that's about to be posted
 10550  	// in rotateBonds too.
 10551  	singlyBondedReserves := bondAsset.Amt*targetTier*2 + bondFeeBuffer
 10552  
 10553  	type acctState struct {
 10554  		targetTier   uint64
 10555  		maxBondedAmt uint64
 10556  	}
 10557  
 10558  	for _, tt := range []struct {
 10559  		name        string
 10560  		bal         uint64
 10561  		form        BondOptionsForm
 10562  		before      acctState
 10563  		after       acctState
 10564  		expReserves uint64
 10565  		addOtherDC  bool
 10566  		wantErr     bool
 10567  	}{
 10568  		{
 10569  			name: "set target tier to 1",
 10570  			bal:  singlyBondedReserves,
 10571  			form: BondOptionsForm{
 10572  				Host:        acct.host,
 10573  				TargetTier:  &targetTier,
 10574  				BondAssetID: &bondAsset.ID,
 10575  			},
 10576  			after: acctState{
 10577  				targetTier:   1,
 10578  				maxBondedAmt: defaultMaxBondedAmt,
 10579  			},
 10580  			expReserves: singlyBondedReserves,
 10581  		},
 10582  		{
 10583  			name: "low balance",
 10584  			bal:  singlyBondedReserves - 1,
 10585  			form: BondOptionsForm{
 10586  				Host:        acct.host,
 10587  				TargetTier:  &targetTier,
 10588  				BondAssetID: &bondAsset.ID,
 10589  			},
 10590  			wantErr: true,
 10591  		},
 10592  		{
 10593  			name: "max-bonded too low",
 10594  			bal:  singlyBondedReserves,
 10595  			form: BondOptionsForm{
 10596  				Host:         acct.host,
 10597  				TargetTier:   &targetTier,
 10598  				BondAssetID:  &bondAsset.ID,
 10599  				MaxBondedAmt: &tooLowMaxBonded,
 10600  			},
 10601  			wantErr: true,
 10602  		},
 10603  		{
 10604  			name: "unsupported bond asset",
 10605  			form: BondOptionsForm{
 10606  				Host:        acct.host,
 10607  				TargetTier:  &targetTier,
 10608  				BondAssetID: &wrongBondAssetID,
 10609  			},
 10610  			wantErr: true,
 10611  		},
 10612  		{
 10613  			name: "lower target tier with zero balance OK",
 10614  			bal:  0,
 10615  			form: BondOptionsForm{
 10616  				Host:        acct.host,
 10617  				TargetTier:  &targetTierZero,
 10618  				BondAssetID: &bondAsset.ID,
 10619  			},
 10620  			before: acctState{
 10621  				targetTier:   1,
 10622  				maxBondedAmt: defaultMaxBondedAmt,
 10623  			},
 10624  			after:       acctState{},
 10625  			expReserves: 0,
 10626  		},
 10627  		{
 10628  			name: "lower target tier to zero with other exchanges still keeps reserves",
 10629  			bal:  0,
 10630  			form: BondOptionsForm{
 10631  				Host:        acct.host,
 10632  				TargetTier:  &targetTierZero,
 10633  				BondAssetID: &bondAsset.ID,
 10634  			},
 10635  			before: acctState{
 10636  				targetTier:   1,
 10637  				maxBondedAmt: defaultMaxBondedAmt,
 10638  			},
 10639  			addOtherDC:  true,
 10640  			after:       acctState{},
 10641  			expReserves: bondFeeBuffer,
 10642  		},
 10643  	} {
 10644  		t.Run(tt.name, func(t *testing.T) {
 10645  			before, after := tt.before, tt.after
 10646  			acct.targetTier = before.targetTier
 10647  			acct.maxBondedAmt = before.maxBondedAmt
 10648  			tDcrWallet.bal = &asset.Balance{Available: tt.bal}
 10649  
 10650  			if tt.addOtherDC {
 10651  				dc, _, acct := testDexConnection(rig.core.ctx, rig.crypter.(*tCrypter))
 10652  				acct.host = "someotherhost.com"
 10653  				rig.core.conns[acct.host] = dc
 10654  				defer delete(rig.core.conns, acct.host)
 10655  				acct.bondAsset = bondAsset.ID
 10656  				acct.targetTier = 1
 10657  			}
 10658  
 10659  			if err := rig.core.UpdateBondOptions(&tt.form); err != nil {
 10660  				if tt.wantErr {
 10661  					return
 10662  				}
 10663  				t.Fatalf("UpdateBondOptions error: %v", err)
 10664  			}
 10665  			if tt.wantErr {
 10666  				t.Fatalf("No error when one was expected")
 10667  			}
 10668  
 10669  			if acct.targetTier != after.targetTier {
 10670  				t.Fatalf("Wrong targetTier. %d != %d", acct.targetTier, after.targetTier)
 10671  			}
 10672  			if acct.maxBondedAmt != after.maxBondedAmt {
 10673  				t.Fatalf("Wrong maxBondedAmt. %d != %d", acct.maxBondedAmt, after.maxBondedAmt)
 10674  			}
 10675  			if tDcrWallet.reserves.Load() != tt.expReserves {
 10676  				t.Fatalf("Wrong reserves. %d != %d", tDcrWallet.reserves.Load(), tt.expReserves)
 10677  			}
 10678  		})
 10679  	}
 10680  }
 10681  
 10682  func TestRotateBonds(t *testing.T) {
 10683  	const feeRate = 50
 10684  
 10685  	rig := newTestRig()
 10686  	defer rig.shutdown()
 10687  	rig.core.Login(tPW)
 10688  
 10689  	acct := rig.dc.acct
 10690  	acct.isAuthed = true
 10691  
 10692  	dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID)
 10693  	dcrWallet.Wallet = &TFeeRater{tDcrWallet, feeRate}
 10694  	rig.core.wallets[tUTXOAssetA.ID] = dcrWallet
 10695  	bondAsset := dcrBondAsset
 10696  	bondFeeBuffer := tDcrWallet.BondsFeeBuffer(feeRate)
 10697  	maxBondedPerTier := maxBondedMult * bondAsset.Amt
 10698  
 10699  	now := uint64(time.Now().Unix())
 10700  	bondExpiry := rig.dc.config().BondExpiry
 10701  	// bondDuration := minBondLifetime(rig.core.net, bondExpiry)
 10702  	locktimeThresh := now + bondExpiry
 10703  	pBuffer := uint64(pendingBuffer(rig.core.net))
 10704  	mergeableLocktimeThresh := locktimeThresh + bondExpiry/4 + pBuffer
 10705  	// unexpired := locktimeThresh + 1
 10706  	locktimeExpired := locktimeThresh - 1
 10707  	locktimeRefundable := now - 1
 10708  	weakTimeThresh := locktimeThresh + pBuffer
 10709  
 10710  	run := func(wantPending, wantExpired int, expectedReserves uint64) {
 10711  		ctx, cancel := context.WithTimeout(rig.core.ctx, time.Second)
 10712  		rig.core.rotateBonds(ctx)
 10713  		cancel()
 10714  
 10715  		t.Helper()
 10716  		if len(acct.pendingBonds) != wantPending {
 10717  			t.Fatalf("wanted %d pending bonds, got %d", wantPending, len(acct.pendingBonds))
 10718  		}
 10719  		if len(acct.expiredBonds) != wantExpired {
 10720  			t.Fatalf("wanted %d expired bonds, got %d", wantExpired, len(acct.expiredBonds))
 10721  		}
 10722  		if tDcrWallet.reserves.Load() != expectedReserves {
 10723  			t.Fatalf("wrong reserves. expected %d, got %d", expectedReserves, tDcrWallet.reserves.Load())
 10724  		}
 10725  	}
 10726  
 10727  	// No bonds, target tier 1. Should create a new bond and add it to pending.
 10728  	var targetTier uint64 = 1
 10729  	acct.targetTier = targetTier
 10730  	acct.maxBondedAmt = maxBondedPerTier * targetTier
 10731  	acct.bondAsset = bondAsset.ID
 10732  	tDcrWallet.bal = &asset.Balance{Available: bondAsset.Amt*targetTier + bondFeeBuffer}
 10733  	rig.queuePrevalidateBond()
 10734  	run(1, 0, bondAsset.Amt+bondFeeBuffer)
 10735  
 10736  	// Post and then expire the bond. This first bond should move to expired and we
 10737  	// should create another bond.
 10738  	acct.bonds, acct.pendingBonds = acct.pendingBonds, nil
 10739  	acct.bonds[0].LockTime = locktimeExpired
 10740  	rig.queuePrevalidateBond()
 10741  	// The newly expired bond will be refunded in time to fund our next round,
 10742  	// so we only need fees reserved.
 10743  	run(1, 1, bondFeeBuffer)
 10744  
 10745  	// If the live bond is closer to expiration, the expired bond won't be
 10746  	// ready in time, so we'll need more reserves.
 10747  	acct.bonds, acct.pendingBonds = acct.pendingBonds, nil
 10748  	acct.bonds[0].LockTime = weakTimeThresh + 1
 10749  	run(0, 1, bondAsset.Amt+bondFeeBuffer)
 10750  
 10751  	// Make the live bond weak. Should get a pending bond. Only fees reserves,
 10752  	// because we still have an expired bond.
 10753  	acct.bonds[0].LockTime = weakTimeThresh - 1
 10754  	rig.queuePrevalidateBond()
 10755  	run(1, 1, bondFeeBuffer)
 10756  
 10757  	// Refund the expired bond
 10758  	acct.expiredBonds[0].LockTime = locktimeRefundable
 10759  	tDcrWallet.contractExpired = true
 10760  	tDcrWallet.refundBondCoin = &tCoin{}
 10761  	run(1, 0, bondAsset.Amt+bondFeeBuffer)
 10762  
 10763  	acct.targetTier = 2
 10764  	acct.bonds = nil
 10765  	rig.queuePrevalidateBond()
 10766  	run(2, 0, bondAsset.Amt*2+bondFeeBuffer)
 10767  
 10768  	// Check that a new bond will be scheduled for merge with an existing bond
 10769  	// if the locktime is not too soon.
 10770  	acct.bonds = append(acct.bonds, acct.pendingBonds[0])
 10771  	acct.pendingBonds = nil
 10772  	acct.bonds[0].LockTime = mergeableLocktimeThresh + 5
 10773  	rig.queuePrevalidateBond()
 10774  	run(1, 0, 2*bondAsset.Amt+bondFeeBuffer)
 10775  	mergingBond := acct.pendingBonds[0]
 10776  	if mergingBond.LockTime != acct.bonds[0].LockTime {
 10777  		t.Fatalf("Mergeable bond was not merged")
 10778  	}
 10779  
 10780  	// Same thing, but without the merge, just to check our threshold calc.
 10781  	acct.pendingBonds = nil
 10782  	acct.bonds[0].LockTime = mergeableLocktimeThresh - 1
 10783  	rig.queuePrevalidateBond()
 10784  	run(1, 0, 2*bondAsset.Amt+bondFeeBuffer)
 10785  	unmergingBond := acct.pendingBonds[0]
 10786  	if unmergingBond.LockTime == acct.bonds[0].LockTime {
 10787  		t.Fatalf("Unmergeable bond was scheduled for merged")
 10788  	}
 10789  }
 10790  
 10791  func TestFindBondKeyIdx(t *testing.T) {
 10792  	rig := newTestRig()
 10793  	defer rig.shutdown()
 10794  	rig.core.Login(tPW)
 10795  
 10796  	pkhEqualFnFn := func(find bool) func(bondKey *secp256k1.PrivateKey) bool {
 10797  		return func(bondKey *secp256k1.PrivateKey) bool {
 10798  			return find
 10799  		}
 10800  	}
 10801  	tests := []struct {
 10802  		name       string
 10803  		pkhEqualFn func(bondKey *secp256k1.PrivateKey) bool
 10804  		wantErr    bool
 10805  	}{{
 10806  		name:       "ok",
 10807  		pkhEqualFn: pkhEqualFnFn(true),
 10808  	}, {
 10809  		name:       "cant find",
 10810  		pkhEqualFn: pkhEqualFnFn(false),
 10811  		wantErr:    true,
 10812  	}}
 10813  
 10814  	for _, test := range tests {
 10815  		t.Run(test.name, func(t *testing.T) {
 10816  			_, err := rig.core.findBondKeyIdx(test.pkhEqualFn, 0)
 10817  			if test.wantErr {
 10818  				if err == nil {
 10819  					t.Fatal("expected error")
 10820  				}
 10821  				return
 10822  			}
 10823  			if err != nil {
 10824  				t.Fatalf("unexpected error: %v", err)
 10825  			}
 10826  		})
 10827  	}
 10828  }
 10829  
 10830  func TestFindBond(t *testing.T) {
 10831  	rig := newTestRig()
 10832  	defer rig.shutdown()
 10833  	dcrWallet, tDcrWallet := newTWallet(tUTXOAssetA.ID)
 10834  	rig.core.wallets[tUTXOAssetA.ID] = dcrWallet
 10835  	rig.core.Login(tPW)
 10836  
 10837  	bd := &asset.BondDetails{
 10838  		Bond: &asset.Bond{
 10839  			Amount:  tFee,
 10840  			AssetID: tUTXOAssetA.ID,
 10841  		},
 10842  		LockTime: time.Now(),
 10843  		CheckPrivKey: func(bondKey *secp256k1.PrivateKey) bool {
 10844  			return true
 10845  		},
 10846  	}
 10847  	msgBond := &msgjson.Bond{
 10848  		Version: 0,
 10849  		AssetID: tUTXOAssetA.ID,
 10850  	}
 10851  
 10852  	tests := []struct {
 10853  		name        string
 10854  		findBond    *asset.BondDetails
 10855  		findBondErr error
 10856  		wantStr     uint32
 10857  	}{{
 10858  		name:     "ok",
 10859  		findBond: bd,
 10860  		wantStr:  1,
 10861  	}, {
 10862  		name:        "find bond error",
 10863  		findBondErr: errors.New("some error"),
 10864  	}}
 10865  
 10866  	for _, test := range tests {
 10867  		t.Run(test.name, func(t *testing.T) {
 10868  			tDcrWallet.findBond = test.findBond
 10869  			tDcrWallet.findBondErr = test.findBondErr
 10870  			str, _ := rig.core.findBond(rig.dc, msgBond)
 10871  			if str != test.wantStr {
 10872  				t.Fatalf("wanted str %d but got %d", test.wantStr, str)
 10873  			}
 10874  		})
 10875  	}
 10876  }
 10877  
 10878  func TestNetworkFeeRate(t *testing.T) {
 10879  	rig := newTestRig()
 10880  	defer rig.shutdown()
 10881  
 10882  	assetID := tUTXOAssetA.ID
 10883  	wallet, tWallet := newTWallet(assetID)
 10884  	rig.core.wallets[assetID] = wallet
 10885  
 10886  	const feeRaterRate = 50
 10887  	dumbWallet := wallet.Wallet
 10888  	wallet.Wallet = &TFeeRater{
 10889  		TXCWallet: tWallet,
 10890  		feeRate:   feeRaterRate,
 10891  	}
 10892  	if r := rig.core.NetworkFeeRate(assetID); r != feeRaterRate {
 10893  		t.Fatalf("FeeRater not working. %d != %d", r, feeRaterRate)
 10894  	}
 10895  	wallet.Wallet = dumbWallet
 10896  
 10897  	const bookFeedFeeRate = 60
 10898  	book := newBookie(rig.dc, assetID, tUTXOAssetB.ID, nil, tLogger)
 10899  	rig.dc.books[tDcrBtcMktName] = book
 10900  	book.logEpochReport(&msgjson.EpochReportNote{BaseFeeRate: bookFeedFeeRate})
 10901  	if r := rig.core.NetworkFeeRate(assetID); r != bookFeedFeeRate {
 10902  		t.Fatalf("Book feed fee rate not working. %d != %d", r, bookFeedFeeRate)
 10903  	}
 10904  	delete(rig.dc.books, tDcrBtcMktName)
 10905  
 10906  	const serverFeeRate = 70
 10907  	rig.ws.queueResponse(msgjson.FeeRateRoute, func(msg *msgjson.Message, f msgFunc) error {
 10908  		resp, _ := msgjson.NewResponse(msg.ID, serverFeeRate, nil)
 10909  		f(resp)
 10910  		return nil
 10911  	})
 10912  	if r := rig.core.NetworkFeeRate(assetID); r != serverFeeRate {
 10913  		t.Fatalf("Server fee rate not working. %d != %d", r, serverFeeRate)
 10914  	}
 10915  }
 10916  
 10917  func TestPokesCacheInit(t *testing.T) {
 10918  	tPokes := []*db.Notification{
 10919  		{DetailText: "poke 1"},
 10920  		{DetailText: "poke 2"},
 10921  		{DetailText: "poke 3"},
 10922  		{DetailText: "poke 4"},
 10923  		{DetailText: "poke 5"},
 10924  	}
 10925  	{
 10926  		pokesCapacity := 6
 10927  		c := newPokesCache(pokesCapacity)
 10928  		c.init(tPokes)
 10929  
 10930  		// Check if the cache is initialized correctly
 10931  		if len(c.cache) != 5 {
 10932  			t.Errorf("Expected cache length %d, got %d", len(tPokes), len(c.cache))
 10933  		}
 10934  
 10935  		if c.cursor != 5 {
 10936  			t.Errorf("Expected cursor %d, got %d", len(tPokes)%pokesCapacity, c.cursor)
 10937  		}
 10938  
 10939  		// Check if the cache contains the correct pokes
 10940  		for i, poke := range tPokes {
 10941  			if c.cache[i] != poke {
 10942  				t.Errorf("Expected poke %v at index %d, got %v", poke, i, c.cache[i])
 10943  			}
 10944  		}
 10945  	}
 10946  	{
 10947  		pokesCapacity := 4
 10948  		c := newPokesCache(pokesCapacity)
 10949  		c.init(tPokes)
 10950  
 10951  		// Check if the cache is initialized correctly
 10952  		if len(c.cache) != 1 {
 10953  			t.Errorf("Expected cache length %d, got %d", 1, len(c.cache))
 10954  		}
 10955  
 10956  		if c.cursor != 1 {
 10957  			t.Errorf("Expected cursor %d, got %d", 1, c.cursor)
 10958  		}
 10959  
 10960  		// Check if the cache contains the correct pokes
 10961  		for i, poke := range tPokes[:len(tPokes)-pokesCapacity] {
 10962  			if c.cache[i] != poke {
 10963  				t.Errorf("Expected poke %v at index %d, got %v", poke, i, c.cache[i])
 10964  			}
 10965  		}
 10966  	}
 10967  }
 10968  
 10969  func TestPokesAdd(t *testing.T) {
 10970  	tPokes := []*db.Notification{
 10971  		{DetailText: "poke 1"},
 10972  		{DetailText: "poke 2"},
 10973  		{DetailText: "poke 3"},
 10974  		{DetailText: "poke 4"},
 10975  		{DetailText: "poke 5"},
 10976  	}
 10977  	tNewPoke := &db.Notification{
 10978  		DetailText: "poke 6",
 10979  	}
 10980  	{
 10981  		pokesCapacity := 6
 10982  		c := newPokesCache(pokesCapacity)
 10983  		c.init(tPokes)
 10984  		c.add(tNewPoke)
 10985  
 10986  		// Check if the cache is updated correctly
 10987  		if len(c.cache) != 6 {
 10988  			t.Errorf("Expected cache length %d, got %d", len(tPokes), len(c.cache))
 10989  		}
 10990  
 10991  		if c.cursor != 0 {
 10992  			t.Errorf("Expected cursor %d, got %d", 0, c.cursor)
 10993  		}
 10994  
 10995  		// Check if the cache contains the correct pokes
 10996  		tAllPokes := append(tPokes, tNewPoke)
 10997  		for i, poke := range tAllPokes {
 10998  			if c.cache[i] != poke {
 10999  				t.Errorf("Expected poke %v at index %d, got %v", poke, i, c.cache[i])
 11000  			}
 11001  		}
 11002  	}
 11003  	{
 11004  		pokesCapacity := 5
 11005  		c := newPokesCache(pokesCapacity)
 11006  		c.init(tPokes)
 11007  		c.add(tNewPoke)
 11008  
 11009  		// Check if the cache is updated correctly
 11010  		if len(c.cache) != pokesCapacity {
 11011  			t.Errorf("Expected cache length %d, got %d", pokesCapacity, len(c.cache))
 11012  		}
 11013  
 11014  		if c.cursor != 1 {
 11015  			t.Errorf("Expected cursor %d, got %d", 1, c.cursor)
 11016  		}
 11017  
 11018  		// Check if the cache contains the correct pokes
 11019  		tAllPokes := make([]*db.Notification, 0)
 11020  		tAllPokes = append(tAllPokes, tNewPoke)
 11021  		tAllPokes = append(tAllPokes, tPokes[1:]...)
 11022  		for i, poke := range tAllPokes {
 11023  			if c.cache[i] != poke {
 11024  				t.Errorf("Expected poke %v at index %d, got %v", poke, i, c.cache[i])
 11025  			}
 11026  		}
 11027  	}
 11028  }
 11029  
 11030  func TestPokesCachePokes(t *testing.T) {
 11031  	tPokes := []*db.Notification{
 11032  		{TimeStamp: 1, DetailText: "poke 1"},
 11033  		{TimeStamp: 2, DetailText: "poke 2"},
 11034  		{TimeStamp: 3, DetailText: "poke 3"},
 11035  		{TimeStamp: 4, DetailText: "poke 4"},
 11036  		{TimeStamp: 5, DetailText: "poke 5"},
 11037  	}
 11038  	{
 11039  		pokesCapacity := 6
 11040  		c := newPokesCache(pokesCapacity)
 11041  		c.init(tPokes)
 11042  		pokes := c.pokes()
 11043  
 11044  		// Check if the result length is correct
 11045  		if len(pokes) != len(tPokes) {
 11046  			t.Errorf("Expected pokes length %d, got %d", len(tPokes), len(pokes))
 11047  		}
 11048  
 11049  		// Check if the result contains the correct pokes
 11050  		for i, poke := range tPokes {
 11051  			if pokes[i] != poke {
 11052  				t.Errorf("Expected poke %v at index %d, got %v", poke, i, pokes[i])
 11053  			}
 11054  		}
 11055  	}
 11056  	{
 11057  		pokesCapacity := 5
 11058  		tNewPoke := &db.Notification{
 11059  			TimeStamp:  6,
 11060  			DetailText: "poke 6",
 11061  		}
 11062  		c := newPokesCache(pokesCapacity)
 11063  		c.init(tPokes)
 11064  		c.add(tNewPoke)
 11065  		pokes := c.pokes()
 11066  
 11067  		// Check if the result length is correct
 11068  		if len(pokes) != pokesCapacity {
 11069  			t.Errorf("Expected cache length %d, got %d", 1, len(pokes))
 11070  		}
 11071  
 11072  		tAllPokes := append(tPokes[1:], tNewPoke)
 11073  		// Check if the result contains the correct pokes
 11074  		for i, poke := range tAllPokes {
 11075  			if pokes[i] != poke {
 11076  				t.Errorf("Expected poke %v at index %d, got %v", poke, i, pokes[i])
 11077  			}
 11078  		}
 11079  	}
 11080  }
 11081  
 11082  func TestTradingLimits(t *testing.T) {
 11083  	rig := newTestRig()
 11084  	defer rig.shutdown()
 11085  
 11086  	checkTradingLimits := func(expectedUserParcels, expectedParcelLimit uint32) {
 11087  		t.Helper()
 11088  
 11089  		userParcels, parcelLimit, err := rig.core.TradingLimits(tDexHost)
 11090  		if err != nil {
 11091  			t.Fatalf("unexpected error: %v", err)
 11092  		}
 11093  
 11094  		if userParcels != expectedUserParcels {
 11095  			t.Fatalf("expected user parcels %d, got %d", expectedUserParcels, userParcels)
 11096  		}
 11097  
 11098  		if parcelLimit != expectedParcelLimit {
 11099  			t.Fatalf("expected parcel limit %d, got %d", expectedParcelLimit, parcelLimit)
 11100  		}
 11101  	}
 11102  
 11103  	rig.dc.acct.rep.BondedTier = 10
 11104  	book := newBookie(rig.dc, tUTXOAssetA.ID, tUTXOAssetB.ID, nil, tLogger)
 11105  	rig.dc.books[tDcrBtcMktName] = book
 11106  	checkTradingLimits(0, 20)
 11107  
 11108  	oids := []order.OrderID{
 11109  		{0x01}, {0x02}, {0x03}, {0x04}, {0x05},
 11110  	}
 11111  
 11112  	// Add an epoch order, 2 lots not likely taker
 11113  	ord := &order.LimitOrder{
 11114  		Force: order.StandingTiF,
 11115  		P:     order.Prefix{ServerTime: time.Now()},
 11116  		T: order.Trade{
 11117  			Sell:     true,
 11118  			Quantity: dcrBtcLotSize * 2,
 11119  		},
 11120  	}
 11121  	tracker := &trackedTrade{
 11122  		Order:  ord,
 11123  		preImg: newPreimage(),
 11124  		mktID:  tDcrBtcMktName,
 11125  		db:     rig.db,
 11126  		dc:     rig.dc,
 11127  		metaData: &db.OrderMetaData{
 11128  			Status: order.OrderStatusEpoch,
 11129  		},
 11130  	}
 11131  	rig.dc.trades[oids[0]] = tracker
 11132  	checkTradingLimits(2, 20)
 11133  
 11134  	// Add another epoch order, 2 lots, likely taker, so 2x
 11135  	ord = &order.LimitOrder{
 11136  		Force: order.ImmediateTiF,
 11137  		P:     order.Prefix{ServerTime: time.Now()},
 11138  		T: order.Trade{
 11139  			Sell:     true,
 11140  			Quantity: dcrBtcLotSize * 2,
 11141  		},
 11142  	}
 11143  	tracker = &trackedTrade{
 11144  		Order:  ord,
 11145  		preImg: newPreimage(),
 11146  		mktID:  tDcrBtcMktName,
 11147  		db:     rig.db,
 11148  		dc:     rig.dc,
 11149  		metaData: &db.OrderMetaData{
 11150  			Status: order.OrderStatusEpoch,
 11151  		},
 11152  	}
 11153  	rig.dc.trades[oids[1]] = tracker
 11154  	checkTradingLimits(6, 20)
 11155  
 11156  	// Add partially filled booked order
 11157  	ord = &order.LimitOrder{
 11158  		P: order.Prefix{ServerTime: time.Now()},
 11159  		T: order.Trade{
 11160  			Sell:     true,
 11161  			Quantity: dcrBtcLotSize * 2,
 11162  			FillAmt:  dcrBtcLotSize,
 11163  		},
 11164  	}
 11165  	tracker = &trackedTrade{
 11166  		Order:  ord,
 11167  		preImg: newPreimage(),
 11168  		mktID:  tDcrBtcMktName,
 11169  		db:     rig.db,
 11170  		dc:     rig.dc,
 11171  		metaData: &db.OrderMetaData{
 11172  			Status: order.OrderStatusBooked,
 11173  		},
 11174  	}
 11175  	rig.dc.trades[oids[2]] = tracker
 11176  	checkTradingLimits(7, 20)
 11177  
 11178  	// Add settling match to the booked order
 11179  	tracker.matches = map[order.MatchID]*matchTracker{
 11180  		{0x01}: {
 11181  			MetaMatch: db.MetaMatch{
 11182  				UserMatch: &order.UserMatch{
 11183  					Quantity: dcrBtcLotSize,
 11184  				},
 11185  				MetaData: &db.MatchMetaData{
 11186  					Proof: db.MatchProof{},
 11187  				},
 11188  			},
 11189  		},
 11190  	}
 11191  	checkTradingLimits(8, 20)
 11192  }
 11193  
 11194  func TestTakeAction(t *testing.T) {
 11195  	rig := newTestRig()
 11196  	defer rig.shutdown()
 11197  
 11198  	coinID := encode.RandomBytes(32)
 11199  	uniqueID := dex.Bytes(coinID).String()
 11200  
 11201  	newMatch := func() *matchTracker {
 11202  		var matchID order.MatchID
 11203  		copy(matchID[:], encode.RandomBytes(32))
 11204  		return &matchTracker{
 11205  			MetaMatch: db.MetaMatch{
 11206  				UserMatch: &order.UserMatch{
 11207  					Status:  order.MatchComplete,
 11208  					MatchID: matchID,
 11209  					Side:    order.Taker,
 11210  				},
 11211  				MetaData: &db.MatchMetaData{},
 11212  			},
 11213  		}
 11214  	}
 11215  	rightMatch := newMatch()
 11216  	rightMatch.MetaData.Proof.TakerRedeem = coinID
 11217  	rightMatch.redemptionRejected = true
 11218  
 11219  	wrongMatch := newMatch()
 11220  	wrongMatch.MetaData.Proof.TakerRedeem = encode.RandomBytes(31)
 11221  
 11222  	makerMatch := newMatch()
 11223  	makerMatch.Status = order.MakerRedeemed
 11224  	makerMatch.MetaData.Proof.MakerRedeem = coinID
 11225  	makerMatch.Side = order.Maker
 11226  
 11227  	tracker := &trackedTrade{
 11228  		matches: map[order.MatchID]*matchTracker{
 11229  			rightMatch.MatchID: rightMatch,
 11230  			wrongMatch.MatchID: wrongMatch,
 11231  			makerMatch.MatchID: makerMatch,
 11232  		},
 11233  	}
 11234  
 11235  	var oid order.OrderID
 11236  	copy(oid[:], encode.RandomBytes(32))
 11237  
 11238  	rig.dc.trades[oid] = tracker
 11239  
 11240  	requestData := []byte(fmt.Sprintf(`{"orderID":"abcd","coinID":"%s","retry":true}`, dex.Bytes(coinID)))
 11241  
 11242  	err := rig.core.TakeAction(0, ActionIDRedeemRejected, requestData)
 11243  	if err == nil {
 11244  		t.Fatalf("expected error for wrong order ID but got nothing")
 11245  	}
 11246  
 11247  	rig.core.requestedActions[uniqueID] = nil
 11248  	requestData = []byte(fmt.Sprintf(`{"orderID":"%s","coinID":"%s","retry":false}`, oid, dex.Bytes(coinID)))
 11249  
 11250  	err = rig.core.TakeAction(0, ActionIDRedeemRejected, requestData)
 11251  	if err != nil {
 11252  		t.Fatalf("error for retry=false: %v", err)
 11253  	}
 11254  	if len(rig.core.requestedActions) != 0 {
 11255  		t.Fatal("requested action not removed")
 11256  	}
 11257  
 11258  	requestData = []byte(fmt.Sprintf(`{"orderID":"%s","coinID":"%s","retry":true}`, oid, dex.Bytes(coinID)))
 11259  	err = rig.core.TakeAction(0, ActionIDRedeemRejected, requestData)
 11260  	if err != nil {
 11261  		t.Fatalf("error for taker retry=true: %v", err)
 11262  	}
 11263  
 11264  	if len(rightMatch.MetaData.Proof.TakerRedeem) != 0 {
 11265  		t.Fatalf("taker redemption not cleared")
 11266  	}
 11267  	if len(wrongMatch.MetaData.Proof.TakerRedeem) == 0 {
 11268  		t.Fatalf("wrong taker redemption cleared")
 11269  	}
 11270  
 11271  	makerMatch.redemptionRejected = true
 11272  	err = rig.core.TakeAction(0, ActionIDRedeemRejected, requestData)
 11273  	if err != nil {
 11274  		t.Fatalf("error for maker retry=true: %v", err)
 11275  	}
 11276  	if len(makerMatch.MetaData.Proof.MakerRedeem) != 0 {
 11277  		t.Fatalf("maker redemption not cleared")
 11278  	}
 11279  
 11280  }