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