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

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package core
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"crypto/sha256"
    10  	"encoding/binary"
    11  	"encoding/csv"
    12  	"encoding/hex"
    13  	"encoding/json"
    14  	"errors"
    15  	"fmt"
    16  	"math"
    17  	"net"
    18  	"net/url"
    19  	"os"
    20  	"path/filepath"
    21  	"runtime"
    22  	"runtime/debug"
    23  	"sort"
    24  	"strconv"
    25  	"strings"
    26  	"sync"
    27  	"sync/atomic"
    28  	"time"
    29  
    30  	"decred.org/dcrdex/client/asset"
    31  	"decred.org/dcrdex/client/comms"
    32  	"decred.org/dcrdex/client/db"
    33  	"decred.org/dcrdex/client/db/bolt"
    34  	"decred.org/dcrdex/client/mnemonic"
    35  	"decred.org/dcrdex/client/orderbook"
    36  	"decred.org/dcrdex/dex"
    37  	"decred.org/dcrdex/dex/calc"
    38  	"decred.org/dcrdex/dex/config"
    39  	"decred.org/dcrdex/dex/encode"
    40  	"decred.org/dcrdex/dex/encrypt"
    41  	"decred.org/dcrdex/dex/msgjson"
    42  	"decred.org/dcrdex/dex/order"
    43  	"decred.org/dcrdex/dex/wait"
    44  	"decred.org/dcrdex/server/account"
    45  	serverdex "decred.org/dcrdex/server/dex"
    46  	"github.com/decred/dcrd/crypto/blake256"
    47  	"github.com/decred/dcrd/dcrec/secp256k1/v4"
    48  	"github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
    49  	"github.com/decred/dcrd/hdkeychain/v3"
    50  	"github.com/decred/go-socks/socks"
    51  	"golang.org/x/text/language"
    52  	"golang.org/x/text/message"
    53  )
    54  
    55  const (
    56  	// tickCheckDivisions is how many times to tick trades per broadcast timeout
    57  	// interval. e.g. 12 min btimeout / 8 divisions = 90 sec between checks.
    58  	tickCheckDivisions = 8
    59  	// defaultTickInterval is the tick interval used before the broadcast
    60  	// timeout is known (e.g. startup with down server).
    61  	defaultTickInterval = 30 * time.Second
    62  
    63  	marketBuyRedemptionSlippageBuffer = 2
    64  
    65  	// preimageReqTimeout the server's preimage request timeout period. When
    66  	// considered with a market's epoch duration, this is used to detect when an
    67  	// order should have gone through matching for a certain epoch. TODO:
    68  	// consider sharing const for the preimage timeout with the server packages,
    69  	// or a config response field if it should be considered variable.
    70  	preimageReqTimeout = 20 * time.Second
    71  
    72  	// wsMaxAnomalyCount is the maximum websocket connection anomaly after which
    73  	// a client receives a notification to check their connectivity.
    74  	wsMaxAnomalyCount = 3
    75  	// If a client's websocket connection to a server disconnects before
    76  	// wsAnomalyDuration since last connect time, the client's websocket
    77  	// connection anomaly count is increased.
    78  	wsAnomalyDuration = 60 * time.Minute
    79  
    80  	// This is a configurable server parameter, but we're assuming servers have
    81  	// changed it from the default , We're using this for the v1 ConnectResult,
    82  	// where we don't have the necessary information to calculate our bonded
    83  	// tier, so we calculate our bonus/revoked tier from the score in the
    84  	// ConnectResult.
    85  	defaultPenaltyThreshold = 20
    86  
    87  	// legacySeedLength is the length of the generated app seed used for app protection.
    88  	legacySeedLength = 64
    89  
    90  	// pokesCapacity is the maximum number of poke notifications that
    91  	// will be cached.
    92  	pokesCapacity = 100
    93  
    94  	// walletLockTimeout is the default timeout used when locking wallets.
    95  	walletLockTimeout = 5 * time.Second
    96  )
    97  
    98  var (
    99  	unbip = dex.BipIDSymbol
   100  	// The coin waiters will query for transaction data every recheckInterval.
   101  	recheckInterval = time.Second * 5
   102  	// When waiting for a wallet to sync, a SyncStatus check will be performed
   103  	// every syncTickerPeriod. var instead of const for testing purposes.
   104  	syncTickerPeriod = 3 * time.Second
   105  	// supportedAPIVers are the DEX server API versions this client is capable
   106  	// of communicating with.
   107  	//
   108  	// NOTE: API version may change at any time. Keep this in mind when
   109  	// updating the API. Long-running operations may start and end with
   110  	// differing versions.
   111  	supportedAPIVers = []int32{serverdex.V1APIVersion}
   112  	// ActiveOrdersLogoutErr is returned from logout when there are active
   113  	// orders.
   114  	ActiveOrdersLogoutErr = errors.New("cannot log out with active orders")
   115  	// walletDisabledErrStr is the error message returned when trying to use a
   116  	// disabled wallet.
   117  	walletDisabledErrStr = "%s wallet is disabled"
   118  
   119  	errTimeout = errors.New("timeout")
   120  )
   121  
   122  type dexTicker struct {
   123  	dur int64 // atomic
   124  	*time.Ticker
   125  }
   126  
   127  func newDexTicker(dur time.Duration) *dexTicker {
   128  	return &dexTicker{
   129  		dur:    int64(dur),
   130  		Ticker: time.NewTicker(dur),
   131  	}
   132  }
   133  
   134  func (dt *dexTicker) Reset(dur time.Duration) {
   135  	atomic.StoreInt64(&dt.dur, int64(dur))
   136  	dt.Ticker.Reset(dur)
   137  }
   138  
   139  func (dt *dexTicker) Dur() time.Duration {
   140  	return time.Duration(atomic.LoadInt64(&dt.dur))
   141  }
   142  
   143  type pendingFeeState struct {
   144  	confs uint32
   145  	asset uint32
   146  }
   147  
   148  // dexConnection is the websocket connection and the DEX configuration.
   149  type dexConnection struct {
   150  	comms.WsConn
   151  	connMaster *dex.ConnectionMaster
   152  	log        dex.Logger
   153  	acct       *dexAccount
   154  	notify     func(Notification)
   155  	ticker     *dexTicker
   156  	// apiVer is an atomic. An uninitiated connection should be set to -1.
   157  	apiVer int32
   158  
   159  	assetsMtx sync.RWMutex
   160  	assets    map[uint32]*dex.Asset
   161  
   162  	cfgMtx sync.RWMutex
   163  	cfg    *msgjson.ConfigResult
   164  
   165  	booksMtx sync.RWMutex
   166  	books    map[string]*bookie
   167  
   168  	// tradeMtx is used to synchronize access to the trades map.
   169  	tradeMtx sync.RWMutex
   170  	// trades tracks outstanding orders issued by this client.
   171  	trades map[order.OrderID]*trackedTrade
   172  	// inFlightOrders tracks orders issued by this client that have not been
   173  	// processed by a dex server.
   174  	inFlightOrders map[uint64]*InFlightOrder
   175  
   176  	// A map linking cancel order IDs to trade order IDs.
   177  	cancelsMtx sync.RWMutex
   178  	cancels    map[order.OrderID]order.OrderID
   179  
   180  	blindCancelsMtx sync.Mutex
   181  	blindCancels    map[order.OrderID]order.Preimage
   182  
   183  	epochMtx sync.RWMutex
   184  	epoch    map[string]uint64
   185  	// resolvedEpoch differs from epoch in that an epoch is not considered
   186  	// resolved until all of our orders are out of the epoch queue. i.e.
   187  	// we have received match or nomatch notification for all of our orders
   188  	// from the epoch.
   189  	resolvedEpoch map[string]uint64
   190  
   191  	// connectionStatus is a best guess on the ws connection status.
   192  	connectionStatus uint32
   193  
   194  	reportingConnects uint32
   195  
   196  	spotsMtx sync.RWMutex
   197  	spots    map[string]*msgjson.Spot
   198  
   199  	// anomaliesCount tracks client's connection anomalies.
   200  	anomaliesCount uint32 // atomic
   201  	lastConnectMtx sync.RWMutex
   202  	lastConnect    time.Time
   203  }
   204  
   205  // DefaultResponseTimeout is the default timeout for responses after a request is
   206  // successfully sent.
   207  const (
   208  	DefaultResponseTimeout = comms.DefaultResponseTimeout
   209  	fundingTxWait          = time.Minute // TODO: share var with server/market or put in config
   210  )
   211  
   212  // running returns the status of the provided market.
   213  func (dc *dexConnection) running(mkt string) bool {
   214  	dc.cfgMtx.RLock()
   215  	defer dc.cfgMtx.RUnlock()
   216  	mktCfg := dc.findMarketConfig(mkt)
   217  	if mktCfg == nil {
   218  		return false // not found means not running
   219  	}
   220  	return mktCfg.Running()
   221  }
   222  
   223  // status returns the status of the connection to the dex.
   224  func (dc *dexConnection) status() comms.ConnectionStatus {
   225  	return comms.ConnectionStatus(atomic.LoadUint32(&dc.connectionStatus))
   226  }
   227  
   228  func (dc *dexConnection) config() *msgjson.ConfigResult {
   229  	dc.cfgMtx.RLock()
   230  	defer dc.cfgMtx.RUnlock()
   231  	return dc.cfg
   232  }
   233  
   234  func (dc *dexConnection) bondAsset(assetID uint32) (*msgjson.BondAsset, uint64) {
   235  	assetSymb := dex.BipIDSymbol(assetID)
   236  	dc.cfgMtx.RLock()
   237  	defer dc.cfgMtx.RUnlock()
   238  	if dc.cfg == nil {
   239  		return nil, 0
   240  	}
   241  	bondExpiry := dc.cfg.BondExpiry
   242  	bondAsset := dc.cfg.BondAssets[assetSymb]
   243  	return bondAsset, bondExpiry // bondAsset may be nil
   244  }
   245  
   246  func (dc *dexConnection) bondAssets() (map[uint32]*BondAsset, uint64) {
   247  	bondAssets := make(map[uint32]*BondAsset)
   248  	cfg := dc.config()
   249  	if cfg == nil {
   250  		return nil, 0
   251  	}
   252  	for symb, ba := range cfg.BondAssets {
   253  		assetID, ok := dex.BipSymbolID(symb)
   254  		if !ok {
   255  			continue
   256  		}
   257  		coreBondAsset := BondAsset(*ba)
   258  		bondAssets[assetID] = &coreBondAsset
   259  	}
   260  	return bondAssets, cfg.BondExpiry
   261  }
   262  
   263  func (dc *dexConnection) registerCancelLink(cid, oid order.OrderID) {
   264  	dc.cancelsMtx.Lock()
   265  	dc.cancels[cid] = oid
   266  	dc.cancelsMtx.Unlock()
   267  }
   268  
   269  func (dc *dexConnection) deleteCancelLink(cid order.OrderID) {
   270  	dc.cancelsMtx.Lock()
   271  	delete(dc.cancels, cid)
   272  	dc.cancelsMtx.Unlock()
   273  }
   274  
   275  func (dc *dexConnection) cancelTradeID(cid order.OrderID) (order.OrderID, bool) {
   276  	dc.cancelsMtx.RLock()
   277  	defer dc.cancelsMtx.RUnlock()
   278  	oid, found := dc.cancels[cid]
   279  	return oid, found
   280  }
   281  
   282  // marketConfig is the market's configuration, as returned by the server in the
   283  // 'config' response.
   284  func (dc *dexConnection) marketConfig(mktID string) *msgjson.Market {
   285  	dc.cfgMtx.RLock()
   286  	defer dc.cfgMtx.RUnlock()
   287  	return dc.findMarketConfig(mktID)
   288  }
   289  
   290  func (dc *dexConnection) assetConfig(assetID uint32) *dex.Asset {
   291  	dc.assetsMtx.RLock()
   292  	defer dc.assetsMtx.RUnlock()
   293  	return dc.assets[assetID]
   294  }
   295  
   296  // marketMap creates a map of this DEX's *Market keyed by name/ID,
   297  // [base]_[quote].
   298  func (dc *dexConnection) marketMap() map[string]*Market {
   299  	dc.cfgMtx.RLock()
   300  	cfg := dc.cfg
   301  	dc.cfgMtx.RUnlock()
   302  	if cfg == nil {
   303  		return nil
   304  	}
   305  	mktConfigs := cfg.Markets
   306  
   307  	marketMap := make(map[string]*Market, len(mktConfigs))
   308  	for _, msgMkt := range mktConfigs {
   309  		mkt := coreMarketFromMsgMarket(dc, msgMkt)
   310  		marketMap[mkt.marketName()] = mkt
   311  	}
   312  
   313  	// Populate spots.
   314  	dc.spotsMtx.RLock()
   315  	for mktID, mkt := range marketMap {
   316  		mkt.SpotPrice = dc.spots[mktID]
   317  	}
   318  	dc.spotsMtx.RUnlock()
   319  
   320  	return marketMap
   321  }
   322  
   323  // marketMap creates a map of this DEX's *Market keyed by name/ID,
   324  // [base]_[quote].
   325  func (dc *dexConnection) coreMarket(mktName string) *Market {
   326  	dc.cfgMtx.RLock()
   327  	cfg := dc.cfg
   328  	dc.cfgMtx.RUnlock()
   329  	if cfg == nil {
   330  		return nil
   331  	}
   332  	var mkt *Market
   333  	for _, m := range cfg.Markets {
   334  		if m.Name == mktName {
   335  			mkt = coreMarketFromMsgMarket(dc, m)
   336  			break
   337  		}
   338  	}
   339  	if mkt == nil {
   340  		return nil
   341  	}
   342  
   343  	// Populate spots.
   344  	dc.spotsMtx.RLock()
   345  	mkt.SpotPrice = dc.spots[mktName]
   346  	dc.spotsMtx.RUnlock()
   347  
   348  	return mkt
   349  }
   350  
   351  func coreMarketFromMsgMarket(dc *dexConnection, msgMkt *msgjson.Market) *Market {
   352  	// The presence of the asset for every market was already verified when the
   353  	// dexConnection was created in connectDEX.
   354  	dc.assetsMtx.RLock()
   355  	base, quote := dc.assets[msgMkt.Base], dc.assets[msgMkt.Quote]
   356  	dc.assetsMtx.RUnlock()
   357  
   358  	bconv, qconv := base.UnitInfo.Conventional.ConversionFactor, quote.UnitInfo.Conventional.ConversionFactor
   359  
   360  	mkt := &Market{
   361  		Name:            msgMkt.Name,
   362  		BaseID:          base.ID,
   363  		BaseSymbol:      base.Symbol,
   364  		QuoteID:         quote.ID,
   365  		QuoteSymbol:     quote.Symbol,
   366  		LotSize:         msgMkt.LotSize,
   367  		ParcelSize:      msgMkt.ParcelSize,
   368  		RateStep:        msgMkt.RateStep,
   369  		EpochLen:        msgMkt.EpochLen,
   370  		StartEpoch:      msgMkt.StartEpoch,
   371  		MarketBuyBuffer: msgMkt.MarketBuyBuffer,
   372  		AtomToConv:      float64(bconv) / float64(qconv),
   373  		MinimumRate:     dc.minimumMarketRate(quote, msgMkt.LotSize),
   374  	}
   375  
   376  	trades, inFlight := dc.marketTrades(mkt.marketName())
   377  	mkt.InFlightOrders = inFlight
   378  
   379  	for _, trade := range trades {
   380  		mkt.Orders = append(mkt.Orders, trade.coreOrder())
   381  	}
   382  
   383  	return mkt
   384  }
   385  
   386  func (dc *dexConnection) minimumMarketRate(q *dex.Asset, lotSize uint64) uint64 {
   387  	quoteDust, found := asset.MinimumLotSize(q.ID, q.MaxFeeRate)
   388  	if !found {
   389  		dc.log.Errorf("couldn't find minimum lot size for %s", q.Symbol)
   390  		return 0
   391  	}
   392  	return calc.MinimumMarketRate(lotSize, quoteDust)
   393  }
   394  
   395  // temporaryOrderIDCounter is used for inflight orders and must never be zero
   396  // when used for an inflight order.
   397  var temporaryOrderIDCounter uint64
   398  
   399  // storeInFlightOrder stores an inflight order and returns a generated ID.
   400  func (dc *dexConnection) storeInFlightOrder(ord *Order) uint64 {
   401  	tempID := atomic.AddUint64(&temporaryOrderIDCounter, 1)
   402  	dc.tradeMtx.Lock()
   403  	dc.inFlightOrders[tempID] = &InFlightOrder{
   404  		Order:       ord,
   405  		TemporaryID: tempID,
   406  	}
   407  	dc.tradeMtx.Unlock()
   408  	return tempID
   409  }
   410  
   411  func (dc *dexConnection) deleteInFlightOrder(tempID uint64) {
   412  	dc.tradeMtx.Lock()
   413  	delete(dc.inFlightOrders, tempID)
   414  	dc.tradeMtx.Unlock()
   415  }
   416  
   417  func (dc *dexConnection) trackedTrades() []*trackedTrade {
   418  	dc.tradeMtx.RLock()
   419  	defer dc.tradeMtx.RUnlock()
   420  	allTrades := make([]*trackedTrade, 0, len(dc.trades))
   421  	for _, trade := range dc.trades {
   422  		allTrades = append(allTrades, trade)
   423  	}
   424  	return allTrades
   425  }
   426  
   427  // marketTrades returns a slice of active trades in the trades map and a slice
   428  // of inflight orders in the inFlightOrders map.
   429  func (dc *dexConnection) marketTrades(mktID string) ([]*trackedTrade, []*InFlightOrder) {
   430  	// Copy trades to avoid locking both tradeMtx and trackedTrade.mtx.
   431  	allTrades := dc.trackedTrades()
   432  	trades := make([]*trackedTrade, 0, len(allTrades)) // may over-allocate
   433  	for _, trade := range allTrades {
   434  		if trade.mktID == mktID && trade.isActive() {
   435  			trades = append(trades, trade)
   436  		}
   437  		// Retiring inactive orders is presently the responsibility of ticker.
   438  	}
   439  
   440  	dc.tradeMtx.RLock()
   441  	inFlight := make([]*InFlightOrder, 0, len(dc.inFlightOrders)) // may over-allocate
   442  	for _, ord := range dc.inFlightOrders {
   443  		if ord.MarketID == mktID {
   444  			inFlight = append(inFlight, ord)
   445  		}
   446  	}
   447  	dc.tradeMtx.RUnlock()
   448  	return trades, inFlight
   449  }
   450  
   451  // pendingBonds returns the PendingBondState for all pending bonds. pendingBonds
   452  // should be called with the acct.authMtx locked.
   453  func (dc *dexConnection) pendingBonds() []*PendingBondState {
   454  	pendingBonds := make([]*PendingBondState, len(dc.acct.pendingBonds))
   455  	for i, pb := range dc.acct.pendingBonds {
   456  		bondIDStr := coinIDString(pb.AssetID, pb.CoinID)
   457  		confs := dc.acct.pendingBondsConfs[bondIDStr]
   458  		pendingBonds[i] = &PendingBondState{
   459  			CoinID:  bondIDStr,
   460  			AssetID: pb.AssetID,
   461  			Symbol:  unbip(pb.AssetID),
   462  			Confs:   confs,
   463  		}
   464  	}
   465  	return pendingBonds
   466  }
   467  
   468  func (c *Core) exchangeInfo(dc *dexConnection) *Exchange {
   469  	// Set AcctID to empty string if not registered.
   470  	acctID := dc.acct.ID().String()
   471  	var emptyAcctID account.AccountID
   472  	if dc.acct.ID() == emptyAcctID {
   473  		acctID = ""
   474  	}
   475  
   476  	dc.cfgMtx.RLock()
   477  	cfg := dc.cfg
   478  	dc.cfgMtx.RUnlock()
   479  	if cfg == nil { // no config, assets, or markets data
   480  		return &Exchange{
   481  			Host:             dc.acct.host,
   482  			AcctID:           acctID,
   483  			ConnectionStatus: dc.status(),
   484  			Disabled:         dc.acct.isDisabled(),
   485  			Markets:          make(map[string]*Market),
   486  			Assets:           make(map[uint32]*dex.Asset),
   487  			BondAssets:       make(map[string]*BondAsset),
   488  		}
   489  	}
   490  
   491  	bondAssets := make(map[string]*BondAsset, len(cfg.BondAssets))
   492  	for symb, bondAsset := range cfg.BondAssets {
   493  		assetID, ok := dex.BipSymbolID(symb)
   494  		if !ok || assetID != bondAsset.ID {
   495  			dc.log.Warnf("Invalid bondAssets config with mismatched asset symbol %q and ID %d",
   496  				symb, bondAsset.ID)
   497  		}
   498  		coreBondAsset := BondAsset(*bondAsset) // convert msgjson.BondAsset to core.BondAsset
   499  
   500  		bondAssets[symb] = &coreBondAsset
   501  	}
   502  
   503  	dc.assetsMtx.RLock()
   504  	assets := make(map[uint32]*dex.Asset, len(dc.assets))
   505  	for assetID, dexAsset := range dc.assets {
   506  		assets[assetID] = dexAsset
   507  	}
   508  	dc.assetsMtx.RUnlock()
   509  
   510  	bondCfg := c.dexBondConfig(dc, time.Now().Unix())
   511  	acctBondState := c.bondStateOfDEX(dc, bondCfg)
   512  
   513  	return &Exchange{
   514  		Host:             dc.acct.host,
   515  		AcctID:           acctID,
   516  		Markets:          dc.marketMap(),
   517  		Assets:           assets,
   518  		BondExpiry:       cfg.BondExpiry,
   519  		BondAssets:       bondAssets,
   520  		ConnectionStatus: dc.status(),
   521  		CandleDurs:       cfg.BinSizes,
   522  		ViewOnly:         dc.acct.isViewOnly(),
   523  		Auth:             acctBondState.ExchangeAuth,
   524  		MaxScore:         cfg.MaxScore,
   525  		PenaltyThreshold: cfg.PenaltyThreshold,
   526  		Disabled:         dc.acct.isDisabled(),
   527  	}
   528  }
   529  
   530  // assetFamily prepares a map of asset IDs for asset that share a parent asset
   531  // with the specified assetID. The assetID and the parent asset's ID both have
   532  // entries, as well as any tokens.
   533  func assetFamily(assetID uint32) map[uint32]bool {
   534  	assetFamily := make(map[uint32]bool, 1)
   535  	var parentAsset *asset.RegisteredAsset
   536  	if parentAsset = asset.Asset(assetID); parentAsset == nil {
   537  		if tkn := asset.TokenInfo(assetID); tkn != nil {
   538  			parentAsset = asset.Asset(tkn.ParentID)
   539  		}
   540  	}
   541  	if parentAsset != nil {
   542  		assetFamily[parentAsset.ID] = true
   543  		for tokenID := range parentAsset.Tokens {
   544  			assetFamily[tokenID] = true
   545  		}
   546  	}
   547  	return assetFamily
   548  }
   549  
   550  // hasActiveAssetOrders checks whether there are any active orders or negotiating
   551  // matches for the specified asset.
   552  func (dc *dexConnection) hasActiveAssetOrders(assetID uint32) bool {
   553  	familial := assetFamily(assetID)
   554  	dc.tradeMtx.RLock()
   555  	defer dc.tradeMtx.RUnlock()
   556  	for _, inFlight := range dc.inFlightOrders {
   557  		if familial[inFlight.BaseID] || familial[inFlight.QuoteID] {
   558  			return true
   559  		}
   560  	}
   561  
   562  	for _, trade := range dc.trades {
   563  		if (familial[trade.Base()] || familial[trade.Quote()]) &&
   564  			trade.isActive() {
   565  			return true
   566  		}
   567  
   568  	}
   569  	return false
   570  }
   571  
   572  // hasActiveOrders checks whether there are any active orders for the dexConnection.
   573  func (dc *dexConnection) hasActiveOrders() bool {
   574  	dc.tradeMtx.RLock()
   575  	defer dc.tradeMtx.RUnlock()
   576  
   577  	if len(dc.inFlightOrders) > 0 {
   578  		return true
   579  	}
   580  
   581  	for _, trade := range dc.trades {
   582  		if trade.isActive() {
   583  			return true
   584  		}
   585  	}
   586  	return false
   587  }
   588  
   589  // activeOrders returns a slice of active orders and inflight orders.
   590  func (dc *dexConnection) activeOrders() ([]*Order, []*InFlightOrder) {
   591  	dc.tradeMtx.RLock()
   592  	defer dc.tradeMtx.RUnlock()
   593  
   594  	var activeOrders []*Order
   595  	for _, trade := range dc.trades {
   596  		if trade.isActive() {
   597  			activeOrders = append(activeOrders, trade.coreOrder())
   598  		}
   599  	}
   600  
   601  	var inflightOrders []*InFlightOrder
   602  	for _, ord := range dc.inFlightOrders {
   603  		inflightOrders = append(inflightOrders, ord)
   604  	}
   605  
   606  	return activeOrders, inflightOrders
   607  }
   608  
   609  // findOrder returns the tracker and preimage for an order ID, and a boolean
   610  // indicating whether this is a cancel order.
   611  func (dc *dexConnection) findOrder(oid order.OrderID) (tracker *trackedTrade, isCancel bool) {
   612  	dc.tradeMtx.RLock()
   613  	defer dc.tradeMtx.RUnlock()
   614  	// Try to find the order as a trade.
   615  	if tracker, found := dc.trades[oid]; found {
   616  		return tracker, false
   617  	}
   618  
   619  	if tid, found := dc.cancelTradeID(oid); found {
   620  		if tracker, found := dc.trades[tid]; found {
   621  			return tracker, true
   622  		} else {
   623  			dc.log.Errorf("Did not find trade for cancel order ID %s", oid)
   624  		}
   625  	}
   626  	return
   627  }
   628  
   629  func (c *Core) sendCancelOrder(dc *dexConnection, oid order.OrderID, base, quote uint32) (order.Preimage, *order.CancelOrder, []byte, chan struct{}, error) {
   630  	preImg := newPreimage()
   631  	co := &order.CancelOrder{
   632  		P: order.Prefix{
   633  			AccountID:  dc.acct.ID(),
   634  			BaseAsset:  base,
   635  			QuoteAsset: quote,
   636  			OrderType:  order.CancelOrderType,
   637  			ClientTime: time.Now(),
   638  			Commit:     preImg.Commit(),
   639  		},
   640  		TargetOrderID: oid,
   641  	}
   642  	err := order.ValidateOrder(co, order.OrderStatusEpoch, 0)
   643  	if err != nil {
   644  		return preImg, nil, nil, nil, err
   645  	}
   646  
   647  	commitSig := make(chan struct{})
   648  	c.sentCommitsMtx.Lock()
   649  	c.sentCommits[co.Commit] = commitSig
   650  	c.sentCommitsMtx.Unlock()
   651  
   652  	// Create and send the order message. Check the response before using it.
   653  	route, msgOrder, _ := messageOrder(co, nil)
   654  	var result = new(msgjson.OrderResult)
   655  	err = dc.signAndRequest(msgOrder, route, result, DefaultResponseTimeout)
   656  	if err != nil {
   657  		// At this point there is a possibility that the server got the request
   658  		// and created the cancel order, but we lost the connection before
   659  		// receiving the response with the cancel's order ID. Any preimage
   660  		// request will be unrecognized. This order is ABANDONED.
   661  		c.sentCommitsMtx.Lock()
   662  		delete(c.sentCommits, co.Commit)
   663  		c.sentCommitsMtx.Unlock()
   664  		return preImg, nil, nil, nil, fmt.Errorf("failed to submit cancel order targeting trade %v: %w", oid, err)
   665  	}
   666  	err = validateOrderResponse(dc, result, co, msgOrder)
   667  	if err != nil {
   668  		c.sentCommitsMtx.Lock()
   669  		delete(c.sentCommits, co.Commit)
   670  		c.sentCommitsMtx.Unlock()
   671  		return preImg, nil, nil, nil, fmt.Errorf("Abandoning order. preimage: %x, server time: %d: %w",
   672  			preImg[:], result.ServerTime, err)
   673  	}
   674  
   675  	return preImg, co, result.Sig, commitSig, nil
   676  }
   677  
   678  // tryCancel will look for an order with the specified order ID, and attempt to
   679  // cancel the order. It is not an error if the order is not found.
   680  func (c *Core) tryCancel(dc *dexConnection, oid order.OrderID) (found bool, err error) {
   681  	tracker, _ := dc.findOrder(oid)
   682  	if tracker == nil {
   683  		return // false, nil
   684  	}
   685  	return true, c.tryCancelTrade(dc, tracker)
   686  }
   687  
   688  // tryCancelTrade attempts to cancel the order.
   689  func (c *Core) tryCancelTrade(dc *dexConnection, tracker *trackedTrade) error {
   690  	oid := tracker.ID()
   691  	if lo, ok := tracker.Order.(*order.LimitOrder); !ok || lo.Force != order.StandingTiF {
   692  		return fmt.Errorf("cannot cancel %s order %s that is not a standing limit order", tracker.Type(), oid)
   693  	}
   694  
   695  	mktConf := dc.marketConfig(tracker.mktID)
   696  	if mktConf == nil {
   697  		return newError(marketErr, "unknown market %q", tracker.mktID)
   698  	}
   699  
   700  	tracker.mtx.Lock()
   701  	defer tracker.mtx.Unlock()
   702  
   703  	if status := tracker.metaData.Status; status != order.OrderStatusEpoch && status != order.OrderStatusBooked {
   704  		return fmt.Errorf("order %v not cancellable in status %v", oid, status)
   705  	}
   706  
   707  	if tracker.cancel != nil {
   708  		// Existing cancel might be stale. Deleting it now allows this
   709  		// cancel attempt to proceed.
   710  		tracker.deleteStaleCancelOrder()
   711  
   712  		if tracker.cancel != nil {
   713  			return fmt.Errorf("order %s - only one cancel order can be submitted per order per epoch. "+
   714  				"still waiting on cancel order %s to match", oid, tracker.cancel.ID())
   715  		}
   716  	}
   717  
   718  	// Construct and send the order.
   719  	preImg, co, sig, commitSig, err := c.sendCancelOrder(dc, oid, tracker.Base(), tracker.Quote())
   720  	if err != nil {
   721  		return err
   722  	}
   723  	defer close(commitSig)
   724  
   725  	// Store the cancel order with the tracker.
   726  	err = tracker.cancelTrade(co, preImg, mktConf.EpochLen)
   727  	if err != nil {
   728  		return fmt.Errorf("error storing cancel order info %s: %w", co.ID(), err)
   729  	}
   730  
   731  	// Store the cancel order.
   732  	err = c.db.UpdateOrder(&db.MetaOrder{
   733  		MetaData: &db.OrderMetaData{
   734  			Status: order.OrderStatusEpoch,
   735  			Host:   dc.acct.host,
   736  			Proof: db.OrderProof{
   737  				DEXSig:   sig,
   738  				Preimage: preImg[:],
   739  			},
   740  			EpochDur:    mktConf.EpochLen, // epochIndex := result.ServerTime / mktConf.EpochLen
   741  			LinkedOrder: oid,
   742  		},
   743  		Order: co,
   744  	})
   745  	if err != nil {
   746  		return fmt.Errorf("failed to store order in database: %w", err)
   747  	}
   748  
   749  	c.log.Infof("Cancel order %s targeting order %s at %s has been placed",
   750  		co.ID(), oid, dc.acct.host)
   751  
   752  	subject, details := c.formatDetails(TopicCancellingOrder, makeOrderToken(tracker.token()))
   753  	c.notify(newOrderNote(TopicCancellingOrder, subject, details, db.Poke, tracker.coreOrderInternal()))
   754  
   755  	return nil
   756  }
   757  
   758  // signAndRequest signs and sends the request, unmarshaling the response into
   759  // the provided interface.
   760  func (dc *dexConnection) signAndRequest(signable msgjson.Signable, route string, result any, timeout time.Duration) error {
   761  	if dc.acct.locked() {
   762  		return fmt.Errorf("cannot sign: %s account locked", dc.acct.host)
   763  	}
   764  	sign(dc.acct.privKey, signable)
   765  	return sendRequest(dc.WsConn, route, signable, result, timeout)
   766  }
   767  
   768  // ack sends an Acknowledgement for a match-related request.
   769  func (dc *dexConnection) ack(msgID uint64, matchID order.MatchID, signable msgjson.Signable) (err error) {
   770  	ack := &msgjson.Acknowledgement{
   771  		MatchID: matchID[:],
   772  	}
   773  	sigMsg := signable.Serialize()
   774  	ack.Sig, err = dc.acct.sign(sigMsg)
   775  	if err != nil {
   776  		return fmt.Errorf("sign error - %w", err)
   777  	}
   778  	msg, err := msgjson.NewResponse(msgID, ack, nil)
   779  	if err != nil {
   780  		return fmt.Errorf("NewResponse error - %w", err)
   781  	}
   782  	err = dc.Send(msg)
   783  	if err != nil {
   784  		return fmt.Errorf("Send error - %w", err)
   785  	}
   786  	return nil
   787  }
   788  
   789  // serverMatches are an intermediate structure used by the dexConnection to
   790  // sort incoming match notifications.
   791  type serverMatches struct {
   792  	tracker    *trackedTrade
   793  	msgMatches []*msgjson.Match
   794  	cancel     *msgjson.Match
   795  }
   796  
   797  // parseMatches sorts the list of matches and associates them with a trade. This
   798  // may be called from handleMatchRoute on receipt of a new 'match' request, or
   799  // by authDEX with the list of active matches returned by the 'connect' request.
   800  func (dc *dexConnection) parseMatches(msgMatches []*msgjson.Match, checkSigs bool) (map[order.OrderID]*serverMatches, []msgjson.Acknowledgement, error) {
   801  	var acks []msgjson.Acknowledgement
   802  	matches := make(map[order.OrderID]*serverMatches)
   803  	var errs []string
   804  	for _, msgMatch := range msgMatches {
   805  		var oid order.OrderID
   806  		copy(oid[:], msgMatch.OrderID)
   807  		tracker, isCancel := dc.findOrder(oid)
   808  		if tracker == nil {
   809  			dc.blindCancelsMtx.Lock()
   810  			_, found := dc.blindCancels[oid]
   811  			delete(dc.blindCancels, oid)
   812  			dc.blindCancelsMtx.Unlock()
   813  			if found { // We're done. The targeted order isn't tracked, and we don't need to ack.
   814  				dc.log.Infof("Blind cancel order %v matched.", oid)
   815  				continue
   816  			}
   817  			errs = append(errs, "order "+oid.String()+" not found")
   818  			continue
   819  		}
   820  
   821  		// Check the fee rate against the maxfeerate recorded at order time.
   822  		swapRate := msgMatch.FeeRateQuote
   823  		if tracker.Trade().Sell {
   824  			swapRate = msgMatch.FeeRateBase
   825  		}
   826  		if !isCancel && swapRate > tracker.metaData.MaxFeeRate {
   827  			errs = append(errs, fmt.Sprintf("rejecting match %s for order %s because assigned rate (%d) is > MaxFeeRate (%d)",
   828  				msgMatch.MatchID, msgMatch.OrderID, swapRate, tracker.metaData.MaxFeeRate))
   829  			continue
   830  		}
   831  
   832  		sigMsg := msgMatch.Serialize()
   833  		if checkSigs {
   834  			err := dc.acct.checkSig(sigMsg, msgMatch.Sig)
   835  			if err != nil {
   836  				// If the caller (e.g. handleMatchRoute) requests signature
   837  				// verification, this is fatal.
   838  				return nil, nil, fmt.Errorf("parseMatches: match signature verification failed: %w", err)
   839  			}
   840  		}
   841  		sig, err := dc.acct.sign(sigMsg)
   842  		if err != nil {
   843  			errs = append(errs, err.Error())
   844  			continue
   845  		}
   846  
   847  		// Success. Add the serverMatch and the Acknowledgement.
   848  		acks = append(acks, msgjson.Acknowledgement{
   849  			MatchID: msgMatch.MatchID,
   850  			Sig:     sig,
   851  		})
   852  
   853  		trackerID := tracker.ID()
   854  		match := matches[trackerID]
   855  		if match == nil {
   856  			match = &serverMatches{
   857  				tracker: tracker,
   858  			}
   859  			matches[trackerID] = match
   860  		}
   861  		if isCancel {
   862  			match.cancel = msgMatch // taker match
   863  		} else {
   864  			match.msgMatches = append(match.msgMatches, msgMatch)
   865  		}
   866  
   867  		status := order.MatchStatus(msgMatch.Status)
   868  		dc.log.Debugf("Registering match %v for order %v (%v) in status %v",
   869  			msgMatch.MatchID, oid, order.MatchSide(msgMatch.Side), status)
   870  	}
   871  
   872  	var err error
   873  	if len(errs) > 0 {
   874  		err = fmt.Errorf("parseMatches errors: %s", strings.Join(errs, ", "))
   875  	}
   876  	// A non-nil error only means that at least one match failed to parse, so we
   877  	// must return the successful matches and acks for further processing.
   878  	return matches, acks, err
   879  }
   880  
   881  // matchDiscreps specifies a trackedTrades's missing and extra matches compared
   882  // to the server's list of active matches as returned in the connect response.
   883  type matchDiscreps struct {
   884  	trade   *trackedTrade
   885  	missing []*matchTracker
   886  	extra   []*msgjson.Match
   887  }
   888  
   889  // matchStatusConflict is a conflict between our status, and the status returned
   890  // by the server in the connect response.
   891  type matchStatusConflict struct {
   892  	trade   *trackedTrade
   893  	matches []*matchTracker
   894  }
   895  
   896  // compareServerMatches resolves the matches reported by the server in the
   897  // 'connect' response against those marked incomplete in the matchTracker map
   898  // for each serverMatch.
   899  // Reported matches with missing trackers are already checked by parseMatches,
   900  // but we also must check for incomplete matches that the server is not
   901  // reporting.
   902  func (dc *dexConnection) compareServerMatches(srvMatches map[order.OrderID]*serverMatches) (
   903  	exceptions map[order.OrderID]*matchDiscreps, statusConflicts map[order.OrderID]*matchStatusConflict) {
   904  
   905  	exceptions = make(map[order.OrderID]*matchDiscreps)
   906  	statusConflicts = make(map[order.OrderID]*matchStatusConflict)
   907  
   908  	// Identify extra matches named by the server response that we do not
   909  	// recognize.
   910  	for oid, match := range srvMatches {
   911  		var extra []*msgjson.Match
   912  		match.tracker.mtx.RLock()
   913  		for _, msgMatch := range match.msgMatches {
   914  			var matchID order.MatchID
   915  			copy(matchID[:], msgMatch.MatchID)
   916  			mt := match.tracker.matches[matchID]
   917  			if mt == nil {
   918  				extra = append(extra, msgMatch)
   919  				continue
   920  			}
   921  			mt.exceptionMtx.Lock()
   922  			mt.checkServerRevoke = false
   923  			mt.exceptionMtx.Unlock()
   924  			if mt.Status != order.MatchStatus(msgMatch.Status) {
   925  				conflict := statusConflicts[oid]
   926  				if conflict == nil {
   927  					conflict = &matchStatusConflict{trade: match.tracker}
   928  					statusConflicts[oid] = conflict
   929  				}
   930  				conflict.matches = append(conflict.matches, mt)
   931  			}
   932  		}
   933  		match.tracker.mtx.RUnlock()
   934  		if len(extra) > 0 {
   935  			exceptions[match.tracker.ID()] = &matchDiscreps{
   936  				trade: match.tracker,
   937  				extra: extra,
   938  			}
   939  		}
   940  	}
   941  
   942  	in := func(matches []*msgjson.Match, mid []byte) bool {
   943  		for _, m := range matches {
   944  			if bytes.Equal(m.MatchID, mid) {
   945  				return true
   946  			}
   947  		}
   948  		return false
   949  	}
   950  
   951  	setMissing := func(trade *trackedTrade, missing []*matchTracker) {
   952  		if tt, found := exceptions[trade.ID()]; found {
   953  			tt.missing = missing
   954  		} else {
   955  			exceptions[trade.ID()] = &matchDiscreps{
   956  				trade:   trade,
   957  				missing: missing,
   958  			}
   959  		}
   960  	}
   961  
   962  	// Identify active matches that are missing from server's response.
   963  	dc.tradeMtx.RLock()
   964  	defer dc.tradeMtx.RUnlock()
   965  	for oid, trade := range dc.trades {
   966  		var activeMatches []*matchTracker
   967  		for _, m := range trade.activeMatches() {
   968  			// Server is not expected to report matches that have been fully
   969  			// redeemed or are revoked. Only client cares about redeem confs.
   970  			if m.Status >= order.MatchComplete || m.MetaData.Proof.IsRevoked() {
   971  				continue
   972  			}
   973  			activeMatches = append(activeMatches, m)
   974  		}
   975  		if len(activeMatches) == 0 {
   976  			continue
   977  		}
   978  		tradeMatches, found := srvMatches[oid]
   979  		if !found {
   980  			// ALL of this trade's active matches are missing.
   981  			setMissing(trade, activeMatches)
   982  			continue // check next local trade
   983  		}
   984  		// Check this local trade's active matches against server's reported
   985  		// matches for this trade.
   986  		var missing []*matchTracker
   987  		for _, match := range activeMatches { // each local match
   988  			if !in(tradeMatches.msgMatches, match.MatchID[:]) { // against reported matches
   989  				missing = append(missing, match)
   990  			}
   991  		}
   992  		if len(missing) > 0 {
   993  			setMissing(trade, missing)
   994  		}
   995  	}
   996  
   997  	return
   998  }
   999  
  1000  // updateOrderStatus updates the order's status, cleaning up any associated
  1001  // cancel orders, unlocking funding coins and refund/redemption reserves, and
  1002  // updating the order in the DB. The trackedTrade's mutex must be write locked.
  1003  func (dc *dexConnection) updateOrderStatus(trade *trackedTrade, newStatus order.OrderStatus) {
  1004  	oid := trade.ID()
  1005  	previousStatus := trade.metaData.Status
  1006  	if previousStatus == newStatus { // may be expected if no srvOrderStatuses provided
  1007  		return
  1008  	}
  1009  	trade.metaData.Status = newStatus
  1010  	// If there is an associated cancel order, and we are revising the
  1011  	// status of the targeted order to anything other than canceled, we can
  1012  	// infer the cancel order is done. Update the status of the cancel order
  1013  	// and unlink it from the trade. If the targeted order is reported as
  1014  	// canceled, that indicates we submitted a cancel order preimage but
  1015  	// missed the match notification, so the cancel order is executed.
  1016  	if newStatus != order.OrderStatusCanceled {
  1017  		trade.deleteCancelOrder()
  1018  	} else if trade.cancel != nil {
  1019  		cid := trade.cancel.ID()
  1020  		err := trade.db.UpdateOrderStatus(cid, order.OrderStatusExecuted)
  1021  		if err != nil {
  1022  			dc.log.Errorf("Failed to update status of executed cancel order %v: %v", cid, err)
  1023  		}
  1024  	}
  1025  	// If we're updating an order from an active state to executed,
  1026  	// canceled, or revoked, and there are no active matches, return the
  1027  	// locked funding coins and any refund/redeem reserves.
  1028  	trade.maybeReturnCoins()
  1029  	if newStatus >= order.OrderStatusExecuted && trade.Trade().Remaining() > 0 &&
  1030  		(!trade.isMarketBuy() || len(trade.matches) == 0) {
  1031  		if trade.isMarketBuy() {
  1032  			trade.unlockRedemptionFraction(1, 1)
  1033  			trade.unlockRefundFraction(1, 1)
  1034  		} else {
  1035  			trade.unlockRedemptionFraction(trade.Trade().Remaining(), trade.Trade().Quantity)
  1036  			trade.unlockRefundFraction(trade.Trade().Remaining(), trade.Trade().Quantity)
  1037  		}
  1038  	}
  1039  	// Now update the trade.
  1040  	if err := trade.db.UpdateOrder(trade.metaOrder()); err != nil {
  1041  		dc.log.Errorf("Error updating status in db for order %v from %v to %v", oid, previousStatus, newStatus)
  1042  	} else {
  1043  		dc.log.Warnf("Order %v updated from recorded status %q to new status %q reported by DEX %s",
  1044  			oid, previousStatus, newStatus, dc.acct.host)
  1045  	}
  1046  
  1047  	subject, details := trade.formatDetails(TopicOrderStatusUpdate, makeOrderToken(trade.token()), previousStatus, newStatus)
  1048  	dc.notify(newOrderNote(TopicOrderStatusUpdate, subject, details, db.WarningLevel, trade.coreOrderInternal()))
  1049  }
  1050  
  1051  // syncOrderStatuses requests and updates the status for each of the trades.
  1052  func (dc *dexConnection) syncOrderStatuses(orders []*trackedTrade) (reconciledOrdersCount int) {
  1053  	orderStatusRequests := make([]*msgjson.OrderStatusRequest, len(orders))
  1054  	tradeMap := make(map[order.OrderID]*trackedTrade, len(orders))
  1055  	for i, trade := range orders {
  1056  		oid := trade.ID()
  1057  		tradeMap[oid] = trade
  1058  		orderStatusRequests[i] = &msgjson.OrderStatusRequest{
  1059  			Base:    trade.Base(),
  1060  			Quote:   trade.Quote(),
  1061  			OrderID: oid.Bytes(),
  1062  		}
  1063  	}
  1064  
  1065  	dc.log.Debugf("Requesting statuses for %d orders from DEX %s", len(orderStatusRequests), dc.acct.host)
  1066  
  1067  	// Send the 'order_status' request.
  1068  	var orderStatusResults []*msgjson.OrderStatus
  1069  	err := sendRequest(dc.WsConn, msgjson.OrderStatusRoute, orderStatusRequests,
  1070  		&orderStatusResults, DefaultResponseTimeout)
  1071  	if err != nil {
  1072  		dc.log.Errorf("Error retrieving order statuses from DEX %s: %v", dc.acct.host, err)
  1073  		return
  1074  	}
  1075  
  1076  	if len(orderStatusResults) != len(orderStatusRequests) {
  1077  		dc.log.Errorf("Retrieved statuses for %d out of %d orders from order_status route",
  1078  			len(orderStatusResults), len(orderStatusRequests))
  1079  	}
  1080  
  1081  	// Update the orders with the statuses received.
  1082  	for _, srvOrderStatus := range orderStatusResults {
  1083  		var oid order.OrderID
  1084  		copy(oid[:], srvOrderStatus.ID)
  1085  		trade := tradeMap[oid] // no need to lock dc.tradeMtx
  1086  		if trade == nil {
  1087  			dc.log.Warnf("Server reported status for order %v that we did not request.", oid)
  1088  			continue
  1089  		}
  1090  		reconciledOrdersCount++
  1091  		trade.mtx.Lock()
  1092  		dc.updateOrderStatus(trade, order.OrderStatus(srvOrderStatus.Status))
  1093  		trade.mtx.Unlock()
  1094  	}
  1095  
  1096  	// Treat orders with no status reported as revoked.
  1097  reqsLoop:
  1098  	for _, req := range orderStatusRequests {
  1099  		for _, res := range orderStatusResults {
  1100  			if req.OrderID.Equal(res.ID) {
  1101  				continue reqsLoop
  1102  			}
  1103  		}
  1104  		// No result for this order.
  1105  		dc.log.Warnf("Server did not report status for order %v", req.OrderID)
  1106  		var oid order.OrderID
  1107  		copy(oid[:], req.OrderID)
  1108  		trade := tradeMap[oid]
  1109  		reconciledOrdersCount++
  1110  		trade.mtx.Lock()
  1111  		dc.updateOrderStatus(trade, order.OrderStatusRevoked)
  1112  		trade.mtx.Unlock()
  1113  	}
  1114  
  1115  	return
  1116  }
  1117  
  1118  // reconcileTrades compares the statuses of orders in the dc.trades map to the
  1119  // statuses returned by the server on `connect`, updating the statuses of the
  1120  // tracked trades where applicable e.g.
  1121  //   - Booked orders that were tracked as Epoch are updated to status Booked.
  1122  //   - Orders thought to be active in the dc.trades map but not returned by the
  1123  //     server are updated to Executed, Canceled or Revoked.
  1124  //
  1125  // Setting the order status appropriately now, especially for inactive orders,
  1126  // ensures that...
  1127  //   - the affected trades can be retired once the trade ticker (in core.listen)
  1128  //     observes that there are no active matches for the trades.
  1129  //   - coins are unlocked either as the affected trades' matches are swapped or
  1130  //     revoked (for trades with active matches), or when the trades are retired.
  1131  //
  1132  // Also purges "stale" cancel orders if the targeted order is returned in the
  1133  // server's `connect` response. See *trackedTrade.deleteStaleCancelOrder for
  1134  // the definition of a stale cancel order.
  1135  func (dc *dexConnection) reconcileTrades(srvOrderStatuses []*msgjson.OrderStatus) (unknownOrders []order.OrderID, reconciledOrdersCount int) {
  1136  	dc.tradeMtx.RLock()
  1137  	// Check for unknown orders reported as active by the server. If such
  1138  	// exists, could be that they were known to the client but were thought
  1139  	// to be inactive and thus were not loaded from db or were retired.
  1140  	srvActiveOrderStatuses := make(map[order.OrderID]*msgjson.OrderStatus, len(srvOrderStatuses))
  1141  	for _, srvOrderStatus := range srvOrderStatuses {
  1142  		var oid order.OrderID
  1143  		copy(oid[:], srvOrderStatus.ID)
  1144  		if _, tracked := dc.trades[oid]; tracked {
  1145  			srvActiveOrderStatuses[oid] = srvOrderStatus
  1146  		} else {
  1147  			dc.log.Warnf("Unknown order %v reported by DEX %s as active", oid, dc.acct.host)
  1148  			unknownOrders = append(unknownOrders, oid)
  1149  		}
  1150  	}
  1151  	knownActiveTrades := make(map[order.OrderID]*trackedTrade)
  1152  	for oid, trade := range dc.trades {
  1153  		status := trade.status()
  1154  		if status == order.OrderStatusEpoch || status == order.OrderStatusBooked {
  1155  			knownActiveTrades[oid] = trade
  1156  		} else if srvOrderStatus := srvActiveOrderStatuses[oid]; srvOrderStatus != nil {
  1157  			// Lock redemption funds?
  1158  			dc.log.Warnf("Inactive order %v, status %q reported by DEX %s as active, status %q",
  1159  				oid, status, dc.acct.host, order.OrderStatus(srvOrderStatus.Status))
  1160  		}
  1161  	}
  1162  	dc.tradeMtx.RUnlock()
  1163  
  1164  	// Compare the status reported by the server for each known active trade.
  1165  	// Orders for which the server did not return a status are no longer active
  1166  	// (now Executed, Canceled or Revoked). Use the order_status route to
  1167  	// determine the correct status for such orders and update accordingly.
  1168  	var mysteryOrders []*trackedTrade
  1169  	for oid, trade := range knownActiveTrades {
  1170  		srvOrderStatus := srvActiveOrderStatuses[oid]
  1171  		if srvOrderStatus == nil {
  1172  			// Order status not returned by server. Must be inactive now.
  1173  			// Request current status from the DEX.
  1174  			mysteryOrders = append(mysteryOrders, trade)
  1175  			continue
  1176  		}
  1177  
  1178  		trade.mtx.Lock()
  1179  
  1180  		// Server reports this order as active. Delete any associated cancel
  1181  		// order if the cancel order's epoch has passed.
  1182  		trade.deleteStaleCancelOrder() // could be too soon, so we'll have to check in tick too
  1183  
  1184  		ourStatus := trade.metaData.Status
  1185  		serverStatus := order.OrderStatus(srvOrderStatus.Status)
  1186  		if ourStatus == serverStatus {
  1187  			dc.log.Tracef("Status reconciliation not required for order %v, status %q, server-reported status %q",
  1188  				oid, ourStatus, serverStatus)
  1189  		} else if ourStatus == order.OrderStatusEpoch && serverStatus == order.OrderStatusBooked {
  1190  			// Only standing orders can move from Epoch to Booked. This must have
  1191  			// happened in the client's absence (maybe a missed nomatch message).
  1192  			if lo, ok := trade.Order.(*order.LimitOrder); ok && lo.Force == order.StandingTiF {
  1193  				reconciledOrdersCount++
  1194  				dc.updateOrderStatus(trade, serverStatus)
  1195  			} else {
  1196  				dc.log.Warnf("Incorrect status %q reported for non-standing order %v by DEX %s, client status = %q",
  1197  					serverStatus, oid, dc.acct.host, ourStatus)
  1198  			}
  1199  		} else {
  1200  			dc.log.Warnf("Inconsistent status %q reported for order %v by DEX %s, client status = %q",
  1201  				serverStatus, oid, dc.acct.host, ourStatus)
  1202  		}
  1203  
  1204  		trade.mtx.Unlock()
  1205  	}
  1206  
  1207  	if len(mysteryOrders) > 0 {
  1208  		reconciledOrdersCount += dc.syncOrderStatuses(mysteryOrders)
  1209  	}
  1210  
  1211  	return
  1212  }
  1213  
  1214  // tickAsset checks open matches related to a specific asset for needed action.
  1215  func (c *Core) tickAsset(dc *dexConnection, assetID uint32) assetMap {
  1216  	dc.tradeMtx.RLock()
  1217  	assetTrades := make([]*trackedTrade, 0, len(dc.trades))
  1218  	for _, trade := range dc.trades {
  1219  		if trade.Base() == assetID || trade.Quote() == assetID {
  1220  			assetTrades = append(assetTrades, trade)
  1221  		}
  1222  	}
  1223  	dc.tradeMtx.RUnlock()
  1224  
  1225  	updated := make(assetMap)
  1226  	updateChan := make(chan assetMap)
  1227  	for _, trade := range assetTrades {
  1228  		if c.ctx.Err() != nil { // don't fail each one in sequence if shutting down
  1229  			return updated
  1230  		}
  1231  		trade := trade // bad go, bad
  1232  		go func() {
  1233  			newUpdates, err := c.tick(trade)
  1234  			if err != nil {
  1235  				c.log.Errorf("%s tick error: %v", dc.acct.host, err)
  1236  			}
  1237  			updateChan <- newUpdates
  1238  		}()
  1239  	}
  1240  
  1241  	for range assetTrades {
  1242  		updated.merge(<-updateChan)
  1243  	}
  1244  	return updated
  1245  }
  1246  
  1247  // Get the *dexConnection and connection status for the the host.
  1248  func (c *Core) dex(addr string) (*dexConnection, bool, error) {
  1249  	host, err := addrHost(addr)
  1250  	if err != nil {
  1251  		return nil, false, newError(addressParseErr, "error parsing address: %w", err)
  1252  	}
  1253  
  1254  	// Get the dexConnection and the dex.Asset for each asset.
  1255  	c.connMtx.RLock()
  1256  	dc, found := c.conns[host]
  1257  	c.connMtx.RUnlock()
  1258  	if !found {
  1259  		return nil, false, fmt.Errorf("unknown DEX %s", addr)
  1260  	}
  1261  	return dc, dc.status() == comms.Connected, nil
  1262  }
  1263  
  1264  // addDexConnection is a helper used to add a dex connection.
  1265  func (c *Core) addDexConnection(dc *dexConnection) {
  1266  	if dc == nil {
  1267  		return
  1268  	}
  1269  	c.connMtx.Lock()
  1270  	c.conns[dc.acct.host] = dc
  1271  	c.connMtx.Unlock()
  1272  }
  1273  
  1274  // Get the *dexConnection for the host. Return an error if the DEX is not
  1275  // registered, connected, and unlocked.
  1276  func (c *Core) registeredDEX(addr string) (*dexConnection, error) {
  1277  	dc, connected, err := c.dex(addr)
  1278  	if err != nil {
  1279  		return nil, err
  1280  	}
  1281  
  1282  	if dc.acct.isViewOnly() {
  1283  		return nil, fmt.Errorf("not yet registered at %s", dc.acct.host)
  1284  	}
  1285  
  1286  	if dc.acct.locked() {
  1287  		return nil, fmt.Errorf("account for %s is locked. Are you logged in?", dc.acct.host)
  1288  	}
  1289  
  1290  	if !connected {
  1291  		return nil, fmt.Errorf("currently disconnected from %s", dc.acct.host)
  1292  	}
  1293  	return dc, nil
  1294  }
  1295  
  1296  // setEpoch sets the epoch. If the passed epoch is greater than the highest
  1297  // previously passed epoch, an epoch notification is sent to all subscribers and
  1298  // true is returned.
  1299  func (dc *dexConnection) setEpoch(mktID string, epochIdx uint64) bool {
  1300  	dc.epochMtx.Lock()
  1301  	defer dc.epochMtx.Unlock()
  1302  	if epochIdx > dc.epoch[mktID] {
  1303  		dc.epoch[mktID] = epochIdx
  1304  		dc.notify(newEpochNotification(dc.acct.host, mktID, epochIdx))
  1305  		return true
  1306  	}
  1307  	return false
  1308  }
  1309  
  1310  // marketEpochDuration gets the market's epoch duration. If the market is not
  1311  // known, an error is logged and 0 is returned.
  1312  func (dc *dexConnection) marketEpochDuration(mktID string) uint64 {
  1313  	mkt := dc.marketConfig(mktID)
  1314  	if mkt == nil {
  1315  		return 0
  1316  	}
  1317  	return mkt.EpochLen
  1318  }
  1319  
  1320  // marketEpoch gets the epoch index for the specified market and time stamp. If
  1321  // the market is not known, 0 is returned.
  1322  func (dc *dexConnection) marketEpoch(mktID string, stamp time.Time) uint64 {
  1323  	epochLen := dc.marketEpochDuration(mktID)
  1324  	if epochLen == 0 {
  1325  		return 0
  1326  	}
  1327  	return uint64(stamp.UnixMilli()) / epochLen
  1328  }
  1329  
  1330  // fetchFeeRate gets an asset's fee rate estimate from the server.
  1331  func (dc *dexConnection) fetchFeeRate(assetID uint32) (rate uint64) {
  1332  	msg, err := msgjson.NewRequest(dc.NextID(), msgjson.FeeRateRoute, assetID)
  1333  	if err != nil {
  1334  		dc.log.Errorf("Error fetching fee rate for %s: %v", unbip(assetID), err)
  1335  		return
  1336  	}
  1337  	errChan := make(chan error, 1)
  1338  	err = dc.RequestWithTimeout(msg, func(msg *msgjson.Message) {
  1339  		errChan <- msg.UnmarshalResult(&rate)
  1340  	}, DefaultResponseTimeout, func() {
  1341  		errChan <- fmt.Errorf("timed out waiting for fee_rate response")
  1342  	})
  1343  	if err == nil {
  1344  		err = <-errChan
  1345  	}
  1346  	if err != nil {
  1347  		dc.log.Errorf("Error fetching fee rate for %s: %v", unbip(assetID), err)
  1348  		return
  1349  	}
  1350  	return
  1351  }
  1352  
  1353  // bestBookFeeSuggestion attempts to find a fee rate for the specified asset in
  1354  // any synced book.
  1355  func (dc *dexConnection) bestBookFeeSuggestion(assetID uint32) uint64 {
  1356  	dc.booksMtx.RLock()
  1357  	defer dc.booksMtx.RUnlock()
  1358  	for _, book := range dc.books {
  1359  		var feeRate uint64
  1360  		switch assetID {
  1361  		case book.base:
  1362  			feeRate = book.BaseFeeRate()
  1363  		case book.quote:
  1364  			feeRate = book.QuoteFeeRate()
  1365  		}
  1366  		if feeRate > 0 {
  1367  			return feeRate
  1368  		}
  1369  	}
  1370  	return 0
  1371  }
  1372  
  1373  type pokesCache struct {
  1374  	sync.RWMutex
  1375  	cache         []*db.Notification
  1376  	cursor        int
  1377  	pokesCapacity int
  1378  }
  1379  
  1380  func newPokesCache(pokesCapacity int) *pokesCache {
  1381  	return &pokesCache{
  1382  		pokesCapacity: pokesCapacity,
  1383  	}
  1384  }
  1385  
  1386  func (c *pokesCache) add(poke *db.Notification) {
  1387  	c.Lock()
  1388  	defer c.Unlock()
  1389  
  1390  	if len(c.cache) >= c.pokesCapacity {
  1391  		c.cache[c.cursor] = poke
  1392  	} else {
  1393  		c.cache = append(c.cache, poke)
  1394  	}
  1395  	c.cursor = (c.cursor + 1) % c.pokesCapacity
  1396  }
  1397  
  1398  func (c *pokesCache) pokes() []*db.Notification {
  1399  	c.RLock()
  1400  	defer c.RUnlock()
  1401  
  1402  	pokes := make([]*db.Notification, len(c.cache))
  1403  	copy(pokes, c.cache)
  1404  	sort.Slice(pokes, func(i, j int) bool {
  1405  		return pokes[i].TimeStamp < pokes[j].TimeStamp
  1406  	})
  1407  	return pokes
  1408  }
  1409  
  1410  func (c *pokesCache) init(pokes []*db.Notification) {
  1411  	c.Lock()
  1412  	defer c.Unlock()
  1413  
  1414  	if len(pokes) > c.pokesCapacity {
  1415  		pokes = pokes[:len(pokes)-c.pokesCapacity]
  1416  	}
  1417  	c.cache = pokes
  1418  	c.cursor = len(pokes) % c.pokesCapacity
  1419  }
  1420  
  1421  // blockWaiter is a message waiting to be stamped, signed, and sent once a
  1422  // specified coin has the requisite confirmations. The blockWaiter is similar to
  1423  // dcrdex/server/blockWaiter.Waiter, but is different enough to warrant a
  1424  // separate type.
  1425  type blockWaiter struct {
  1426  	assetID uint32
  1427  	trigger func() (bool, error)
  1428  	action  func(error)
  1429  }
  1430  
  1431  // Config is the configuration for the Core.
  1432  type Config struct {
  1433  	// DBPath is a filepath to use for the client database. If the database does
  1434  	// not already exist, it will be created.
  1435  	DBPath string
  1436  	// Net is the current network.
  1437  	Net dex.Network
  1438  	// Logger is the Core's logger and is also used to create the sub-loggers
  1439  	// for the asset backends.
  1440  	Logger dex.Logger
  1441  	// Onion is the address (host:port) of a Tor proxy for use with DEX hosts
  1442  	// with a .onion address. To use Tor with regular DEX addresses as well, set
  1443  	// TorProxy.
  1444  	Onion string
  1445  	// TorProxy specifies the address of a Tor proxy server.
  1446  	TorProxy string
  1447  	// TorIsolation specifies whether to enable Tor circuit isolation.
  1448  	TorIsolation bool
  1449  	// Language. A BCP 47 language tag. Default is en-US.
  1450  	Language string
  1451  
  1452  	// NoAutoWalletLock instructs Core to skip locking the wallet on shutdown or
  1453  	// logout. This can be helpful if the user wants the wallet to remain
  1454  	// unlocked. e.g. They started with the wallet unlocked, or they intend to
  1455  	// start Core again and wish to avoid the time to unlock a locked wallet on
  1456  	// startup.
  1457  	NoAutoWalletLock bool // zero value is legacy behavior
  1458  	// NoAutoDBBackup instructs the DB to skip the creation of a backup DB file
  1459  	// on shutdown. This is useful if the consumer is using the BackupDB method,
  1460  	// or simply creating manual backups of the DB file after shutdown.
  1461  	NoAutoDBBackup bool // zero value is legacy behavior
  1462  	// UnlockCoinsOnLogin indicates that on wallet connect during login, or on
  1463  	// creation of a new wallet, all coins with the wallet should be unlocked.
  1464  	UnlockCoinsOnLogin bool
  1465  	// ExtensionModeFile is the path to a file that specifies configuration
  1466  	// for running core in extension mode, which gives the caller options for
  1467  	// e.g. limiting the ability to configure wallets.
  1468  	ExtensionModeFile string
  1469  	// TheOneHost will run core with only the specified server.
  1470  	TheOneHost string
  1471  	// PruneArchive will prune the order archive to the specified number of
  1472  	// orders.
  1473  	PruneArchive uint64
  1474  }
  1475  
  1476  // locale is data associated with the currently selected language.
  1477  type locale struct {
  1478  	lang    language.Tag
  1479  	m       map[Topic]*translation
  1480  	printer *message.Printer
  1481  }
  1482  
  1483  // Core is the core client application. Core manages DEX connections, wallets,
  1484  // database access, match negotiation and more.
  1485  type Core struct {
  1486  	ctx           context.Context
  1487  	wg            sync.WaitGroup
  1488  	ready         chan struct{}
  1489  	rotate        chan struct{}
  1490  	cfg           *Config
  1491  	log           dex.Logger
  1492  	db            db.DB
  1493  	net           dex.Network
  1494  	lockTimeTaker time.Duration
  1495  	lockTimeMaker time.Duration
  1496  	intl          atomic.Value // *locale
  1497  
  1498  	extensionModeConfig *ExtensionModeConfig
  1499  
  1500  	// construction or init sets credentials
  1501  	credMtx     sync.RWMutex
  1502  	credentials *db.PrimaryCredentials
  1503  
  1504  	loginMtx  sync.Mutex
  1505  	loggedIn  bool
  1506  	bondXPriv *hdkeychain.ExtendedKey // derived from creds.EncSeed on login
  1507  
  1508  	seedGenerationTime uint64
  1509  
  1510  	wsConstructor func(*comms.WsCfg) (comms.WsConn, error)
  1511  	newCrypter    func([]byte) encrypt.Crypter
  1512  	reCrypter     func([]byte, []byte) (encrypt.Crypter, error)
  1513  	latencyQ      *wait.TickerQueue
  1514  
  1515  	connMtx sync.RWMutex
  1516  	conns   map[string]*dexConnection
  1517  
  1518  	walletMtx sync.RWMutex
  1519  	wallets   map[uint32]*xcWallet
  1520  
  1521  	waiterMtx    sync.RWMutex
  1522  	blockWaiters map[string]*blockWaiter
  1523  
  1524  	tickSchedMtx sync.Mutex
  1525  	tickSched    map[order.OrderID]*time.Timer
  1526  
  1527  	noteMtx   sync.RWMutex
  1528  	noteChans map[uint64]chan Notification
  1529  
  1530  	sentCommitsMtx sync.Mutex
  1531  	sentCommits    map[order.Commitment]chan struct{}
  1532  
  1533  	ratesMtx        sync.RWMutex
  1534  	fiatRateSources map[string]*commonRateSource
  1535  
  1536  	reFiat chan struct{}
  1537  
  1538  	pendingWalletsMtx sync.RWMutex
  1539  	pendingWallets    map[uint32]bool
  1540  
  1541  	notes chan asset.WalletNotification
  1542  
  1543  	pokesCache *pokesCache
  1544  
  1545  	requestedActionMtx sync.RWMutex
  1546  	requestedActions   map[string]*asset.ActionRequiredNote
  1547  }
  1548  
  1549  // New is the constructor for a new Core.
  1550  func New(cfg *Config) (*Core, error) {
  1551  	if cfg.Logger == nil {
  1552  		return nil, fmt.Errorf("Core.Config must specify a Logger")
  1553  	}
  1554  	dbOpts := bolt.Opts{
  1555  		BackupOnShutdown: !cfg.NoAutoDBBackup,
  1556  		PruneArchive:     cfg.PruneArchive,
  1557  	}
  1558  	boltDB, err := bolt.NewDB(cfg.DBPath, cfg.Logger.SubLogger("DB"), dbOpts)
  1559  	if err != nil {
  1560  		return nil, fmt.Errorf("database initialization error: %w", err)
  1561  	}
  1562  	if cfg.TorProxy != "" {
  1563  		if _, _, err = net.SplitHostPort(cfg.TorProxy); err != nil {
  1564  			return nil, err
  1565  		}
  1566  	}
  1567  	if cfg.Onion != "" {
  1568  		if _, _, err = net.SplitHostPort(cfg.Onion); err != nil {
  1569  			return nil, err
  1570  		}
  1571  	} else { // default to torproxy if onion not set explicitly
  1572  		cfg.Onion = cfg.TorProxy
  1573  	}
  1574  
  1575  	parseLanguage := func(langStr string) (language.Tag, error) {
  1576  		acceptLang, err := language.Parse(langStr)
  1577  		if err != nil {
  1578  			return language.Und, fmt.Errorf("unable to parse requested language: %w", err)
  1579  		}
  1580  		var langs []language.Tag
  1581  		for locale := range locales {
  1582  			tag, err := language.Parse(locale)
  1583  			if err != nil {
  1584  				return language.Und, fmt.Errorf("bad %v: %w", locale, err)
  1585  			}
  1586  			langs = append(langs, tag)
  1587  		}
  1588  		matcher := language.NewMatcher(langs)
  1589  		_, idx, conf := matcher.Match(acceptLang) // use index because tag may end up as something hyper specific like zh-Hans-u-rg-cnzzzz
  1590  		tag := langs[idx]
  1591  		switch conf {
  1592  		case language.Exact:
  1593  		case language.High, language.Low:
  1594  			cfg.Logger.Infof("Using language %v", tag)
  1595  		case language.No:
  1596  			// Fallback to English instead of returning error
  1597  			cfg.Logger.Warnf("Language %q not supported, falling back to %s", langStr, originLang)
  1598  			return language.AmericanEnglish, nil
  1599  		}
  1600  		return tag, nil
  1601  	}
  1602  
  1603  	lang := language.Und
  1604  
  1605  	// Check if the user has set a language with SetLanguage.
  1606  	if langStr, err := boltDB.Language(); err != nil {
  1607  		cfg.Logger.Errorf("Error loading language from database: %v", err)
  1608  	} else if len(langStr) > 0 {
  1609  		if lang, err = parseLanguage(langStr); err != nil {
  1610  			cfg.Logger.Errorf("Error parsing language retrieved from database %q: %w", langStr, err)
  1611  		}
  1612  	}
  1613  
  1614  	// If they haven't changed the language through the UI, perhaps its set in
  1615  	// configuration.
  1616  	if lang.IsRoot() && cfg.Language != "" {
  1617  		if lang, err = parseLanguage(cfg.Language); err != nil {
  1618  			return nil, err
  1619  		}
  1620  	}
  1621  
  1622  	// Default language is English.
  1623  	if lang.IsRoot() {
  1624  		lang = language.AmericanEnglish
  1625  	}
  1626  
  1627  	cfg.Logger.Debugf("Using locale printer for %q", lang)
  1628  
  1629  	translations, found := locales[lang.String()]
  1630  	if !found {
  1631  		cfg.Logger.Warnf("Language %q not supported, falling back to %s", lang, originLang)
  1632  		lang = language.AmericanEnglish
  1633  		translations = locales[originLang]
  1634  	}
  1635  
  1636  	// Try to get the primary credentials, but ignore no-credentials error here
  1637  	// because the client may not be initialized.
  1638  	creds, err := boltDB.PrimaryCredentials()
  1639  	if err != nil && !errors.Is(err, db.ErrNoCredentials) {
  1640  		return nil, err
  1641  	}
  1642  
  1643  	seedGenerationTime, err := boltDB.SeedGenerationTime()
  1644  	if err != nil && !errors.Is(err, db.ErrNoSeedGenTime) {
  1645  		return nil, err
  1646  	}
  1647  
  1648  	var xCfg *ExtensionModeConfig
  1649  	if cfg.ExtensionModeFile != "" {
  1650  		b, err := os.ReadFile(cfg.ExtensionModeFile)
  1651  		if err != nil {
  1652  			return nil, fmt.Errorf("error reading extension mode file at %q: %w", cfg.ExtensionModeFile, err)
  1653  		}
  1654  		if err := json.Unmarshal(b, &xCfg); err != nil {
  1655  			return nil, fmt.Errorf("error unmarshalling extension mode file: %w", err)
  1656  		}
  1657  	}
  1658  
  1659  	c := &Core{
  1660  		cfg:           cfg,
  1661  		credentials:   creds,
  1662  		ready:         make(chan struct{}),
  1663  		rotate:        make(chan struct{}, 1),
  1664  		log:           cfg.Logger,
  1665  		db:            boltDB,
  1666  		conns:         make(map[string]*dexConnection),
  1667  		wallets:       make(map[uint32]*xcWallet),
  1668  		net:           cfg.Net,
  1669  		lockTimeTaker: dex.LockTimeTaker(cfg.Net),
  1670  		lockTimeMaker: dex.LockTimeMaker(cfg.Net),
  1671  		blockWaiters:  make(map[string]*blockWaiter),
  1672  		sentCommits:   make(map[order.Commitment]chan struct{}),
  1673  		tickSched:     make(map[order.OrderID]*time.Timer),
  1674  		// Allowing to change the constructor makes testing a lot easier.
  1675  		wsConstructor: comms.NewWsConn,
  1676  		newCrypter:    encrypt.NewCrypter,
  1677  		reCrypter:     encrypt.Deserialize,
  1678  		latencyQ:      wait.NewTickerQueue(recheckInterval),
  1679  		noteChans:     make(map[uint64]chan Notification),
  1680  
  1681  		extensionModeConfig: xCfg,
  1682  		seedGenerationTime:  seedGenerationTime,
  1683  
  1684  		fiatRateSources: make(map[string]*commonRateSource),
  1685  		reFiat:          make(chan struct{}, 1),
  1686  		pendingWallets:  make(map[uint32]bool),
  1687  
  1688  		notes:            make(chan asset.WalletNotification, 128),
  1689  		requestedActions: make(map[string]*asset.ActionRequiredNote),
  1690  	}
  1691  
  1692  	c.intl.Store(&locale{
  1693  		lang:    lang,
  1694  		m:       translations,
  1695  		printer: message.NewPrinter(lang),
  1696  	})
  1697  
  1698  	// Populate the initial user data. User won't include any DEX info yet, as
  1699  	// those are retrieved when Run is called and the core connects to the DEXes.
  1700  	c.log.Debugf("new client core created")
  1701  	return c, nil
  1702  }
  1703  
  1704  // Run runs the core. Satisfies the runner.Runner interface.
  1705  func (c *Core) Run(ctx context.Context) {
  1706  	c.log.Infof("Starting Bison Wallet core")
  1707  	// Store the context as a field, since we will need to spawn new DEX threads
  1708  	// when new accounts are registered.
  1709  	c.ctx = ctx
  1710  	if err := c.initialize(); err != nil { // connectDEX gets ctx for the wsConn
  1711  		c.log.Critical(err)
  1712  		close(c.ready) // unblock <-Ready()
  1713  		return
  1714  	}
  1715  	close(c.ready)
  1716  
  1717  	// The DB starts first and stops last.
  1718  	ctxDB, stopDB := context.WithCancel(context.Background())
  1719  	var dbWG sync.WaitGroup
  1720  	dbWG.Add(1)
  1721  	go func() {
  1722  		defer dbWG.Done()
  1723  		c.db.Run(ctxDB)
  1724  	}()
  1725  
  1726  	c.wg.Add(1)
  1727  	go func() {
  1728  		defer c.wg.Done()
  1729  		c.latencyQ.Run(ctx)
  1730  	}()
  1731  
  1732  	// Retrieve disabled fiat rate sources from database.
  1733  	disabledSources, err := c.db.DisabledRateSources()
  1734  	if err != nil {
  1735  		c.log.Errorf("Unable to retrieve disabled fiat rate source: %v", err)
  1736  	}
  1737  
  1738  	// Construct enabled fiat rate sources.
  1739  fetchers:
  1740  	for token, rateFetcher := range fiatRateFetchers {
  1741  		for _, v := range disabledSources {
  1742  			if token == v {
  1743  				continue fetchers
  1744  			}
  1745  		}
  1746  		c.fiatRateSources[token] = newCommonRateSource(rateFetcher)
  1747  	}
  1748  	c.fetchFiatExchangeRates(ctx)
  1749  
  1750  	// Start a goroutine to keep the FeeState updated.
  1751  	c.wg.Add(1)
  1752  	go func() {
  1753  		defer c.wg.Done()
  1754  		for {
  1755  			tick := time.NewTicker(time.Minute * 5)
  1756  			select {
  1757  			case <-tick.C:
  1758  				for _, w := range c.xcWallets() {
  1759  					if w.connected() {
  1760  						w.feeRate() // updates the fee state internally.
  1761  					}
  1762  				}
  1763  			case <-ctx.Done():
  1764  				return
  1765  			}
  1766  		}
  1767  	}()
  1768  
  1769  	// Start bond supervisor.
  1770  	c.wg.Add(1)
  1771  	go func() {
  1772  		defer c.wg.Done()
  1773  		c.watchBonds(ctx)
  1774  	}()
  1775  
  1776  	// Handle wallet notifications.
  1777  	c.wg.Add(1)
  1778  	go func() {
  1779  		defer c.wg.Done()
  1780  		for {
  1781  			select {
  1782  			case n := <-c.notes:
  1783  				c.handleWalletNotification(n)
  1784  			case <-ctx.Done():
  1785  				return
  1786  			}
  1787  		}
  1788  	}()
  1789  
  1790  	c.wg.Wait() // block here until all goroutines except DB complete
  1791  
  1792  	if err := c.db.SavePokes(c.pokes()); err != nil {
  1793  		c.log.Errorf("Error saving pokes: %v", err)
  1794  	}
  1795  
  1796  	// Stop the DB after dexConnections and other goroutines are done.
  1797  	stopDB()
  1798  	dbWG.Wait()
  1799  
  1800  	// At this point, it should be safe to access the data structures without
  1801  	// mutex protection. Goroutines have returned, and consumers should not call
  1802  	// Core methods after shutdown. We'll play it safe anyway.
  1803  
  1804  	// Clear account private keys and wait for the DEX ws connections that began
  1805  	// shutting down on context cancellation (the listen goroutines have already
  1806  	// returned however). Warn about specific active orders, and unlock any
  1807  	// locked coins for inactive orders that are not yet retired.
  1808  	for _, dc := range c.dexConnections() {
  1809  		// context is already canceled, allowing just a Wait(), but just in case
  1810  		// use Disconnect otherwise it could hang forever.
  1811  		dc.connMaster.Disconnect()
  1812  		dc.acct.lock()
  1813  
  1814  		// Note active orders, and unlock any coins locked by inactive orders.
  1815  		dc.tradeMtx.Lock()
  1816  		for _, trade := range dc.trades {
  1817  			oid := trade.ID()
  1818  			if trade.isActive() {
  1819  				c.log.Warnf("Shutting down with active order %v in status %v.", oid, trade.metaData.Status)
  1820  				continue
  1821  			}
  1822  			c.log.Debugf("Retiring inactive order %v. Unlocking coins = %v",
  1823  				oid, trade.coinsLocked || trade.changeLocked)
  1824  			delete(dc.trades, oid) // for inspection/debugging
  1825  			trade.returnCoins()
  1826  			// Do not bother with OrderNote/SubjectOrderRetired and BalanceNote
  1827  			// notes since any web/rpc servers should be down by now. Go
  1828  			// consumers can check orders on restart.
  1829  		}
  1830  		dc.tradeMtx.Unlock()
  1831  	}
  1832  
  1833  	// Lock and disconnect the wallets.
  1834  	c.walletMtx.Lock()
  1835  	defer c.walletMtx.Unlock()
  1836  	for assetID, wallet := range c.wallets {
  1837  		delete(c.wallets, assetID)
  1838  		if !wallet.connected() {
  1839  			continue
  1840  		}
  1841  		if !c.cfg.NoAutoWalletLock && wallet.unlocked() { // no-op if Logout did it
  1842  			symb := strings.ToUpper(unbip(assetID))
  1843  			c.log.Infof("Locking %s wallet", symb)
  1844  			if err := wallet.Lock(walletLockTimeout); err != nil {
  1845  				c.log.Errorf("Failed to lock %v wallet: %v", symb, err)
  1846  			}
  1847  		}
  1848  		wallet.Disconnect()
  1849  	}
  1850  
  1851  	c.log.Infof("Bison Wallet core off")
  1852  }
  1853  
  1854  // Ready returns a channel that is closed when Run completes its initialization
  1855  // tasks and Core becomes ready for use.
  1856  func (c *Core) Ready() <-chan struct{} {
  1857  	return c.ready
  1858  }
  1859  
  1860  func (c *Core) locale() *locale {
  1861  	return c.intl.Load().(*locale)
  1862  }
  1863  
  1864  // SetLanguage sets the langauge used for notifications. The language set with
  1865  // SetLanguage persists through restarts and will override any language set in
  1866  // configuration.
  1867  func (c *Core) SetLanguage(lang string) error {
  1868  	tag, err := language.Parse(lang)
  1869  	if err != nil {
  1870  		return fmt.Errorf("error parsing language %q: %w", lang, err)
  1871  	}
  1872  
  1873  	translations, found := locales[lang]
  1874  	if !found {
  1875  		c.log.Warnf("Language %q not supported, using %s", lang, originLang)
  1876  		lang = originLang
  1877  		tag, _ = language.Parse(originLang) // Safe to ignore error, originLang is known valid
  1878  		translations = locales[originLang]
  1879  	}
  1880  	if err := c.db.SetLanguage(lang); err != nil {
  1881  		return fmt.Errorf("error storing language: %w", err)
  1882  	}
  1883  	c.intl.Store(&locale{
  1884  		lang:    tag,
  1885  		m:       translations,
  1886  		printer: message.NewPrinter(tag),
  1887  	})
  1888  	return nil
  1889  }
  1890  
  1891  // Language is the currently configured language.
  1892  func (c *Core) Language() string {
  1893  	return c.locale().lang.String()
  1894  }
  1895  
  1896  // BackupDB makes a backup of the database at the specified location, optionally
  1897  // overwriting any existing file and compacting the database.
  1898  func (c *Core) BackupDB(dst string, overwrite, compact bool) error {
  1899  	return c.db.BackupTo(dst, overwrite, compact)
  1900  }
  1901  
  1902  const defaultDEXPort = "7232"
  1903  
  1904  // addrHost returns the host or url:port pair for an address.
  1905  func addrHost(addr string) (string, error) {
  1906  	addr = strings.TrimSpace(addr)
  1907  	const defaultHost = "localhost"
  1908  	const missingPort = "missing port in address"
  1909  	// Empty addresses are localhost.
  1910  	if addr == "" {
  1911  		return defaultHost + ":" + defaultDEXPort, nil
  1912  	}
  1913  	host, port, splitErr := net.SplitHostPort(addr)
  1914  	_, portErr := strconv.ParseUint(port, 10, 16)
  1915  
  1916  	// net.SplitHostPort will error on anything not in the format
  1917  	// string:string or :string or if a colon is in an unexpected position,
  1918  	// such as in the scheme.
  1919  	// If the port isn't a port, it must also be parsed.
  1920  	if splitErr != nil || portErr != nil {
  1921  		// Any address with no colons is appended with the default port.
  1922  		var addrErr *net.AddrError
  1923  		if errors.As(splitErr, &addrErr) && addrErr.Err == missingPort {
  1924  			host = strings.Trim(addrErr.Addr, "[]") // JoinHostPort expects no brackets for ipv6 hosts
  1925  			return net.JoinHostPort(host, defaultDEXPort), nil
  1926  		}
  1927  		// These are addresses with at least one colon in an unexpected
  1928  		// position.
  1929  		a, err := url.Parse(addr)
  1930  		// This address is of an unknown format.
  1931  		if err != nil {
  1932  			return "", fmt.Errorf("addrHost: unable to parse address '%s'", addr)
  1933  		}
  1934  		host, port = a.Hostname(), a.Port()
  1935  		// If the address parses but there is no port, append the default port.
  1936  		if port == "" {
  1937  			return net.JoinHostPort(host, defaultDEXPort), nil
  1938  		}
  1939  	}
  1940  	// We have a port but no host. Replace with localhost.
  1941  	if host == "" {
  1942  		host = defaultHost
  1943  	}
  1944  	return net.JoinHostPort(host, port), nil
  1945  }
  1946  
  1947  // creds returns the *PrimaryCredentials.
  1948  func (c *Core) creds() *db.PrimaryCredentials {
  1949  	c.credMtx.RLock()
  1950  	defer c.credMtx.RUnlock()
  1951  	if c.credentials == nil {
  1952  		return nil
  1953  	}
  1954  	if len(c.credentials.EncInnerKey) == 0 {
  1955  		// database upgraded, but Core hasn't updated the PrimaryCredentials.
  1956  		return nil
  1957  	}
  1958  	return c.credentials
  1959  }
  1960  
  1961  // setCredentials stores the *PrimaryCredentials.
  1962  func (c *Core) setCredentials(creds *db.PrimaryCredentials) {
  1963  	c.credMtx.Lock()
  1964  	c.credentials = creds
  1965  	c.credMtx.Unlock()
  1966  }
  1967  
  1968  // Network returns the current DEX network.
  1969  func (c *Core) Network() dex.Network {
  1970  	return c.net
  1971  }
  1972  
  1973  // Exchanges creates a map of *Exchange keyed by host, including markets and
  1974  // orders.
  1975  func (c *Core) Exchanges() map[string]*Exchange {
  1976  	dcs := c.dexConnections()
  1977  	infos := make(map[string]*Exchange, len(dcs))
  1978  	for _, dc := range dcs {
  1979  		infos[dc.acct.host] = c.exchangeInfo(dc)
  1980  	}
  1981  	return infos
  1982  }
  1983  
  1984  // Exchange returns an exchange with a certain host. It returns an error if
  1985  // no exchange exists at that host.
  1986  func (c *Core) Exchange(host string) (*Exchange, error) {
  1987  	dc, _, err := c.dex(host)
  1988  	if err != nil {
  1989  		return nil, err
  1990  	}
  1991  	return c.exchangeInfo(dc), nil
  1992  }
  1993  
  1994  // ExchangeMarket returns the market with the given base and quote assets at the
  1995  // given host. It returns an error if no market exists at that host.
  1996  func (c *Core) ExchangeMarket(host string, baseID, quoteID uint32) (*Market, error) {
  1997  	dc, _, err := c.dex(host)
  1998  	if err != nil {
  1999  		return nil, err
  2000  	}
  2001  
  2002  	mkt := dc.coreMarket(marketName(baseID, quoteID))
  2003  	if mkt == nil {
  2004  		return nil, fmt.Errorf("no market found for %s-%s at %s", unbip(baseID), unbip(quoteID), host)
  2005  	}
  2006  
  2007  	return mkt, nil
  2008  }
  2009  
  2010  // MarketConfig gets the configuration for the market.
  2011  func (c *Core) MarketConfig(host string, baseID, quoteID uint32) (*msgjson.Market, error) {
  2012  	dc, _, err := c.dex(host)
  2013  	if err != nil {
  2014  		return nil, err
  2015  	}
  2016  	for _, mkt := range dc.config().Markets {
  2017  		if mkt.Base == baseID && mkt.Quote == quoteID {
  2018  			return mkt, nil
  2019  		}
  2020  	}
  2021  	return nil, fmt.Errorf("market (%d, %d) not found for host %s", baseID, quoteID, host)
  2022  }
  2023  
  2024  // dexConnections creates a slice of the *dexConnection in c.conns.
  2025  func (c *Core) dexConnections() []*dexConnection {
  2026  	c.connMtx.RLock()
  2027  	defer c.connMtx.RUnlock()
  2028  	conns := make([]*dexConnection, 0, len(c.conns))
  2029  	for _, conn := range c.conns {
  2030  		conns = append(conns, conn)
  2031  	}
  2032  	return conns
  2033  }
  2034  
  2035  // wallet gets the wallet for the specified asset ID in a thread-safe way.
  2036  func (c *Core) wallet(assetID uint32) (*xcWallet, bool) {
  2037  	c.walletMtx.RLock()
  2038  	defer c.walletMtx.RUnlock()
  2039  	w, found := c.wallets[assetID]
  2040  	return w, found
  2041  }
  2042  
  2043  // encryptionKey retrieves the application encryption key. The password is used
  2044  // to recreate the outer key/crypter, which is then used to decode and recreate
  2045  // the inner key/crypter.
  2046  func (c *Core) encryptionKey(pw []byte) (encrypt.Crypter, error) {
  2047  	creds := c.creds()
  2048  	if creds == nil {
  2049  		return nil, fmt.Errorf("primary credentials not retrieved. Is the client initialized?")
  2050  	}
  2051  	outerCrypter, err := c.reCrypter(pw, creds.OuterKeyParams)
  2052  	if err != nil {
  2053  		return nil, fmt.Errorf("outer key deserialization error: %w", err)
  2054  	}
  2055  	defer outerCrypter.Close()
  2056  	innerKey, err := outerCrypter.Decrypt(creds.EncInnerKey)
  2057  	if err != nil {
  2058  		return nil, fmt.Errorf("inner key decryption error: %w", err)
  2059  	}
  2060  	innerCrypter, err := c.reCrypter(innerKey, creds.InnerKeyParams)
  2061  	if err != nil {
  2062  		return nil, fmt.Errorf("inner key deserialization error: %w", err)
  2063  	}
  2064  	return innerCrypter, nil
  2065  }
  2066  
  2067  func (c *Core) storeDepositAddress(wdbID []byte, addr string) error {
  2068  	// Store the new address in the DB.
  2069  	dbWallet, err := c.db.Wallet(wdbID)
  2070  	if err != nil {
  2071  		return fmt.Errorf("error retrieving DB wallet: %w", err)
  2072  	}
  2073  	dbWallet.Address = addr
  2074  	return c.db.UpdateWallet(dbWallet)
  2075  }
  2076  
  2077  // connectAndUpdateWalletResumeTrades creates a connection to a wallet and
  2078  // updates the balance. If resumeTrades is set to true, an attempt to resume
  2079  // any trades that were unable to be resumed at startup will be made.
  2080  func (c *Core) connectAndUpdateWalletResumeTrades(w *xcWallet, resumeTrades bool) error {
  2081  	assetID := w.AssetID
  2082  
  2083  	token := asset.TokenInfo(assetID)
  2084  	if token != nil {
  2085  		parentWallet, found := c.wallet(token.ParentID)
  2086  		if !found {
  2087  			return fmt.Errorf("token %s wallet has no %s parent?", unbip(assetID), unbip(token.ParentID))
  2088  		}
  2089  		if !parentWallet.connected() {
  2090  			if err := c.connectAndUpdateWalletResumeTrades(parentWallet, resumeTrades); err != nil {
  2091  				return fmt.Errorf("failed to connect %s parent wallet for %s token: %v",
  2092  					unbip(token.ParentID), unbip(assetID), err)
  2093  			}
  2094  		}
  2095  	}
  2096  
  2097  	c.log.Debugf("Connecting wallet for %s", unbip(assetID))
  2098  	addr := w.currentDepositAddress()
  2099  	newAddr, err := c.connectWalletResumeTrades(w, resumeTrades)
  2100  	if err != nil {
  2101  		return fmt.Errorf("connectWallet: %w", err) // core.Error with code connectWalletErr
  2102  	}
  2103  	if newAddr != addr {
  2104  		c.log.Infof("New deposit address for %v wallet: %v", unbip(assetID), newAddr)
  2105  		if err = c.storeDepositAddress(w.dbID, newAddr); err != nil {
  2106  			return fmt.Errorf("storeDepositAddress: %w", err)
  2107  		}
  2108  	}
  2109  	// First update balances since it is included in WalletState. Ignore errors
  2110  	// because some wallets may not reveal balance until unlocked.
  2111  	_, err = c.updateWalletBalance(w)
  2112  	if err != nil {
  2113  		// Warn because the balances will be stale.
  2114  		c.log.Warnf("Could not retrieve balances from %s wallet: %v", unbip(assetID), err)
  2115  	}
  2116  
  2117  	c.notify(newWalletStateNote(w.state()))
  2118  	return nil
  2119  }
  2120  
  2121  // connectAndUpdateWallet creates a connection to a wallet and updates the
  2122  // balance.
  2123  func (c *Core) connectAndUpdateWallet(w *xcWallet) error {
  2124  	return c.connectAndUpdateWalletResumeTrades(w, true)
  2125  }
  2126  
  2127  // connectedWallet fetches a wallet and will connect the wallet if it is not
  2128  // already connected. If the wallet gets connected, this also emits WalletState
  2129  // and WalletBalance notification.
  2130  func (c *Core) connectedWallet(assetID uint32) (*xcWallet, error) {
  2131  	wallet, exists := c.wallet(assetID)
  2132  	if !exists {
  2133  		return nil, newError(missingWalletErr, "no configured wallet found for %s (%d)",
  2134  			strings.ToUpper(unbip(assetID)), assetID)
  2135  	}
  2136  	if !wallet.connected() {
  2137  		err := c.connectAndUpdateWallet(wallet)
  2138  		if err != nil {
  2139  			return nil, err
  2140  		}
  2141  	}
  2142  	return wallet, nil
  2143  }
  2144  
  2145  // connectWalletResumeTrades connects to the wallet and returns the deposit
  2146  // address validated by the xcWallet after connecting. If the wallet backend
  2147  // is still syncing, this also starts a goroutine to monitor sync status,
  2148  // emitting WalletStateNotes on each progress update. If resumeTrades is set to
  2149  // true, an attempt to resume any trades that were unable to be resumed at
  2150  // startup will be made.
  2151  func (c *Core) connectWalletResumeTrades(w *xcWallet, resumeTrades bool) (depositAddr string, err error) {
  2152  	if w.isDisabled() {
  2153  		return "", fmt.Errorf(walletDisabledErrStr, w.Symbol)
  2154  	}
  2155  
  2156  	err = w.Connect() // ensures valid deposit address
  2157  	if err != nil {
  2158  		return "", newError(connectWalletErr, "failed to connect %s wallet: %w", w.Symbol, err)
  2159  	}
  2160  
  2161  	// This may be a wallet that does not require a password, so we can attempt
  2162  	// to resume any active trades.
  2163  	if resumeTrades {
  2164  		go c.resumeTrades(nil)
  2165  	}
  2166  
  2167  	w.mtx.RLock()
  2168  	depositAddr = w.address
  2169  	synced := w.syncStatus.Synced
  2170  	w.mtx.RUnlock()
  2171  
  2172  	// If the wallet is synced, update the bond reserves, logging any balance
  2173  	// insufficiencies, otherwise start a loop to check the sync status until it
  2174  	// is.
  2175  	if synced {
  2176  		c.updateBondReserves(w.AssetID)
  2177  	} else {
  2178  		c.startWalletSyncMonitor(w)
  2179  	}
  2180  
  2181  	return
  2182  }
  2183  
  2184  // connectWallet connects to the wallet and returns the deposit address
  2185  // validated by the xcWallet after connecting. If the wallet backend is still
  2186  // syncing, this also starts a goroutine to monitor sync status, emitting
  2187  // WalletStateNotes on each progress update.
  2188  func (c *Core) connectWallet(w *xcWallet) (depositAddr string, err error) {
  2189  	return c.connectWalletResumeTrades(w, true)
  2190  }
  2191  
  2192  // unlockWalletResumeTrades will unlock a wallet if it is not yet unlocked. If
  2193  // resumeTrades is set to true, an attempt to resume any trades that were
  2194  // unable to be resumed at startup will be made.
  2195  func (c *Core) unlockWalletResumeTrades(crypter encrypt.Crypter, wallet *xcWallet, resumeTrades bool) error {
  2196  	// Unlock if either the backend itself is locked or if we lack a cached
  2197  	// unencrypted password for encrypted wallets.
  2198  	if !wallet.unlocked() {
  2199  		if crypter == nil {
  2200  			return newError(noAuthError, "wallet locked and no password provided")
  2201  		}
  2202  		// Note that in cases where we already had the cached decrypted password
  2203  		// but it was just the backend reporting as locked, only unlocking the
  2204  		// backend is needed but this redecrypts the password using the provided
  2205  		// crypter. This case could instead be handled with a refreshUnlock.
  2206  		err := wallet.Unlock(crypter)
  2207  		if err != nil {
  2208  			return newError(walletAuthErr, "failed to unlock %s wallet: %w",
  2209  				unbip(wallet.AssetID), err)
  2210  		}
  2211  		// Notify new wallet state.
  2212  		c.notify(newWalletStateNote(wallet.state()))
  2213  
  2214  		if resumeTrades {
  2215  			go c.resumeTrades(crypter)
  2216  		}
  2217  	}
  2218  
  2219  	return nil
  2220  }
  2221  
  2222  // unlockWallet will unlock a wallet if it is not yet unlocked.
  2223  func (c *Core) unlockWallet(crypter encrypt.Crypter, wallet *xcWallet) error {
  2224  	return c.unlockWalletResumeTrades(crypter, wallet, true)
  2225  }
  2226  
  2227  // connectAndUnlockResumeTrades will connect to the wallet if not already
  2228  // connected, and unlock the wallet if not already unlocked. If the wallet
  2229  // backend is still syncing, this also starts a goroutine to monitor sync
  2230  // status, emitting WalletStateNotes on each progress update. If resumeTrades
  2231  // is set to true, an attempt to resume any trades that were unable to be
  2232  // resumed at startup will be made.
  2233  func (c *Core) connectAndUnlockResumeTrades(crypter encrypt.Crypter, wallet *xcWallet, resumeTrades bool) error {
  2234  	if !wallet.connected() {
  2235  		err := c.connectAndUpdateWalletResumeTrades(wallet, resumeTrades)
  2236  		if err != nil {
  2237  			return err
  2238  		}
  2239  	}
  2240  
  2241  	return c.unlockWalletResumeTrades(crypter, wallet, resumeTrades)
  2242  }
  2243  
  2244  // connectAndUnlock will connect to the wallet if not already connected,
  2245  // and unlock the wallet if not already unlocked. If the wallet backend
  2246  // is still syncing, this also starts a goroutine to monitor sync status,
  2247  // emitting WalletStateNotes on each progress update.
  2248  func (c *Core) connectAndUnlock(crypter encrypt.Crypter, wallet *xcWallet) error {
  2249  	return c.connectAndUnlockResumeTrades(crypter, wallet, true)
  2250  }
  2251  
  2252  // walletBalance gets the xcWallet's current WalletBalance, which includes the
  2253  // db.Balance plus order/contract locked amounts. The data is not stored. Use
  2254  // updateWalletBalance instead to also update xcWallet.balance and the DB.
  2255  func (c *Core) walletBalance(wallet *xcWallet) (*WalletBalance, error) {
  2256  	bal, err := wallet.Balance()
  2257  	if err != nil {
  2258  		return nil, err
  2259  	}
  2260  	contractLockedAmt, orderLockedAmt, bondLockedAmt := c.lockedAmounts(wallet.AssetID)
  2261  	return &WalletBalance{
  2262  		Balance: &db.Balance{
  2263  			Balance: *bal,
  2264  			Stamp:   time.Now(),
  2265  		},
  2266  		OrderLocked:    orderLockedAmt,
  2267  		ContractLocked: contractLockedAmt,
  2268  		BondLocked:     bondLockedAmt,
  2269  	}, nil
  2270  }
  2271  
  2272  // updateWalletBalance retrieves balances for the wallet, updates
  2273  // xcWallet.balance and the balance in the DB, and emits a BalanceNote.
  2274  func (c *Core) updateWalletBalance(wallet *xcWallet) (*WalletBalance, error) {
  2275  	walletBal, err := c.walletBalance(wallet)
  2276  	if err != nil {
  2277  		return nil, err
  2278  	}
  2279  	return walletBal, c.storeAndSendWalletBalance(wallet, walletBal)
  2280  }
  2281  
  2282  func (c *Core) storeAndSendWalletBalance(wallet *xcWallet, walletBal *WalletBalance) error {
  2283  	wallet.setBalance(walletBal)
  2284  
  2285  	// Store the db.Balance.
  2286  	err := c.db.UpdateBalance(wallet.dbID, walletBal.Balance)
  2287  	if err != nil {
  2288  		return fmt.Errorf("error updating %s balance in database: %w", unbip(wallet.AssetID), err)
  2289  	}
  2290  	c.notify(newBalanceNote(wallet.AssetID, walletBal))
  2291  	return nil
  2292  }
  2293  
  2294  // lockedAmounts returns the total amount locked in unredeemed and unrefunded
  2295  // swaps (contractLocked), the total amount locked by orders for future swaps
  2296  // (orderLocked), and the total amount locked in fidelity bonds (bondLocked).
  2297  // Only applies to trades where the specified assetID is the fromAssetID.
  2298  func (c *Core) lockedAmounts(assetID uint32) (contractLocked, orderLocked, bondLocked uint64) {
  2299  	for _, dc := range c.dexConnections() {
  2300  		bondLocked, _ = dc.bondTotal(assetID)
  2301  		for _, tracker := range dc.trackedTrades() {
  2302  			if tracker.fromAssetID == assetID {
  2303  				tracker.mtx.RLock()
  2304  				contractLocked += tracker.unspentContractAmounts()
  2305  				orderLocked += tracker.lockedAmount()
  2306  				tracker.mtx.RUnlock()
  2307  			}
  2308  		}
  2309  	}
  2310  	return
  2311  }
  2312  
  2313  // updateBalances updates the balance for every key in the counter map.
  2314  // Notifications are sent.
  2315  func (c *Core) updateBalances(assets assetMap) {
  2316  	if len(assets) == 0 {
  2317  		return
  2318  	}
  2319  	for assetID := range assets {
  2320  		w, exists := c.wallet(assetID)
  2321  		if !exists {
  2322  			// This should never be the case, but log an error in case I'm
  2323  			// wrong or something changes.
  2324  			c.log.Errorf("non-existent %d wallet should exist", assetID)
  2325  			continue
  2326  		}
  2327  		_, err := c.updateWalletBalance(w)
  2328  		if err != nil {
  2329  			c.log.Errorf("error updating %q balance: %v", unbip(assetID), err)
  2330  			continue
  2331  		}
  2332  
  2333  		if token := asset.TokenInfo(assetID); token != nil {
  2334  			if _, alreadyUpdating := assets[token.ParentID]; alreadyUpdating {
  2335  				continue
  2336  			}
  2337  			parentWallet, exists := c.wallet(token.ParentID)
  2338  			if !exists {
  2339  				c.log.Errorf("non-existent %d wallet should exist", token.ParentID)
  2340  				continue
  2341  			}
  2342  			_, err := c.updateWalletBalance(parentWallet)
  2343  			if err != nil {
  2344  				c.log.Errorf("error updating %q balance: %v", unbip(token.ParentID), err)
  2345  				continue
  2346  			}
  2347  		}
  2348  	}
  2349  }
  2350  
  2351  // updateAssetBalance updates the balance for the specified asset. A
  2352  // notification is sent.
  2353  func (c *Core) updateAssetBalance(assetID uint32) {
  2354  	c.updateBalances(assetMap{assetID: struct{}{}})
  2355  }
  2356  
  2357  // xcWallets creates a slice of the c.wallets xcWallets.
  2358  func (c *Core) xcWallets() []*xcWallet {
  2359  	c.walletMtx.RLock()
  2360  	defer c.walletMtx.RUnlock()
  2361  	wallets := make([]*xcWallet, 0, len(c.wallets))
  2362  	for _, wallet := range c.wallets {
  2363  		wallets = append(wallets, wallet)
  2364  	}
  2365  	return wallets
  2366  }
  2367  
  2368  // Wallets creates a slice of WalletState for all known wallets.
  2369  func (c *Core) Wallets() []*WalletState {
  2370  	wallets := c.xcWallets()
  2371  	state := make([]*WalletState, 0, len(wallets))
  2372  	for _, wallet := range wallets {
  2373  		state = append(state, wallet.state())
  2374  	}
  2375  	return state
  2376  }
  2377  
  2378  // ToggleWalletStatus changes a wallet's status to either disabled or enabled.
  2379  func (c *Core) ToggleWalletStatus(assetID uint32, disable bool) error {
  2380  	wallet, exists := c.wallet(assetID)
  2381  	if !exists {
  2382  		return newError(missingWalletErr, "no configured wallet found for %s (%d)",
  2383  			strings.ToUpper(unbip(assetID)), assetID)
  2384  	}
  2385  
  2386  	// Return early if this wallet is already disabled or already enabled.
  2387  	if disable == wallet.isDisabled() {
  2388  		return nil
  2389  	}
  2390  
  2391  	// If this wallet is a parent, disable/enable all token wallets.
  2392  	var affectedWallets []*xcWallet
  2393  	if disable {
  2394  		// Ensure wallet is not a parent of an enabled token wallet with active
  2395  		// orders.
  2396  		if assetInfo := asset.Asset(assetID); assetInfo != nil {
  2397  			for id := range assetInfo.Tokens {
  2398  				if wallet, exists := c.wallet(id); exists && !wallet.isDisabled() {
  2399  					if c.assetHasActiveOrders(wallet.AssetID) {
  2400  						return newError(activeOrdersErr, "active orders for %v", unbip(wallet.AssetID))
  2401  					}
  2402  					affectedWallets = append(affectedWallets, wallet)
  2403  				}
  2404  			}
  2405  		}
  2406  
  2407  		// If wallet is a parent wallet, it will be the last to be disconnected
  2408  		// and disabled.
  2409  		affectedWallets = append(affectedWallets, wallet)
  2410  
  2411  		if c.assetHasActiveOrders(assetID) {
  2412  			return newError(activeOrdersErr, "active orders for %v", unbip(assetID))
  2413  		}
  2414  
  2415  		// Ensure wallet is not an active bond asset wallet. This check will
  2416  		// cover for token wallets if this wallet is a parent.
  2417  		if c.isActiveBondAsset(assetID, true) {
  2418  			return newError(bondAssetErr, "%v is an active bond asset wallet", unbip(assetID))
  2419  		}
  2420  
  2421  		// Disconnect and disable all affected wallets.
  2422  		for _, wallet := range affectedWallets {
  2423  			if wallet.connected() {
  2424  				wallet.Disconnect() // before disable or it refuses
  2425  			}
  2426  			wallet.setDisabled(true)
  2427  		}
  2428  	} else {
  2429  		if wallet.parent != nil && wallet.parent.isDisabled() {
  2430  			// Ensure parent wallet starts first.
  2431  			affectedWallets = append(affectedWallets, wallet.parent)
  2432  		}
  2433  
  2434  		affectedWallets = append(affectedWallets, wallet)
  2435  
  2436  		for _, wallet := range affectedWallets {
  2437  			// Update wallet status before attempting to connect wallet because disabled
  2438  			// wallets cannot be connected to.
  2439  			wallet.setDisabled(false)
  2440  
  2441  			// Attempt to connect wallet.
  2442  			err := c.connectAndUpdateWallet(wallet)
  2443  			if err != nil {
  2444  				c.log.Errorf("Error connecting to %s wallet: %v", unbip(assetID), err)
  2445  			}
  2446  		}
  2447  	}
  2448  
  2449  	for _, wallet := range affectedWallets {
  2450  		// Update db with wallet status.
  2451  		err := c.db.UpdateWalletStatus(wallet.dbID, disable)
  2452  		if err != nil {
  2453  			return fmt.Errorf("db.UpdateWalletStatus error: %w", err)
  2454  		}
  2455  
  2456  		c.notify(newWalletStateNote(wallet.state()))
  2457  	}
  2458  
  2459  	return nil
  2460  }
  2461  
  2462  // SupportedAssets returns a map of asset information for supported assets.
  2463  func (c *Core) SupportedAssets() map[uint32]*SupportedAsset {
  2464  	return c.assetMap()
  2465  }
  2466  
  2467  func (c *Core) walletCreationPending(tokenID uint32) bool {
  2468  	c.pendingWalletsMtx.RLock()
  2469  	defer c.pendingWalletsMtx.RUnlock()
  2470  	return c.pendingWallets[tokenID]
  2471  }
  2472  
  2473  func (c *Core) setWalletCreationPending(tokenID uint32) error {
  2474  	c.pendingWalletsMtx.Lock()
  2475  	defer c.pendingWalletsMtx.Unlock()
  2476  	if c.pendingWallets[tokenID] {
  2477  		return fmt.Errorf("creation already pending for %s", unbip(tokenID))
  2478  	}
  2479  	c.pendingWallets[tokenID] = true
  2480  	return nil
  2481  }
  2482  
  2483  func (c *Core) setWalletCreationComplete(tokenID uint32) {
  2484  	c.pendingWalletsMtx.Lock()
  2485  	delete(c.pendingWallets, tokenID)
  2486  	c.pendingWalletsMtx.Unlock()
  2487  }
  2488  
  2489  // assetMap returns a map of asset information for supported assets.
  2490  func (c *Core) assetMap() map[uint32]*SupportedAsset {
  2491  	supported := asset.Assets()
  2492  	assets := make(map[uint32]*SupportedAsset, len(supported))
  2493  	c.walletMtx.RLock()
  2494  	defer c.walletMtx.RUnlock()
  2495  	for assetID, asset := range supported {
  2496  		var wallet *WalletState
  2497  		w, found := c.wallets[assetID]
  2498  		if found {
  2499  			wallet = w.state()
  2500  		}
  2501  		assets[assetID] = &SupportedAsset{
  2502  			ID:       assetID,
  2503  			Symbol:   asset.Symbol,
  2504  			Wallet:   wallet,
  2505  			Info:     asset.Info,
  2506  			Name:     asset.Info.Name,
  2507  			UnitInfo: asset.Info.UnitInfo,
  2508  		}
  2509  		for tokenID, token := range asset.Tokens {
  2510  			wallet = nil
  2511  			w, found := c.wallets[tokenID]
  2512  			if found {
  2513  				wallet = w.state()
  2514  			}
  2515  			assets[tokenID] = &SupportedAsset{
  2516  				ID:                    tokenID,
  2517  				Symbol:                dex.BipIDSymbol(tokenID),
  2518  				Wallet:                wallet,
  2519  				Token:                 token,
  2520  				Name:                  token.Name,
  2521  				UnitInfo:              token.UnitInfo,
  2522  				WalletCreationPending: c.walletCreationPending(tokenID),
  2523  			}
  2524  		}
  2525  	}
  2526  	return assets
  2527  }
  2528  
  2529  func (c *Core) asset(assetID uint32) *SupportedAsset {
  2530  	var wallet *WalletState
  2531  	w, _ := c.wallet(assetID)
  2532  	if w != nil {
  2533  		wallet = w.state()
  2534  	}
  2535  	regAsset := asset.Asset(assetID)
  2536  	if regAsset != nil {
  2537  		return &SupportedAsset{
  2538  			ID:       assetID,
  2539  			Symbol:   regAsset.Symbol,
  2540  			Wallet:   wallet,
  2541  			Info:     regAsset.Info,
  2542  			Name:     regAsset.Info.Name,
  2543  			UnitInfo: regAsset.Info.UnitInfo,
  2544  		}
  2545  	}
  2546  
  2547  	token := asset.TokenInfo(assetID)
  2548  	if token == nil {
  2549  		return nil
  2550  	}
  2551  
  2552  	return &SupportedAsset{
  2553  		ID:                    assetID,
  2554  		Symbol:                dex.BipIDSymbol(assetID),
  2555  		Wallet:                wallet,
  2556  		Token:                 token,
  2557  		Name:                  token.Name,
  2558  		UnitInfo:              token.UnitInfo,
  2559  		WalletCreationPending: c.walletCreationPending(assetID),
  2560  	}
  2561  }
  2562  
  2563  // User is a thread-safe getter for the User.
  2564  func (c *Core) User() *User {
  2565  	return &User{
  2566  		Assets:             c.assetMap(),
  2567  		Exchanges:          c.Exchanges(),
  2568  		Initialized:        c.IsInitialized(),
  2569  		SeedGenerationTime: c.seedGenerationTime,
  2570  		FiatRates:          c.fiatConversions(),
  2571  		Net:                c.net,
  2572  		ExtensionConfig:    c.extensionModeConfig,
  2573  		Actions:            c.requestedActionsList(),
  2574  	}
  2575  }
  2576  
  2577  func (c *Core) requestedActionsList() []*asset.ActionRequiredNote {
  2578  	c.requestedActionMtx.RLock()
  2579  	defer c.requestedActionMtx.RUnlock()
  2580  	actions := make([]*asset.ActionRequiredNote, 0, len(c.requestedActions))
  2581  	for _, a := range c.requestedActions {
  2582  		actions = append(actions, a)
  2583  	}
  2584  	return actions
  2585  }
  2586  
  2587  // CreateWallet creates a new exchange wallet.
  2588  func (c *Core) CreateWallet(appPW, walletPW []byte, form *WalletForm) error {
  2589  	assetID := form.AssetID
  2590  	symbol := unbip(assetID)
  2591  	_, exists := c.wallet(assetID)
  2592  	if exists {
  2593  		return fmt.Errorf("%s wallet already exists", symbol)
  2594  	}
  2595  
  2596  	crypter, err := c.encryptionKey(appPW)
  2597  	if err != nil {
  2598  		return err
  2599  	}
  2600  
  2601  	var creationQueued bool
  2602  	defer func() {
  2603  		if !creationQueued {
  2604  			crypter.Close()
  2605  		}
  2606  	}()
  2607  
  2608  	// If this isn't a token, easy route.
  2609  	token := asset.TokenInfo(assetID)
  2610  	if token == nil {
  2611  		_, err = c.createWalletOrToken(crypter, walletPW, form)
  2612  		return err
  2613  	}
  2614  
  2615  	// Prevent two different tokens from trying to create the parent simultaneously.
  2616  	if err = c.setWalletCreationPending(token.ParentID); err != nil {
  2617  		return err
  2618  	}
  2619  	defer c.setWalletCreationComplete(token.ParentID)
  2620  
  2621  	// If the parent already exists, easy route.
  2622  	_, found := c.wallet(token.ParentID)
  2623  	if found {
  2624  		_, err = c.createWalletOrToken(crypter, walletPW, form)
  2625  		return err
  2626  	}
  2627  
  2628  	// Double-registration mode. The parent wallet will be created
  2629  	// synchronously, then a goroutine is launched to wait for the parent to
  2630  	// sync before creating the token wallet. The caller can get information
  2631  	// about the asynchronous creation from WalletCreationNote notifications.
  2632  
  2633  	// First check that they configured the parent asset.
  2634  	if form.ParentForm == nil {
  2635  		return fmt.Errorf("no parent wallet %d for token %d (%s), and no parent asset configuration provided",
  2636  			token.ParentID, assetID, unbip(assetID))
  2637  	}
  2638  	if form.ParentForm.AssetID != token.ParentID {
  2639  		return fmt.Errorf("parent form asset ID %d is not expected value %d",
  2640  			form.ParentForm.AssetID, token.ParentID)
  2641  	}
  2642  
  2643  	// Create the parent synchronously.
  2644  	parentWallet, err := c.createWalletOrToken(crypter, walletPW, form.ParentForm)
  2645  	if err != nil {
  2646  		return fmt.Errorf("error creating parent wallet: %v", err)
  2647  	}
  2648  
  2649  	if err = c.setWalletCreationPending(assetID); err != nil {
  2650  		return err
  2651  	}
  2652  
  2653  	// Start a goroutine to wait until the parent wallet is synced, and then
  2654  	// begin creation of the token wallet.
  2655  	c.wg.Add(1)
  2656  
  2657  	c.notify(newWalletCreationNote(TopicCreationQueued, "", "", db.Data, assetID))
  2658  
  2659  	go func() {
  2660  		defer c.wg.Done()
  2661  		defer c.setWalletCreationComplete(assetID)
  2662  		defer crypter.Close()
  2663  
  2664  		for {
  2665  			parentWallet.mtx.RLock()
  2666  			synced := parentWallet.syncStatus.Synced
  2667  			parentWallet.mtx.RUnlock()
  2668  			if synced {
  2669  				break
  2670  			}
  2671  			select {
  2672  			case <-c.ctx.Done():
  2673  				return
  2674  			case <-time.After(time.Second):
  2675  			}
  2676  		}
  2677  		// If there was a walletPW provided, it was for the parent wallet, so
  2678  		// use nil here.
  2679  		if _, err := c.createWalletOrToken(crypter, nil, form); err != nil {
  2680  			c.log.Errorf("failed to create token wallet: %v", err)
  2681  			subject, details := c.formatDetails(TopicQueuedCreationFailed, unbip(token.ParentID), symbol)
  2682  			c.notify(newWalletCreationNote(TopicQueuedCreationFailed, subject, details, db.ErrorLevel, assetID))
  2683  		} else {
  2684  			c.notify(newWalletCreationNote(TopicQueuedCreationSuccess, "", "", db.Data, assetID))
  2685  		}
  2686  	}()
  2687  	creationQueued = true
  2688  	return nil
  2689  }
  2690  
  2691  func (c *Core) createWalletOrToken(crypter encrypt.Crypter, walletPW []byte, form *WalletForm) (wallet *xcWallet, err error) {
  2692  	assetID := form.AssetID
  2693  	symbol := unbip(assetID)
  2694  	token := asset.TokenInfo(assetID)
  2695  	var dbWallet *db.Wallet
  2696  	if token != nil {
  2697  		dbWallet, err = c.createTokenWallet(assetID, token, form)
  2698  	} else {
  2699  		dbWallet, err = c.createWallet(crypter, walletPW, assetID, form)
  2700  	}
  2701  	if err != nil {
  2702  		return nil, err
  2703  	}
  2704  
  2705  	wallet, err = c.loadWallet(dbWallet)
  2706  	if err != nil {
  2707  		return nil, fmt.Errorf("error loading wallet for %d -> %s: %w", assetID, symbol, err)
  2708  	}
  2709  	// Block PeersChange until we know this wallet is ready.
  2710  	atomic.StoreUint32(wallet.broadcasting, 0)
  2711  
  2712  	dbWallet.Address, err = c.connectWallet(wallet)
  2713  	if err != nil {
  2714  		return nil, err
  2715  	}
  2716  
  2717  	if c.cfg.UnlockCoinsOnLogin {
  2718  		if err = wallet.ReturnCoins(nil); err != nil {
  2719  			c.log.Errorf("Failed to unlock all %s wallet coins: %v", unbip(wallet.AssetID), err)
  2720  		}
  2721  	}
  2722  
  2723  	initErr := func(s string, a ...any) (*xcWallet, error) {
  2724  		_ = wallet.Lock(2 * time.Second) // just try, but don't confuse the user with an error
  2725  		wallet.Disconnect()
  2726  		return nil, fmt.Errorf(s, a...)
  2727  	}
  2728  
  2729  	err = c.unlockWallet(crypter, wallet) // no-op if !wallet.Wallet.Locked() && len(encPW) == 0
  2730  	if err != nil {
  2731  		wallet.Disconnect()
  2732  		return nil, fmt.Errorf("%s wallet authentication error: %w", symbol, err)
  2733  	}
  2734  
  2735  	balances, err := c.walletBalance(wallet)
  2736  	if err != nil {
  2737  		return initErr("error getting wallet balance for %s: %w", symbol, err)
  2738  	}
  2739  	wallet.setBalance(balances)         // update xcWallet's WalletBalance
  2740  	dbWallet.Balance = balances.Balance // store the db.Balance
  2741  
  2742  	// Store the wallet in the database.
  2743  	err = c.db.UpdateWallet(dbWallet)
  2744  	if err != nil {
  2745  		return initErr("error storing wallet credentials: %w", err)
  2746  	}
  2747  
  2748  	c.log.Infof("Created %s wallet. Balance available = %d / "+
  2749  		"locked = %d / locked in contracts = %d, Deposit address = %s",
  2750  		symbol, balances.Available, balances.Locked, balances.ContractLocked,
  2751  		dbWallet.Address)
  2752  
  2753  	// The wallet has been successfully created. Store it.
  2754  	c.updateWallet(assetID, wallet)
  2755  
  2756  	atomic.StoreUint32(wallet.broadcasting, 1)
  2757  	c.notify(newWalletStateNote(wallet.state()))
  2758  	c.walletCheckAndNotify(wallet)
  2759  
  2760  	return wallet, nil
  2761  }
  2762  
  2763  func (c *Core) createWallet(crypter encrypt.Crypter, walletPW []byte, assetID uint32, form *WalletForm) (*db.Wallet, error) {
  2764  	walletDef, err := asset.WalletDef(assetID, form.Type)
  2765  	if err != nil {
  2766  		return nil, newError(assetSupportErr, "asset.WalletDef error: %w", err)
  2767  	}
  2768  
  2769  	// Sometimes core will insert data into the Settings map to communicate
  2770  	// information back to the wallet, so it cannot be nil.
  2771  	if form.Config == nil {
  2772  		form.Config = make(map[string]string)
  2773  	}
  2774  
  2775  	// Remove unused key-values from parsed settings before saving to db.
  2776  	// Especially necessary if settings was parsed from a config file, b/c
  2777  	// config files usually define more key-values than we need.
  2778  	// Expected keys should be lowercase because config.Parse returns lowercase
  2779  	// keys.
  2780  	expectedKeys := make(map[string]bool, len(walletDef.ConfigOpts))
  2781  	for _, option := range walletDef.ConfigOpts {
  2782  		expectedKeys[strings.ToLower(option.Key)] = true
  2783  	}
  2784  	for key := range form.Config {
  2785  		if !expectedKeys[key] {
  2786  			delete(form.Config, key)
  2787  		}
  2788  	}
  2789  
  2790  	if walletDef.Seeded {
  2791  		if len(walletPW) > 0 {
  2792  			return nil, errors.New("external password incompatible with seeded wallet")
  2793  		}
  2794  		walletPW, err = c.createSeededWallet(assetID, crypter, form)
  2795  		if err != nil {
  2796  			return nil, err
  2797  		}
  2798  	}
  2799  
  2800  	var encPW []byte
  2801  	if len(walletPW) > 0 {
  2802  		encPW, err = crypter.Encrypt(walletPW)
  2803  		if err != nil {
  2804  			return nil, fmt.Errorf("wallet password encryption error: %w", err)
  2805  		}
  2806  	}
  2807  
  2808  	return &db.Wallet{
  2809  		Type:        walletDef.Type,
  2810  		AssetID:     assetID,
  2811  		Settings:    form.Config,
  2812  		EncryptedPW: encPW,
  2813  		// Balance and Address are set after connect.
  2814  	}, nil
  2815  }
  2816  
  2817  func (c *Core) createTokenWallet(tokenID uint32, token *asset.Token, form *WalletForm) (*db.Wallet, error) {
  2818  	wallet, found := c.wallet(token.ParentID)
  2819  	if !found {
  2820  		return nil, fmt.Errorf("no parent wallet %d for token %d (%s)", token.ParentID, tokenID, unbip(tokenID))
  2821  	}
  2822  
  2823  	tokenMaster, is := wallet.Wallet.(asset.TokenMaster)
  2824  	if !is {
  2825  		return nil, fmt.Errorf("parent wallet %s is not a TokenMaster", unbip(token.ParentID))
  2826  	}
  2827  
  2828  	// Sometimes core will insert data into the Settings map to communicate
  2829  	// information back to the wallet, so it cannot be nil.
  2830  	if form.Config == nil {
  2831  		form.Config = make(map[string]string)
  2832  	}
  2833  
  2834  	if err := tokenMaster.CreateTokenWallet(tokenID, form.Config); err != nil {
  2835  		return nil, fmt.Errorf("CreateTokenWallet error: %w", err)
  2836  	}
  2837  
  2838  	return &db.Wallet{
  2839  		Type:     form.Type,
  2840  		AssetID:  tokenID,
  2841  		Settings: form.Config,
  2842  		// EncryptedPW ignored because we assume throughout that token wallet
  2843  		// authorization is handled by the parent.
  2844  		// Balance and Address are set after connect.
  2845  	}, nil
  2846  }
  2847  
  2848  // createSeededWallet initializes a seeded wallet with an asset-specific seed
  2849  // and password derived deterministically from the app seed. The password is
  2850  // returned for encrypting and storing.
  2851  func (c *Core) createSeededWallet(assetID uint32, crypter encrypt.Crypter, form *WalletForm) ([]byte, error) {
  2852  	seed, pw, err := c.assetSeedAndPass(assetID, crypter)
  2853  	if err != nil {
  2854  		return nil, err
  2855  	}
  2856  	defer encode.ClearBytes(seed)
  2857  
  2858  	var bday uint64
  2859  	if creds := c.creds(); !creds.Birthday.IsZero() {
  2860  		bday = uint64(creds.Birthday.Unix())
  2861  	}
  2862  
  2863  	c.log.Infof("Initializing a %s wallet", unbip(assetID))
  2864  	if err = asset.CreateWallet(assetID, &asset.CreateWalletParams{
  2865  		Type:     form.Type,
  2866  		Seed:     seed,
  2867  		Pass:     pw,
  2868  		Birthday: bday,
  2869  		Settings: form.Config,
  2870  		DataDir:  c.assetDataDirectory(assetID),
  2871  		Net:      c.net,
  2872  		Logger:   c.log.SubLogger(unbip(assetID)),
  2873  	}); err != nil {
  2874  		return nil, fmt.Errorf("Error creating wallet: %w", err)
  2875  	}
  2876  
  2877  	return pw, nil
  2878  }
  2879  
  2880  func (c *Core) assetSeedAndPass(assetID uint32, crypter encrypt.Crypter) (seed, pass []byte, err error) {
  2881  	creds := c.creds()
  2882  	if creds == nil {
  2883  		return nil, nil, errors.New("no v2 credentials stored")
  2884  	}
  2885  
  2886  	if tkn := asset.TokenInfo(assetID); tkn != nil {
  2887  		return nil, nil, fmt.Errorf("%s is a token. assets seeds are for base chains onlyu. did you want %s",
  2888  			tkn.Name, asset.Asset(tkn.ParentID).Info.Name)
  2889  	}
  2890  
  2891  	appSeed, err := crypter.Decrypt(creds.EncSeed)
  2892  	if err != nil {
  2893  		return nil, nil, fmt.Errorf("app seed decryption error: %w", err)
  2894  	}
  2895  
  2896  	seed, pass = AssetSeedAndPass(assetID, appSeed)
  2897  	return seed, pass, nil
  2898  }
  2899  
  2900  // AssetSeedAndPass derives the wallet seed and password that would be used to
  2901  // create a native wallet for a particular asset and application seed. Depending
  2902  // on external wallet software and their key derivation paths, this seed may be
  2903  // usable for accessing funds outside of DEX applications, e.g. btcwallet.
  2904  func AssetSeedAndPass(assetID uint32, appSeed []byte) ([]byte, []byte) {
  2905  	const accountBasedSeedAssetID = 60 // ETH
  2906  	seedAssetID := assetID
  2907  	if ai, _ := asset.Info(assetID); ai != nil && ai.IsAccountBased {
  2908  		seedAssetID = accountBasedSeedAssetID
  2909  	}
  2910  	// Tokens asset IDs shouldn't be passed in, but if they are, return the seed
  2911  	// for the parent ID.
  2912  	if tkn := asset.TokenInfo(assetID); tkn != nil {
  2913  		if ai, _ := asset.Info(tkn.ParentID); ai != nil {
  2914  			if ai.IsAccountBased {
  2915  				seedAssetID = accountBasedSeedAssetID
  2916  			}
  2917  		}
  2918  	}
  2919  
  2920  	b := make([]byte, len(appSeed)+4)
  2921  	copy(b, appSeed)
  2922  	binary.BigEndian.PutUint32(b[len(appSeed):], seedAssetID)
  2923  	s := blake256.Sum256(b)
  2924  	p := blake256.Sum256(s[:])
  2925  	return s[:], p[:]
  2926  }
  2927  
  2928  // assetDataDirectory is a directory for a wallet to use for local storage.
  2929  func (c *Core) assetDataDirectory(assetID uint32) string {
  2930  	return filepath.Join(filepath.Dir(c.cfg.DBPath), "assetdb", unbip(assetID))
  2931  }
  2932  
  2933  // assetDataBackupDirectory is a directory for a wallet to use for backups of
  2934  // data. Wallet data is copied here instead of being deleted when recovering a
  2935  // wallet.
  2936  func (c *Core) assetDataBackupDirectory(assetID uint32) string {
  2937  	return filepath.Join(filepath.Dir(c.cfg.DBPath), "assetdb-backup", unbip(assetID))
  2938  }
  2939  
  2940  // loadWallet uses the data from the database to construct a new exchange
  2941  // wallet. The returned wallet is running but not connected.
  2942  func (c *Core) loadWallet(dbWallet *db.Wallet) (*xcWallet, error) {
  2943  	var parent *xcWallet
  2944  	assetID := dbWallet.AssetID
  2945  
  2946  	// Construct the unconnected xcWallet.
  2947  	contractLockedAmt, orderLockedAmt, bondLockedAmt := c.lockedAmounts(assetID)
  2948  	symbol := unbip(assetID)
  2949  	wallet := &xcWallet{ // captured by the PeersChange closure
  2950  		AssetID: assetID,
  2951  		Symbol:  symbol,
  2952  		log:     c.log.SubLogger(symbol),
  2953  		balance: &WalletBalance{
  2954  			Balance:        dbWallet.Balance,
  2955  			OrderLocked:    orderLockedAmt,
  2956  			ContractLocked: contractLockedAmt,
  2957  			BondLocked:     bondLockedAmt,
  2958  		},
  2959  		encPass:      dbWallet.EncryptedPW,
  2960  		address:      dbWallet.Address,
  2961  		peerCount:    -1, // no count yet
  2962  		dbID:         dbWallet.ID(),
  2963  		walletType:   dbWallet.Type,
  2964  		broadcasting: new(uint32),
  2965  		disabled:     dbWallet.Disabled,
  2966  		syncStatus:   &asset.SyncStatus{},
  2967  	}
  2968  
  2969  	token := asset.TokenInfo(assetID)
  2970  
  2971  	peersChange := func(numPeers uint32, err error) {
  2972  		if c.ctx.Err() != nil {
  2973  			return
  2974  		}
  2975  
  2976  		c.wg.Add(1)
  2977  		go func() {
  2978  			defer c.wg.Done()
  2979  			c.peerChange(wallet, numPeers, err)
  2980  		}()
  2981  	}
  2982  
  2983  	// Ensure default settings are always supplied to the wallet as they
  2984  	// may not be saved yet.
  2985  	walletDef, err := asset.WalletDef(assetID, dbWallet.Type)
  2986  	if err != nil {
  2987  		return nil, newError(assetSupportErr, "asset.WalletDef error: %w", err)
  2988  	}
  2989  	defaultValues := make(map[string]string, len(walletDef.ConfigOpts))
  2990  	for _, option := range walletDef.ConfigOpts {
  2991  		defaultValues[strings.ToLower(option.Key)] = option.DefaultValue
  2992  	}
  2993  	settings := dbWallet.Settings
  2994  	for k, v := range defaultValues {
  2995  		if _, has := settings[k]; !has {
  2996  			settings[k] = v
  2997  		}
  2998  	}
  2999  
  3000  	log := c.log.SubLogger(unbip(assetID))
  3001  	var w asset.Wallet
  3002  	if token == nil {
  3003  		walletCfg := &asset.WalletConfig{
  3004  			Type:        dbWallet.Type,
  3005  			Settings:    settings,
  3006  			Emit:        asset.NewWalletEmitter(c.notes, assetID, log),
  3007  			PeersChange: peersChange,
  3008  			DataDir:     c.assetDataDirectory(assetID),
  3009  		}
  3010  
  3011  		settings[asset.SpecialSettingActivelyUsed] =
  3012  			strconv.FormatBool(c.assetHasActiveOrders(dbWallet.AssetID))
  3013  		defer delete(settings, asset.SpecialSettingActivelyUsed)
  3014  
  3015  		w, err = asset.OpenWallet(assetID, walletCfg, log, c.net)
  3016  	} else {
  3017  		var found bool
  3018  		parent, found = c.wallet(token.ParentID)
  3019  		if !found {
  3020  			return nil, fmt.Errorf("cannot load %s wallet before %s wallet", unbip(assetID), unbip(token.ParentID))
  3021  		}
  3022  
  3023  		tokenMaster, is := parent.Wallet.(asset.TokenMaster)
  3024  		if !is {
  3025  			return nil, fmt.Errorf("%s token's %s parent wallet is not a TokenMaster", unbip(assetID), unbip(token.ParentID))
  3026  		}
  3027  
  3028  		w, err = tokenMaster.OpenTokenWallet(&asset.TokenConfig{
  3029  			AssetID:     assetID,
  3030  			Settings:    settings,
  3031  			Emit:        asset.NewWalletEmitter(c.notes, assetID, log),
  3032  			PeersChange: peersChange,
  3033  		})
  3034  	}
  3035  	if err != nil {
  3036  		if errors.Is(err, asset.ErrWalletTypeDisabled) {
  3037  			subject, details := c.formatDetails(TopicWalletTypeDeprecated, unbip(assetID))
  3038  			c.notify(newWalletConfigNote(TopicWalletTypeDeprecated, subject, details, db.WarningLevel, nil))
  3039  		}
  3040  		return nil, fmt.Errorf("error opening wallet: %w", err)
  3041  	}
  3042  
  3043  	wallet.Wallet = w
  3044  	wallet.parent = parent
  3045  	wallet.supportedVersions = w.Info().SupportedVersions
  3046  	wallet.connector = dex.NewConnectionMaster(w)
  3047  	wallet.traits = asset.DetermineWalletTraits(w)
  3048  	atomic.StoreUint32(wallet.broadcasting, 1)
  3049  	return wallet, nil
  3050  }
  3051  
  3052  // WalletState returns the *WalletState for the asset ID.
  3053  func (c *Core) WalletState(assetID uint32) *WalletState {
  3054  	c.walletMtx.Lock()
  3055  	defer c.walletMtx.Unlock()
  3056  	wallet, has := c.wallets[assetID]
  3057  	if !has {
  3058  		c.log.Tracef("wallet status requested for unknown asset %d -> %s", assetID, unbip(assetID))
  3059  		return nil
  3060  	}
  3061  	return wallet.state()
  3062  }
  3063  
  3064  // WalletTraits gets the traits for the wallet.
  3065  func (c *Core) WalletTraits(assetID uint32) (asset.WalletTrait, error) {
  3066  	w, found := c.wallet(assetID)
  3067  	if !found {
  3068  		return 0, fmt.Errorf("no %d wallet found", assetID)
  3069  	}
  3070  	return w.traits, nil
  3071  }
  3072  
  3073  // assetHasActiveOrders checks whether there are any active orders or
  3074  // negotiating matches for the specified asset.
  3075  func (c *Core) assetHasActiveOrders(assetID uint32) bool {
  3076  	for _, dc := range c.dexConnections() {
  3077  		if dc.hasActiveAssetOrders(assetID) {
  3078  			return true
  3079  		}
  3080  	}
  3081  	return false
  3082  }
  3083  
  3084  // walletIsActive combines assetHasActiveOrders with a check for pending
  3085  // registration fee payments and pending bonds.
  3086  func (c *Core) walletIsActive(assetID uint32) bool {
  3087  	if c.assetHasActiveOrders(assetID) {
  3088  		return true
  3089  	}
  3090  	for _, dc := range c.dexConnections() {
  3091  		dc.acct.authMtx.RLock()
  3092  		for _, pb := range dc.pendingBonds() {
  3093  			if pb.AssetID == assetID {
  3094  				dc.acct.authMtx.RUnlock()
  3095  				return true
  3096  			}
  3097  		}
  3098  		dc.acct.authMtx.RUnlock()
  3099  	}
  3100  	return false
  3101  }
  3102  
  3103  func (dc *dexConnection) bondOpts() (assetID uint32, targetTier, max uint64) {
  3104  	dc.acct.authMtx.RLock()
  3105  	defer dc.acct.authMtx.RUnlock()
  3106  	return dc.acct.bondAsset, dc.acct.targetTier, dc.acct.maxBondedAmt
  3107  }
  3108  
  3109  func (dc *dexConnection) bondTotalInternal(assetID uint32) (total, active uint64) {
  3110  	sum := func(bonds []*db.Bond) (amt uint64) {
  3111  		for _, b := range bonds {
  3112  			if assetID == b.AssetID {
  3113  				amt += b.Amount
  3114  			}
  3115  		}
  3116  		return
  3117  	}
  3118  	active = sum(dc.acct.bonds)
  3119  	return active + sum(dc.acct.pendingBonds) + sum(dc.acct.expiredBonds), active
  3120  }
  3121  
  3122  func (dc *dexConnection) bondTotal(assetID uint32) (total, active uint64) {
  3123  	dc.acct.authMtx.RLock()
  3124  	defer dc.acct.authMtx.RUnlock()
  3125  	return dc.bondTotalInternal(assetID)
  3126  }
  3127  
  3128  func (dc *dexConnection) hasUnspentAssetBond(assetID uint32) bool {
  3129  	total, _ := dc.bondTotal(assetID)
  3130  	return total > 0
  3131  }
  3132  
  3133  func (dc *dexConnection) hasUnspentBond() bool {
  3134  	dc.acct.authMtx.RLock()
  3135  	defer dc.acct.authMtx.RUnlock()
  3136  	return len(dc.acct.bonds) > 0 || len(dc.acct.pendingBonds) > 0 || len(dc.acct.expiredBonds) > 0
  3137  }
  3138  
  3139  // isActiveBondAsset indicates if a wallet (or it's parent if the asset is a
  3140  // token, or it's children if it's a base asset) is needed for bonding on any
  3141  // configured DEX. includeLive should be set to consider all existing unspent
  3142  // bonds that need to be refunded in the future (only requires a broadcast, no
  3143  // wallet signing ability).
  3144  func (c *Core) isActiveBondAsset(assetID uint32, includeLive bool) bool {
  3145  	// Consider this asset and any child tokens if it is a base asset, or just
  3146  	// the parent asset if it's a token.
  3147  	assetIDs := map[uint32]bool{
  3148  		assetID: true,
  3149  	}
  3150  	if ra := asset.Asset(assetID); ra != nil { // it's a base asset, all tokens need it
  3151  		for tknAssetID := range ra.Tokens {
  3152  			assetIDs[tknAssetID] = true
  3153  		}
  3154  	} else { // it's a token and we only care about the parent, not sibling tokens
  3155  		if tkn := asset.TokenInfo(assetID); tkn != nil { // it should be
  3156  			assetIDs[tkn.ParentID] = true
  3157  		}
  3158  	}
  3159  
  3160  	for _, dc := range c.dexConnections() {
  3161  		bondAsset, targetTier, _ := dc.bondOpts()
  3162  		if targetTier > 0 && assetIDs[bondAsset] {
  3163  			return true
  3164  		}
  3165  		if includeLive {
  3166  			for id := range assetIDs {
  3167  				if dc.hasUnspentAssetBond(id) {
  3168  					return true
  3169  				}
  3170  			}
  3171  		}
  3172  	}
  3173  	return false
  3174  }
  3175  
  3176  // walletCheckAndNotify sets the xcWallet's synced and syncProgress fields from
  3177  // the wallet's SyncStatus result, emits a WalletStateNote, and returns the
  3178  // synced value. When synced is true, this also updates the wallet's balance,
  3179  // stores the balance in the DB, emits a BalanceNote, and updates the bond
  3180  // reserves (with balance checking).
  3181  func (c *Core) walletCheckAndNotify(w *xcWallet) bool {
  3182  	ss, err := w.SyncStatus()
  3183  	if err != nil {
  3184  		c.log.Errorf("Unable to get wallet/node sync status for %s: %v",
  3185  			unbip(w.AssetID), err)
  3186  		return false
  3187  	}
  3188  
  3189  	w.mtx.Lock()
  3190  	wasSynced := w.syncStatus.Synced
  3191  	w.syncStatus = ss
  3192  	w.mtx.Unlock()
  3193  
  3194  	if atomic.LoadUint32(w.broadcasting) == 1 {
  3195  		c.notify(newWalletSyncNote(w.AssetID, ss))
  3196  	}
  3197  	if ss.Synced && !wasSynced {
  3198  		c.updateWalletBalance(w)
  3199  		c.log.Debugf("Wallet synced for asset %s", unbip(w.AssetID))
  3200  		c.updateBondReserves(w.AssetID)
  3201  	}
  3202  	return ss.Synced
  3203  }
  3204  
  3205  // startWalletSyncMonitor repeatedly calls walletCheckAndNotify on a ticker
  3206  // until it is synced. This launches the monitor goroutine, if not already
  3207  // running, and immediately returns.
  3208  func (c *Core) startWalletSyncMonitor(wallet *xcWallet) {
  3209  	// Prevent multiple sync monitors for this wallet.
  3210  	if !atomic.CompareAndSwapUint32(&wallet.monitored, 0, 1) {
  3211  		return // already monitoring
  3212  	}
  3213  
  3214  	c.wg.Add(1)
  3215  	go func() {
  3216  		defer c.wg.Done()
  3217  		defer atomic.StoreUint32(&wallet.monitored, 0)
  3218  		ticker := time.NewTicker(syncTickerPeriod)
  3219  		defer ticker.Stop()
  3220  		for {
  3221  			select {
  3222  			case <-ticker.C:
  3223  				if c.walletCheckAndNotify(wallet) {
  3224  					return
  3225  				}
  3226  			case <-wallet.connector.Done():
  3227  				c.log.Warnf("%v wallet shut down before sync completed.", wallet.Info().Name)
  3228  				return
  3229  			case <-c.ctx.Done():
  3230  				return
  3231  			}
  3232  		}
  3233  	}()
  3234  }
  3235  
  3236  // RescanWallet will issue a Rescan command to the wallet if supported by the
  3237  // wallet implementation. It is up to the underlying wallet backend if and how
  3238  // to implement this functionality. It may be asynchronous. Core will emit
  3239  // wallet state notifications until the rescan is complete. If force is false,
  3240  // this will check for active orders involving this asset before initiating a
  3241  // rescan. WARNING: It is ill-advised to initiate a wallet rescan with active
  3242  // orders unless as a last ditch effort to get the wallet to recognize a
  3243  // transaction needed to complete a swap.
  3244  func (c *Core) RescanWallet(assetID uint32, force bool) error {
  3245  	if !force && c.walletIsActive(assetID) {
  3246  		return newError(activeOrdersErr, "active orders or registration fee payments for %v", unbip(assetID))
  3247  	}
  3248  
  3249  	wallet, err := c.connectedWallet(assetID)
  3250  	if err != nil {
  3251  		return fmt.Errorf("OpenWallet: wallet not found for %d -> %s: %w",
  3252  			assetID, unbip(assetID), err)
  3253  	}
  3254  
  3255  	walletDef, err := asset.WalletDef(assetID, wallet.walletType)
  3256  	if err != nil {
  3257  		return newError(assetSupportErr, "asset.WalletDef error: %w", err)
  3258  	}
  3259  
  3260  	var bday uint64 // unix time seconds
  3261  	if walletDef.Seeded {
  3262  		creds := c.creds()
  3263  		if !creds.Birthday.IsZero() {
  3264  			bday = uint64(creds.Birthday.Unix())
  3265  		}
  3266  	}
  3267  
  3268  	// Begin potentially asynchronous wallet rescan operation.
  3269  	if err = wallet.rescan(c.ctx, bday); err != nil {
  3270  		return err
  3271  	}
  3272  
  3273  	if c.walletCheckAndNotify(wallet) {
  3274  		return nil // sync done, Rescan may have by synchronous or a no-op
  3275  	}
  3276  
  3277  	// Synchronization still running. Launch a status update goroutine.
  3278  	c.startWalletSyncMonitor(wallet)
  3279  
  3280  	return nil
  3281  }
  3282  
  3283  func (c *Core) removeWallet(assetID uint32) {
  3284  	c.walletMtx.Lock()
  3285  	defer c.walletMtx.Unlock()
  3286  	delete(c.wallets, assetID)
  3287  }
  3288  
  3289  // updateWallet stores or updates an asset's wallet.
  3290  func (c *Core) updateWallet(assetID uint32, wallet *xcWallet) {
  3291  	c.walletMtx.Lock()
  3292  	defer c.walletMtx.Unlock()
  3293  	c.wallets[assetID] = wallet
  3294  }
  3295  
  3296  // RecoverWallet will retrieve some recovery information from the wallet,
  3297  // which may not be possible if the wallet is too corrupted. Disconnect and
  3298  // destroy the old wallet, create a new one, and if the recovery information
  3299  // was retrieved from the old wallet, send this information to the new one.
  3300  // If force is false, this will check for active orders involving this
  3301  // asset before initiating a rescan. WARNING: It is ill-advised to initiate
  3302  // a wallet recovery with active orders unless the wallet db is definitely
  3303  // corrupted and even a rescan will not save it.
  3304  //
  3305  // DO NOT MAKE CONCURRENT CALLS TO THIS FUNCTION WITH THE SAME ASSET.
  3306  func (c *Core) RecoverWallet(assetID uint32, appPW []byte, force bool) error {
  3307  	crypter, err := c.encryptionKey(appPW)
  3308  	if err != nil {
  3309  		return newError(authErr, "RecoverWallet password error: %w", err)
  3310  	}
  3311  	defer crypter.Close()
  3312  
  3313  	if !force {
  3314  		for _, dc := range c.dexConnections() {
  3315  			if dc.hasActiveAssetOrders(assetID) {
  3316  				return newError(activeOrdersErr, "active orders for %v", unbip(assetID))
  3317  			}
  3318  		}
  3319  	}
  3320  
  3321  	oldWallet, found := c.wallet(assetID)
  3322  	if !found {
  3323  		return fmt.Errorf("RecoverWallet: wallet not found for %d -> %s: %w",
  3324  			assetID, unbip(assetID), err)
  3325  	}
  3326  
  3327  	recoverer, isRecoverer := oldWallet.Wallet.(asset.Recoverer)
  3328  	if !isRecoverer {
  3329  		return errors.New("wallet is not a recoverer")
  3330  	}
  3331  	walletDef, err := asset.WalletDef(assetID, oldWallet.walletType)
  3332  	if err != nil {
  3333  		return newError(assetSupportErr, "asset.WalletDef error: %w", err)
  3334  	}
  3335  	// Unseeded wallets shouldn't implement the Recoverer interface. This
  3336  	// is just an additional check for safety.
  3337  	if !walletDef.Seeded {
  3338  		return fmt.Errorf("can only recover a seeded wallet")
  3339  	}
  3340  
  3341  	dbWallet, err := c.db.Wallet(oldWallet.dbID)
  3342  	if err != nil {
  3343  		return fmt.Errorf("error retrieving DB wallet: %w", err)
  3344  	}
  3345  
  3346  	seed, pw, err := c.assetSeedAndPass(assetID, crypter)
  3347  	if err != nil {
  3348  		return err
  3349  	}
  3350  	defer encode.ClearBytes(seed)
  3351  	defer encode.ClearBytes(pw)
  3352  
  3353  	if oldWallet.connected() {
  3354  		if recoveryCfg, err := recoverer.GetRecoveryCfg(); err != nil {
  3355  			c.log.Errorf("RecoverWallet: unable to get recovery config: %v", err)
  3356  		} else {
  3357  			// merge recoveryCfg with dbWallet.Settings
  3358  			for key, val := range recoveryCfg {
  3359  				dbWallet.Settings[key] = val
  3360  			}
  3361  		}
  3362  		oldWallet.Disconnect() // wallet now shut down and w.hookedUp == false -> connected() returns false
  3363  	}
  3364  	// Before we pull the plug, remove the wallet from wallets map. Otherwise,
  3365  	// connectedWallet would try to connect it.
  3366  	c.removeWallet(assetID)
  3367  
  3368  	if err = recoverer.Move(c.assetDataBackupDirectory(assetID)); err != nil {
  3369  		return fmt.Errorf("failed to move wallet data to backup folder: %w", err)
  3370  	}
  3371  
  3372  	if err = asset.CreateWallet(assetID, &asset.CreateWalletParams{
  3373  		Type:     dbWallet.Type,
  3374  		Seed:     seed,
  3375  		Pass:     pw,
  3376  		Settings: dbWallet.Settings,
  3377  		DataDir:  c.assetDataDirectory(assetID),
  3378  		Net:      c.net,
  3379  		Logger:   c.log.SubLogger(unbip(assetID)),
  3380  	}); err != nil {
  3381  		return fmt.Errorf("error creating wallet: %w", err)
  3382  	}
  3383  
  3384  	newWallet, err := c.loadWallet(dbWallet)
  3385  	if err != nil {
  3386  		return newError(walletErr, "error loading wallet for %d -> %s: %w",
  3387  			assetID, unbip(assetID), err)
  3388  	}
  3389  
  3390  	// Ensure we are not trying to connect to a disabled wallet.
  3391  	if newWallet.isDisabled() {
  3392  		c.updateWallet(assetID, newWallet)
  3393  	} else {
  3394  		_, err = c.connectWallet(newWallet)
  3395  		if err != nil {
  3396  			return err
  3397  		}
  3398  		c.updateWalletBalance(newWallet)
  3399  
  3400  		c.updateAssetWalletRefs(newWallet)
  3401  
  3402  		err = c.unlockWallet(crypter, newWallet)
  3403  		if err != nil {
  3404  			return err
  3405  		}
  3406  	}
  3407  
  3408  	c.notify(newWalletStateNote(newWallet.state()))
  3409  
  3410  	return nil
  3411  }
  3412  
  3413  // OpenWallet opens (unlocks) the wallet for use.
  3414  func (c *Core) OpenWallet(assetID uint32, appPW []byte) error {
  3415  	crypter, err := c.encryptionKey(appPW)
  3416  	if err != nil {
  3417  		return err
  3418  	}
  3419  	defer crypter.Close()
  3420  	wallet, err := c.connectedWallet(assetID)
  3421  	if err != nil {
  3422  		return fmt.Errorf("OpenWallet: wallet not found for %d -> %s: %w", assetID, unbip(assetID), err)
  3423  	}
  3424  	err = c.unlockWallet(crypter, wallet)
  3425  	if err != nil {
  3426  		return newError(walletAuthErr, "failed to unlock %s wallet: %w", unbip(assetID), err)
  3427  	}
  3428  
  3429  	state := wallet.state()
  3430  	balances, err := c.updateWalletBalance(wallet)
  3431  	if err != nil {
  3432  		return err
  3433  	}
  3434  	c.log.Infof("Connected to and unlocked %s wallet. Balance available "+
  3435  		"= %d / locked = %d / locked in contracts = %d, locked in bonds = %d, Deposit address = %s",
  3436  		state.Symbol, balances.Available, balances.Locked, balances.ContractLocked,
  3437  		balances.BondLocked, state.Address)
  3438  
  3439  	c.notify(newWalletStateNote(state))
  3440  	return nil
  3441  }
  3442  
  3443  // CloseWallet closes the wallet for the specified asset. The wallet cannot be
  3444  // closed if there are active negotiations for the asset.
  3445  func (c *Core) CloseWallet(assetID uint32) error {
  3446  	if c.isActiveBondAsset(assetID, false) { // unlock not needed for refunds
  3447  		return fmt.Errorf("%s wallet must remain unlocked for bonding", unbip(assetID))
  3448  	}
  3449  	if c.assetHasActiveOrders(assetID) {
  3450  		return fmt.Errorf("cannot lock %s wallet with active swap negotiations", unbip(assetID))
  3451  	}
  3452  	wallet, err := c.connectedWallet(assetID)
  3453  	if err != nil {
  3454  		return fmt.Errorf("wallet not found for %d -> %s: %w", assetID, unbip(assetID), err)
  3455  	}
  3456  	err = wallet.Lock(walletLockTimeout)
  3457  	if err != nil {
  3458  		return err
  3459  	}
  3460  
  3461  	c.notify(newWalletStateNote(wallet.state()))
  3462  
  3463  	return nil
  3464  }
  3465  
  3466  // ConnectWallet connects to the wallet without unlocking.
  3467  func (c *Core) ConnectWallet(assetID uint32) error {
  3468  	wallet, err := c.connectedWallet(assetID)
  3469  	if err != nil {
  3470  		return err
  3471  	}
  3472  	c.notify(newWalletStateNote(wallet.state()))
  3473  	return nil
  3474  }
  3475  
  3476  // WalletSettings fetches the current wallet configuration details from the
  3477  // database.
  3478  func (c *Core) WalletSettings(assetID uint32) (map[string]string, error) {
  3479  	wallet, found := c.wallet(assetID)
  3480  	if !found {
  3481  		return nil, newError(missingWalletErr, "%d -> %s wallet not found", assetID, unbip(assetID))
  3482  	}
  3483  	// Get the settings from the database.
  3484  	dbWallet, err := c.db.Wallet(wallet.dbID)
  3485  	if err != nil {
  3486  		return nil, codedError(dbErr, err)
  3487  	}
  3488  	return dbWallet.Settings, nil
  3489  }
  3490  
  3491  // ChangeAppPass updates the application password to the provided new password
  3492  // after validating the current password.
  3493  func (c *Core) ChangeAppPass(appPW, newAppPW []byte) error {
  3494  	// Validate current password.
  3495  	if len(newAppPW) == 0 {
  3496  		return fmt.Errorf("application password cannot be empty")
  3497  	}
  3498  	creds := c.creds()
  3499  	if creds == nil {
  3500  		return fmt.Errorf("no primary credentials. Is the client initialized?")
  3501  	}
  3502  
  3503  	outerCrypter, err := c.reCrypter(appPW, creds.OuterKeyParams)
  3504  	if err != nil {
  3505  		return newError(authErr, "old password error: %w", err)
  3506  	}
  3507  	defer outerCrypter.Close()
  3508  	innerKey, err := outerCrypter.Decrypt(creds.EncInnerKey)
  3509  	if err != nil {
  3510  		return fmt.Errorf("inner key decryption error: %w", err)
  3511  	}
  3512  
  3513  	return c.changeAppPass(newAppPW, innerKey, creds)
  3514  }
  3515  
  3516  // changeAppPass is a shared method to reset or change user password.
  3517  func (c *Core) changeAppPass(newAppPW, innerKey []byte, creds *db.PrimaryCredentials) error {
  3518  	newOuterCrypter := c.newCrypter(newAppPW)
  3519  	defer newOuterCrypter.Close()
  3520  	newEncInnerKey, err := newOuterCrypter.Encrypt(innerKey)
  3521  	if err != nil {
  3522  		return fmt.Errorf("encryption error: %v", err)
  3523  	}
  3524  
  3525  	newCreds := &db.PrimaryCredentials{
  3526  		EncSeed:        creds.EncSeed,
  3527  		EncInnerKey:    newEncInnerKey,
  3528  		InnerKeyParams: creds.InnerKeyParams,
  3529  		Birthday:       creds.Birthday,
  3530  		OuterKeyParams: newOuterCrypter.Serialize(),
  3531  		Version:        creds.Version,
  3532  	}
  3533  
  3534  	err = c.db.SetPrimaryCredentials(newCreds)
  3535  	if err != nil {
  3536  		return fmt.Errorf("SetPrimaryCredentials error: %w", err)
  3537  	}
  3538  
  3539  	c.setCredentials(newCreds)
  3540  
  3541  	return nil
  3542  }
  3543  
  3544  // ResetAppPass resets the application password to the provided new password.
  3545  func (c *Core) ResetAppPass(newPass []byte, seedStr string) (err error) {
  3546  	if !c.IsInitialized() {
  3547  		return fmt.Errorf("cannot reset password before client is initialized")
  3548  	}
  3549  
  3550  	if len(newPass) == 0 {
  3551  		return fmt.Errorf("application password cannot be empty")
  3552  	}
  3553  
  3554  	seed, _, err := decodeSeedString(seedStr)
  3555  	if err != nil {
  3556  		return fmt.Errorf("error decoding seed: %w", err)
  3557  	}
  3558  
  3559  	creds := c.creds()
  3560  	if creds == nil {
  3561  		return fmt.Errorf("no credentials stored")
  3562  	}
  3563  
  3564  	innerKey := seedInnerKey(seed)
  3565  	_, err = c.reCrypter(innerKey[:], creds.InnerKeyParams)
  3566  	if err != nil {
  3567  		c.log.Errorf("Error reseting password with seed: %v", err)
  3568  		return errors.New("incorrect seed")
  3569  	}
  3570  
  3571  	return c.changeAppPass(newPass, innerKey[:], creds)
  3572  }
  3573  
  3574  // ReconfigureWallet updates the wallet configuration settings, it also updates
  3575  // the password if newWalletPW is non-nil. Do not make concurrent calls to
  3576  // ReconfigureWallet for the same asset.
  3577  func (c *Core) ReconfigureWallet(appPW, newWalletPW []byte, form *WalletForm) error {
  3578  	crypter, err := c.encryptionKey(appPW)
  3579  	if err != nil {
  3580  		return newError(authErr, "ReconfigureWallet password error: %w", err)
  3581  	}
  3582  	defer crypter.Close()
  3583  
  3584  	assetID := form.AssetID
  3585  
  3586  	walletDef, err := asset.WalletDef(assetID, form.Type)
  3587  	if err != nil {
  3588  		return newError(assetSupportErr, "asset.WalletDef error: %w", err)
  3589  	}
  3590  	if walletDef.Seeded && newWalletPW != nil {
  3591  		return newError(passwordErr, "cannot set a password on a built-in(seeded) wallet")
  3592  	}
  3593  
  3594  	oldWallet, found := c.wallet(assetID)
  3595  	if !found {
  3596  		return newError(missingWalletErr, "%d -> %s wallet not found",
  3597  			assetID, unbip(assetID))
  3598  	}
  3599  
  3600  	if oldWallet.isDisabled() { // disabled wallet cannot perform operation.
  3601  		return fmt.Errorf(walletDisabledErrStr, strings.ToUpper(unbip(assetID)))
  3602  	}
  3603  
  3604  	oldDef, err := asset.WalletDef(assetID, oldWallet.walletType)
  3605  	if err != nil {
  3606  		return newError(assetSupportErr, "old wallet asset.WalletDef error: %w", err)
  3607  	}
  3608  	oldDepositAddr := oldWallet.currentDepositAddress()
  3609  
  3610  	dbWallet := &db.Wallet{
  3611  		Type:        form.Type,
  3612  		AssetID:     oldWallet.AssetID,
  3613  		Settings:    form.Config,
  3614  		Balance:     &db.Balance{}, // in case retrieving new balance after connect fails
  3615  		EncryptedPW: oldWallet.encPW(),
  3616  		Address:     oldDepositAddr,
  3617  	}
  3618  
  3619  	storeWithBalance := func(w *xcWallet, dbWallet *db.Wallet) error {
  3620  		balances, err := c.walletBalance(w)
  3621  		if err != nil {
  3622  			c.log.Warnf("Error getting balance for wallet %s: %v", unbip(assetID), err)
  3623  			// Do not fail in case this requires an unlocked wallet.
  3624  		} else {
  3625  			w.setBalance(balances)              // update xcWallet's WalletBalance
  3626  			dbWallet.Balance = balances.Balance // store the db.Balance
  3627  		}
  3628  
  3629  		err = c.db.UpdateWallet(dbWallet)
  3630  		if err != nil {
  3631  			return newError(dbErr, "error saving wallet configuration: %w", err)
  3632  		}
  3633  
  3634  		c.notify(newBalanceNote(assetID, balances)) // redundant with wallet config note?
  3635  		subject, details := c.formatDetails(TopicWalletConfigurationUpdated, unbip(assetID), w.address)
  3636  		c.notify(newWalletConfigNote(TopicWalletConfigurationUpdated, subject, details, db.Success, w.state()))
  3637  
  3638  		return nil
  3639  	}
  3640  
  3641  	clearTickGovernors := func() {
  3642  		for _, dc := range c.dexConnections() {
  3643  			for _, t := range dc.trackedTrades() {
  3644  				if t.Base() != assetID && t.Quote() != assetID {
  3645  					continue
  3646  				}
  3647  				isFromAsset := t.wallets.fromWallet.AssetID == assetID
  3648  				t.mtx.RLock()
  3649  				for _, m := range t.matches { // maybe range t.activeMatches()
  3650  					m.exceptionMtx.Lock()
  3651  					if m.tickGovernor != nil &&
  3652  						((m.suspectSwap && isFromAsset) || (m.suspectRedeem && !isFromAsset)) {
  3653  
  3654  						m.tickGovernor.Stop()
  3655  						m.tickGovernor = nil
  3656  					}
  3657  					m.exceptionMtx.Unlock()
  3658  				}
  3659  				t.mtx.RUnlock()
  3660  			}
  3661  		}
  3662  	}
  3663  
  3664  	// See if the wallet offers a quick path.
  3665  	if configurer, is := oldWallet.Wallet.(asset.LiveReconfigurer); is && oldWallet.walletType == walletDef.Type && oldWallet.connected() {
  3666  		form.Config[asset.SpecialSettingActivelyUsed] = strconv.FormatBool(c.assetHasActiveOrders(dbWallet.AssetID))
  3667  		defer delete(form.Config, asset.SpecialSettingActivelyUsed)
  3668  
  3669  		if restart, err := configurer.Reconfigure(c.ctx, &asset.WalletConfig{
  3670  			Type:     form.Type,
  3671  			Settings: form.Config,
  3672  			DataDir:  c.assetDataDirectory(assetID),
  3673  		}, oldWallet.currentDepositAddress()); err != nil {
  3674  			return fmt.Errorf("Reconfigure: %v", err)
  3675  		} else if !restart {
  3676  			// Config was updated without a need to restart.
  3677  			if owns, err := oldWallet.OwnsDepositAddress(oldWallet.currentDepositAddress()); err != nil {
  3678  				return newError(walletErr, "error checking deposit address after live config update: %w", err)
  3679  			} else if !owns {
  3680  				if dbWallet.Address, err = oldWallet.refreshDepositAddress(); err != nil {
  3681  					return newError(newAddrErr, "error refreshing deposit address after live config update: %w", err)
  3682  				}
  3683  			}
  3684  			if !oldDef.Seeded && newWalletPW != nil {
  3685  				if err = c.setWalletPassword(oldWallet, newWalletPW, crypter); err != nil {
  3686  					return newError(walletAuthErr, "failed to update password: %v", err)
  3687  				}
  3688  				dbWallet.EncryptedPW = oldWallet.encPW()
  3689  
  3690  			}
  3691  			if err = storeWithBalance(oldWallet, dbWallet); err != nil {
  3692  				return err
  3693  			}
  3694  			clearTickGovernors()
  3695  			c.log.Infof("%s wallet configuration updated without a restart 👍", unbip(assetID))
  3696  			return nil
  3697  		}
  3698  	}
  3699  
  3700  	c.log.Infof("%s wallet configuration update will require a restart", unbip(assetID))
  3701  
  3702  	var restartOnFail bool
  3703  
  3704  	defer func() {
  3705  		if restartOnFail {
  3706  			if _, err := c.connectWallet(oldWallet); err != nil {
  3707  				c.log.Errorf("Failed to reconnect wallet after a failed reconfiguration attempt: %v", err)
  3708  			}
  3709  		}
  3710  	}()
  3711  
  3712  	if walletDef.Seeded {
  3713  		exists, err := asset.WalletExists(assetID, form.Type, c.assetDataDirectory(assetID), form.Config, c.net)
  3714  		if err != nil {
  3715  			return newError(existenceCheckErr, "error checking wallet pre-existence: %w", err)
  3716  		}
  3717  
  3718  		// The password on a seeded wallet is deterministic, based on the seed
  3719  		// itself, so if the seeded wallet of this Type for this asset already
  3720  		// exists, recompute the password from the app seed.
  3721  		var pw []byte
  3722  		if exists {
  3723  			_, pw, err = c.assetSeedAndPass(assetID, crypter)
  3724  			if err != nil {
  3725  				return newError(authErr, "error retrieving wallet password: %w", err)
  3726  			}
  3727  		} else {
  3728  			pw, err = c.createSeededWallet(assetID, crypter, form)
  3729  			if err != nil {
  3730  				return newError(createWalletErr, "error creating new %q-type %s wallet: %w", form.Type, unbip(assetID), err)
  3731  			}
  3732  		}
  3733  		dbWallet.EncryptedPW, err = crypter.Encrypt(pw)
  3734  		if err != nil {
  3735  			return fmt.Errorf("wallet password encryption error: %w", err)
  3736  		}
  3737  
  3738  		if oldDef.Seeded && oldWallet.connected() {
  3739  			oldWallet.Disconnect()
  3740  			restartOnFail = true
  3741  		}
  3742  	} else if newWalletPW == nil && oldDef.Seeded {
  3743  		// If we're switching from a seeded wallet to a non-seeded wallet and no
  3744  		// password was provided, use empty string = wallet not encrypted.
  3745  		newWalletPW = []byte{}
  3746  	}
  3747  
  3748  	// Reload the wallet with the new settings.
  3749  	wallet, err := c.loadWallet(dbWallet)
  3750  	if err != nil {
  3751  		return newError(walletErr, "error loading wallet for %d -> %s: %w",
  3752  			assetID, unbip(assetID), err)
  3753  	}
  3754  
  3755  	// Block PeersChange until we know this wallet is ready.
  3756  	atomic.StoreUint32(wallet.broadcasting, 0)
  3757  	var success bool
  3758  	defer func() {
  3759  		if success {
  3760  			atomic.StoreUint32(wallet.broadcasting, 1)
  3761  			c.notify(newWalletStateNote(wallet.state()))
  3762  			c.walletCheckAndNotify(wallet)
  3763  		}
  3764  	}()
  3765  
  3766  	// Helper funciton to make sure trades can be settled by the
  3767  	// keys held within the new wallet.
  3768  	sameWallet := func() error {
  3769  		if c.walletIsActive(assetID) {
  3770  			owns, err := wallet.OwnsDepositAddress(oldDepositAddr)
  3771  			if err != nil {
  3772  				return err
  3773  			}
  3774  			if !owns {
  3775  				return errors.New("new wallet in active use does not own the old deposit address. abandoning configuration update")
  3776  			}
  3777  		}
  3778  		return nil
  3779  	}
  3780  
  3781  	reloadWallet := func(w *xcWallet, dbWallet *db.Wallet, checkSameness bool) error {
  3782  		// Must connect to ensure settings are good. This comes before
  3783  		// setWalletPassword since it would use connectAndUpdateWallet, which
  3784  		// performs additional deposit address validation and balance updates that
  3785  		// are redundant with the rest of this function.
  3786  		dbWallet.Address, err = c.connectWalletResumeTrades(w, false)
  3787  		if err != nil {
  3788  			return fmt.Errorf("connectWallet: %w", err)
  3789  		}
  3790  
  3791  		if checkSameness {
  3792  			if err := sameWallet(); err != nil {
  3793  				wallet.Disconnect()
  3794  				return newError(walletErr, "new wallet cannot be used with current active trades: %w", err)
  3795  			}
  3796  			// If newWalletPW is non-nil, update the wallet's password.
  3797  			if newWalletPW != nil { // includes empty non-nil slice
  3798  				err = c.setWalletPassword(wallet, newWalletPW, crypter)
  3799  				if err != nil {
  3800  					wallet.Disconnect()
  3801  					return fmt.Errorf("setWalletPassword: %v", err)
  3802  				}
  3803  				// Update dbWallet so db.UpdateWallet below reflects the new password.
  3804  				dbWallet.EncryptedPW = wallet.encPW()
  3805  			} else if oldWallet.locallyUnlocked() {
  3806  				// If the password was not changed, carry over any cached password
  3807  				// regardless of backend lock state. loadWallet already copied encPW, so
  3808  				// this will decrypt pw rather than actually copying it, and it will
  3809  				// ensure the backend is also unlocked.
  3810  				err := wallet.Unlock(crypter) // decrypt encPW if set and unlock the backend
  3811  				if err != nil {
  3812  					wallet.Disconnect()
  3813  					return newError(walletAuthErr, "wallet successfully connected, but failed to unlock. "+
  3814  						"reconfiguration not saved: %w", err)
  3815  				}
  3816  			}
  3817  		}
  3818  
  3819  		if err = storeWithBalance(w, dbWallet); err != nil {
  3820  			w.Disconnect()
  3821  			return err
  3822  		}
  3823  
  3824  		c.updateAssetWalletRefs(w)
  3825  		// reReserveFunding is likely a no-op because of the walletIsActive check
  3826  		// above, and because of the way current LiveReconfigurers are implemented.
  3827  		// For forward compatibility though, if a LiveReconfigurer with active
  3828  		// orders indicates restart and the new wallet still owns the keys, we can
  3829  		// end up here and we need to re-reserve.
  3830  		go c.reReserveFunding(w)
  3831  		return nil
  3832  	}
  3833  
  3834  	// Reload the wallet
  3835  	if err := reloadWallet(wallet, dbWallet, true); err != nil {
  3836  		return err
  3837  	}
  3838  
  3839  	restartOnFail = false
  3840  	success = true
  3841  
  3842  	// If there are tokens, reload those wallets.
  3843  	for tokenID := range asset.Asset(assetID).Tokens {
  3844  		tokenWallet, found := c.wallet(tokenID)
  3845  		if found {
  3846  			tokenDBWallet, err := c.db.Wallet((&db.Wallet{AssetID: tokenID}).ID())
  3847  			if err != nil {
  3848  				c.log.Errorf("Error getting db wallet for token %s: %w", unbip(tokenID), err)
  3849  				continue
  3850  			}
  3851  			tokenWallet.Disconnect()
  3852  			tokenWallet, err = c.loadWallet(tokenDBWallet)
  3853  			if err != nil {
  3854  				c.log.Errorf("Error loading wallet for token %s: %w", unbip(tokenID), err)
  3855  				continue
  3856  			}
  3857  			if err := reloadWallet(tokenWallet, tokenDBWallet, false); err != nil {
  3858  				c.log.Errorf("Error reloading token wallet %s: %w", unbip(tokenID), err)
  3859  			}
  3860  		}
  3861  	}
  3862  
  3863  	if oldWallet.connected() {
  3864  		// NOTE: Cannot lock the wallet backend because it may be the same as
  3865  		// the one just connected.
  3866  		go oldWallet.Disconnect()
  3867  	}
  3868  
  3869  	clearTickGovernors()
  3870  
  3871  	c.resumeTrades(crypter)
  3872  
  3873  	return nil
  3874  }
  3875  
  3876  // updateAssetWalletRefs sets all references of an asset's wallet to newWallet.
  3877  func (c *Core) updateAssetWalletRefs(newWallet *xcWallet) {
  3878  	assetID := newWallet.AssetID
  3879  	updateWalletSet := func(t *trackedTrade) {
  3880  		t.mtx.Lock()
  3881  		defer t.mtx.Unlock()
  3882  
  3883  		if t.wallets.fromWallet.AssetID == assetID {
  3884  			t.wallets.fromWallet = newWallet
  3885  		} else if t.wallets.toWallet.AssetID == assetID {
  3886  			t.wallets.toWallet = newWallet
  3887  		} else {
  3888  			return // no need to check base/quote wallet aliases
  3889  		}
  3890  
  3891  		// Also base/quote wallet aliases. The following is more fool-proof and
  3892  		// concise than nested t.Trade().Sell conditions above:
  3893  		if t.wallets.baseWallet.AssetID == assetID {
  3894  			t.wallets.baseWallet = newWallet
  3895  		} else /* t.wallets.quoteWallet.AssetID == assetID */ {
  3896  			t.wallets.quoteWallet = newWallet
  3897  		}
  3898  	}
  3899  
  3900  	for _, dc := range c.dexConnections() {
  3901  		for _, tracker := range dc.trackedTrades() {
  3902  			updateWalletSet(tracker)
  3903  		}
  3904  	}
  3905  
  3906  	c.updateWallet(assetID, newWallet)
  3907  }
  3908  
  3909  // SetWalletPassword updates the (encrypted) password for the wallet. Returns
  3910  // passwordErr if provided newPW is nil. The wallet will be connected if it is
  3911  // not already.
  3912  func (c *Core) SetWalletPassword(appPW []byte, assetID uint32, newPW []byte) error {
  3913  	// Ensure newPW isn't nil.
  3914  	if newPW == nil {
  3915  		return newError(passwordErr, "SetWalletPassword password can't be nil")
  3916  	}
  3917  
  3918  	// Check the app password and get the crypter.
  3919  	crypter, err := c.encryptionKey(appPW)
  3920  	if err != nil {
  3921  		return newError(authErr, "SetWalletPassword password error: %w", err)
  3922  	}
  3923  	defer crypter.Close()
  3924  
  3925  	// Check that the specified wallet exists.
  3926  	c.walletMtx.Lock()
  3927  	defer c.walletMtx.Unlock()
  3928  	wallet, found := c.wallets[assetID]
  3929  	if !found {
  3930  		return newError(missingWalletErr, "wallet for %s (%d) is not known", unbip(assetID), assetID)
  3931  	}
  3932  
  3933  	// Set new password, connecting to it if necessary to verify. It is left
  3934  	// connected since it is in the wallets map.
  3935  	return c.setWalletPassword(wallet, newPW, crypter)
  3936  }
  3937  
  3938  // setWalletPassword updates the (encrypted) password for the wallet.
  3939  func (c *Core) setWalletPassword(wallet *xcWallet, newPW []byte, crypter encrypt.Crypter) error {
  3940  	authenticator, is := wallet.Wallet.(asset.Authenticator)
  3941  	if !is { // password setting is not supported by wallet.
  3942  		return newError(passwordErr, "wallet does not support password setting")
  3943  	}
  3944  
  3945  	walletDef, err := asset.WalletDef(wallet.AssetID, wallet.walletType)
  3946  	if err != nil {
  3947  		return newError(assetSupportErr, "asset.WalletDef error: %w", err)
  3948  	}
  3949  	if walletDef.Seeded || asset.TokenInfo(wallet.AssetID) != nil {
  3950  		return newError(passwordErr, "cannot set a password on a seeded or token wallet")
  3951  	}
  3952  
  3953  	// Connect if necessary.
  3954  	wasConnected := wallet.connected()
  3955  	if !wasConnected {
  3956  		if err := c.connectAndUpdateWallet(wallet); err != nil {
  3957  			return newError(connectionErr, "SetWalletPassword connection error: %w", err)
  3958  		}
  3959  	}
  3960  
  3961  	wasUnlocked := wallet.unlocked()
  3962  	newPasswordSet := len(newPW) > 0 // excludes empty but non-nil
  3963  
  3964  	// Check that the new password works.
  3965  	if newPasswordSet {
  3966  		// Encrypt password if it's not an empty string.
  3967  		encNewPW, err := crypter.Encrypt(newPW)
  3968  		if err != nil {
  3969  			return newError(encryptionErr, "encryption error: %w", err)
  3970  		}
  3971  		err = authenticator.Unlock(newPW)
  3972  		if err != nil {
  3973  			return newError(authErr,
  3974  				"setWalletPassword unlocking wallet error, is the new password correct?: %w", err)
  3975  		}
  3976  		wallet.setEncPW(encNewPW)
  3977  	} else {
  3978  		// Test that the wallet is actually good with no password. At present,
  3979  		// this means the backend either cannot be locked or unlocks with an
  3980  		// empty password. The following Lock->Unlock cycle but may be required
  3981  		// to detect a newly-unprotected wallet without reconnecting. We will
  3982  		// ignore errors in this process as we are discovering the true state.
  3983  		// check the backend directly, not using the xcWallet
  3984  		_ = authenticator.Lock()
  3985  		_ = authenticator.Unlock([]byte{})
  3986  		if authenticator.Locked() {
  3987  			if wasUnlocked { // try to re-unlock the wallet with previous encPW
  3988  				_ = c.unlockWallet(crypter, wallet)
  3989  			}
  3990  			return newError(authErr, "wallet appears to require a password")
  3991  		}
  3992  		wallet.setEncPW(nil)
  3993  	}
  3994  
  3995  	err = c.db.SetWalletPassword(wallet.dbID, wallet.encPW())
  3996  	if err != nil {
  3997  		return codedError(dbErr, err)
  3998  	}
  3999  
  4000  	// Re-lock the wallet if it was previously locked.
  4001  	if !wasUnlocked && newPasswordSet {
  4002  		if err = wallet.Lock(2 * time.Second); err != nil {
  4003  			c.log.Warnf("Unable to relock %s wallet: %v", unbip(wallet.AssetID), err)
  4004  		}
  4005  	}
  4006  
  4007  	// Do not disconnect because the Wallet may not allow reconnection.
  4008  
  4009  	subject, details := c.formatDetails(TopicWalletPasswordUpdated, unbip(wallet.AssetID))
  4010  	c.notify(newWalletConfigNote(TopicWalletPasswordUpdated, subject, details, db.Success, wallet.state()))
  4011  
  4012  	return nil
  4013  }
  4014  
  4015  // NewDepositAddress retrieves a new deposit address from the specified asset's
  4016  // wallet, saves it to the database, and emits a notification. If the wallet
  4017  // does not support generating new addresses, the current address will be
  4018  // returned.
  4019  func (c *Core) NewDepositAddress(assetID uint32) (string, error) {
  4020  	w, exists := c.wallet(assetID)
  4021  	if !exists {
  4022  		return "", newError(missingWalletErr, "no wallet found for %s", unbip(assetID))
  4023  	}
  4024  
  4025  	var addr string
  4026  	if _, ok := w.Wallet.(asset.NewAddresser); ok {
  4027  		// Retrieve a fresh deposit address.
  4028  		var err error
  4029  		addr, err = w.refreshDepositAddress()
  4030  		if err != nil {
  4031  			return "", err
  4032  		}
  4033  		if err = c.storeDepositAddress(w.dbID, addr); err != nil {
  4034  			return "", err
  4035  		}
  4036  		// Update wallet state in the User data struct and emit a WalletStateNote.
  4037  		c.notify(newWalletStateNote(w.state()))
  4038  	} else {
  4039  		addr = w.address
  4040  	}
  4041  
  4042  	return addr, nil
  4043  }
  4044  
  4045  // AddressUsed checks whether an address for a NewAddresser has been used.
  4046  func (c *Core) AddressUsed(assetID uint32, addr string) (bool, error) {
  4047  	w, exists := c.wallet(assetID)
  4048  	if !exists {
  4049  		return false, newError(missingWalletErr, "no wallet found for %s", unbip(assetID))
  4050  	}
  4051  
  4052  	na, ok := w.Wallet.(asset.NewAddresser)
  4053  	if !ok {
  4054  		return false, errors.New("wallet is not a NewAddresser")
  4055  	}
  4056  
  4057  	return na.AddressUsed(addr)
  4058  }
  4059  
  4060  // AutoWalletConfig attempts to load setting from a wallet package's
  4061  // asset.WalletInfo.DefaultConfigPath. If settings are not found, an empty map
  4062  // is returned.
  4063  func (c *Core) AutoWalletConfig(assetID uint32, walletType string) (map[string]string, error) {
  4064  	walletDef, err := asset.WalletDef(assetID, walletType)
  4065  	if err != nil {
  4066  		return nil, newError(assetSupportErr, "asset.WalletDef error: %w", err)
  4067  	}
  4068  
  4069  	if walletDef.DefaultConfigPath == "" {
  4070  		return nil, fmt.Errorf("no config path found for %s wallet, type %q", unbip(assetID), walletType)
  4071  	}
  4072  
  4073  	settings, err := config.Parse(walletDef.DefaultConfigPath)
  4074  	c.log.Infof("%d %s configuration settings loaded from file at default location %s", len(settings), unbip(assetID), walletDef.DefaultConfigPath)
  4075  	if err != nil {
  4076  		c.log.Debugf("config.Parse could not load settings from default path: %v", err)
  4077  		return make(map[string]string), nil
  4078  	}
  4079  	return settings, nil
  4080  }
  4081  
  4082  // tempDexConnection creates an unauthenticated dexConnection. The caller must
  4083  // dc.connMaster.Disconnect when done with the connection.
  4084  func (c *Core) tempDexConnection(dexAddr string, certI any) (*dexConnection, error) {
  4085  	host, err := addrHost(dexAddr)
  4086  	if err != nil {
  4087  		return nil, newError(addressParseErr, "error parsing address: %w", err)
  4088  	}
  4089  	cert, err := parseCert(host, certI, c.net)
  4090  	if err != nil {
  4091  		return nil, newError(fileReadErr, "failed to parse certificate: %w", err)
  4092  	}
  4093  
  4094  	c.connMtx.RLock()
  4095  	_, found := c.conns[host]
  4096  	c.connMtx.RUnlock()
  4097  	if found {
  4098  		return nil, newError(dupeDEXErr, "already registered at %s", dexAddr)
  4099  	}
  4100  
  4101  	// TODO: if a "keyless" (view-only) dex connection exists, this temp
  4102  	// connection may be used to replace the existing connection and likely
  4103  	// without (properly) closing the existing connection. Is this OK??
  4104  	return c.connectDEXWithFlag(&db.AccountInfo{
  4105  		Host:      host,
  4106  		Cert:      cert,
  4107  		BondAsset: defaultBondAsset,
  4108  	}, connectDEXFlagTemporary)
  4109  }
  4110  
  4111  // GetDEXConfig creates a temporary connection to the specified DEX Server and
  4112  // fetches the full exchange config. The connection is closed after the config
  4113  // is retrieved. An error is returned if user is already registered to the DEX
  4114  // since a DEX connection is already established and the config is accessible
  4115  // via the User or Exchanges methods. A TLS certificate, certI, can be provided
  4116  // as either a string filename, or []byte file contents.
  4117  func (c *Core) GetDEXConfig(dexAddr string, certI any) (*Exchange, error) {
  4118  	dc, err := c.tempDexConnection(dexAddr, certI)
  4119  	if err != nil {
  4120  		return nil, err
  4121  	}
  4122  
  4123  	// Since connectDEX succeeded, we have the server config. exchangeInfo is
  4124  	// guaranteed to return an *Exchange with full asset and market info.
  4125  	return c.exchangeInfo(dc), nil
  4126  }
  4127  
  4128  // AddDEX configures a view-only DEX connection. This allows watching trade
  4129  // activity without setting up account keys or communicating account identity
  4130  // with the DEX. DiscoverAccount, PostBond may be used to set up a trading
  4131  // account for this DEX if required.
  4132  func (c *Core) AddDEX(appPW []byte, dexAddr string, certI any) error {
  4133  	if !c.IsInitialized() { // TODO: Allow adding view-only DEX without init.
  4134  		return fmt.Errorf("cannot register DEX because app has not been initialized")
  4135  	}
  4136  
  4137  	host, err := addrHost(dexAddr)
  4138  	if err != nil {
  4139  		return newError(addressParseErr, "error parsing address: %w", err)
  4140  	}
  4141  
  4142  	cert, err := parseCert(host, certI, c.net)
  4143  	if err != nil {
  4144  		return newError(fileReadErr, "failed to parse certificate: %w", err)
  4145  	}
  4146  
  4147  	c.connMtx.RLock()
  4148  	_, found := c.conns[host]
  4149  	c.connMtx.RUnlock()
  4150  	if found {
  4151  		return newError(dupeDEXErr, "already connected to DEX at %s", dexAddr)
  4152  	}
  4153  
  4154  	dc, err := c.connectDEXWithFlag(&db.AccountInfo{
  4155  		Host: host,
  4156  		Cert: cert,
  4157  	}, connectDEXFlagViewOnly)
  4158  	if err != nil {
  4159  		if dc != nil {
  4160  			// Stop (re)connect loop, which may be running even if err != nil.
  4161  			dc.connMaster.Disconnect()
  4162  		}
  4163  		return codedError(connectionErr, err)
  4164  	}
  4165  
  4166  	// Close the connection to the dex server if adding the dex fails.
  4167  	var success bool
  4168  	defer func() {
  4169  		if !success {
  4170  			dc.connMaster.Disconnect()
  4171  		}
  4172  	}()
  4173  
  4174  	// Don't allow adding another dex with the same pubKey. There can only be
  4175  	// one dex connection per pubKey. UpdateDEXHost must be called to connect to
  4176  	// the same dex using a different host name.
  4177  	exists, host := c.dexWithPubKeyExists(dc.acct.dexPubKey)
  4178  	if exists {
  4179  		return newError(dupeDEXErr, "already connected to DEX at %s but with different host name %s", dexAddr, host)
  4180  	}
  4181  
  4182  	err = c.db.CreateAccount(&db.AccountInfo{
  4183  		Host:      dc.acct.host,
  4184  		Cert:      dc.acct.cert,
  4185  		DEXPubKey: dc.acct.dexPubKey,
  4186  	})
  4187  	if err != nil {
  4188  		return fmt.Errorf("error saving account info for view-only DEX: %w", err)
  4189  	}
  4190  
  4191  	success = true
  4192  	c.connMtx.Lock()
  4193  	c.conns[dc.acct.host] = dc
  4194  	c.connMtx.Unlock()
  4195  
  4196  	// If a password was provided, try discoverAccount, but OK if we don't find
  4197  	// it.
  4198  	if len(appPW) > 0 {
  4199  		crypter, err := c.encryptionKey(appPW)
  4200  		if err != nil {
  4201  			return codedError(passwordErr, err)
  4202  		}
  4203  		defer crypter.Close()
  4204  
  4205  		paid, err := c.discoverAccount(dc, crypter)
  4206  		if err != nil {
  4207  			c.log.Errorf("discoverAccount error during AddDEX: %v", err)
  4208  		} else if paid {
  4209  			c.upgradeConnection(dc)
  4210  		}
  4211  	}
  4212  
  4213  	return nil
  4214  }
  4215  
  4216  // dbCreateOrUpdateAccount saves account info to db after an account is
  4217  // discovered or registration/postbond completes.
  4218  func (c *Core) dbCreateOrUpdateAccount(dc *dexConnection, ai *db.AccountInfo) error {
  4219  	dc.acct.keyMtx.Lock()
  4220  	defer dc.acct.keyMtx.Unlock()
  4221  
  4222  	if !dc.acct.viewOnly {
  4223  		return c.db.CreateAccount(ai)
  4224  	}
  4225  
  4226  	err := c.db.UpdateAccountInfo(ai)
  4227  	if err == nil {
  4228  		dc.acct.viewOnly = false
  4229  	}
  4230  	return err
  4231  }
  4232  
  4233  // discoverAccount attempts to identify existing accounts at the connected DEX.
  4234  // The dexConnection.acct struct will have its encKey, privKey, and id fields
  4235  // set. If the bool is true, the account will have been recorded in the DB, and
  4236  // the isPaid and feeCoin fields of the account set. If the bool is false, the
  4237  // account is not paid and the user should register.
  4238  func (c *Core) discoverAccount(dc *dexConnection, crypter encrypt.Crypter) (bool, error) {
  4239  	if dc.acct.dexPubKey == nil {
  4240  		return false, fmt.Errorf("dex server does not support HD key accounts")
  4241  	}
  4242  
  4243  	// Setup our account keys and attempt to authorize with the DEX.
  4244  	creds := c.creds()
  4245  
  4246  	// Start at key index 0 and attempt to authorize accounts until either (1)
  4247  	// the server indicates the account is not found, and we return paid=false
  4248  	// to signal a new account should be registered, or (2) an account is found
  4249  	// that is not suspended, and we return with paid=true after storing the
  4250  	// discovered account and promoting it to a persistent connection. In this
  4251  	// process, we will increment the key index and try again whenever the
  4252  	// connect response indicates a suspended account is found. This instance of
  4253  	// Core lacks any order or match history for this dex to complete any active
  4254  	// swaps that might exist for a suspended account, so the user had better
  4255  	// have another instance with this data if they hope to recover those swaps.
  4256  	var keyIndex uint32
  4257  	for {
  4258  		err := dc.acct.setupCryptoV2(creds, crypter, keyIndex)
  4259  		if err != nil {
  4260  			return false, newError(acctKeyErr, "setupCryptoV2 error: %w", err)
  4261  		}
  4262  
  4263  		// Discover the account by attempting a 'connect' (authorize) request.
  4264  		err = c.authDEX(dc)
  4265  		if err != nil {
  4266  			var mErr *msgjson.Error
  4267  			if errors.As(err, &mErr) && (mErr.Code == msgjson.AccountNotFoundError ||
  4268  				mErr.Code == msgjson.UnpaidAccountError) {
  4269  				if mErr.Code == msgjson.UnpaidAccountError {
  4270  					c.log.Warnf("Detected existing but unpaid account! Register " +
  4271  						"with the same credentials to complete registration with " +
  4272  						"the previously-assigned fee address and asset ID.")
  4273  				}
  4274  				return false, nil // all good, just go register/postbond now
  4275  			}
  4276  			return false, newError(authErr, "unexpected authDEX error: %w", err)
  4277  		}
  4278  
  4279  		// skip key if account cannot be used to trade, i.e. tier < 0 or tier ==
  4280  		// 0 but server doesn't support bonds. If tier == 0 and server supports
  4281  		// bonds, a bond must be posted before the account can be used to trade,
  4282  		// but generating a new key isn't necessary.
  4283  
  4284  		// DRAFT NOTE: This was wrong? Isn't account suspended at tier 0?
  4285  		// cannotTrade := dc.acct.effectiveTier < 0 || (dc.acct.effectiveTier == 0 && dc.apiVersion() < serverdex.BondAPIVersion)
  4286  		rep := dc.acct.rep
  4287  		// Using <= though acknowledging that this ignores the possibility that
  4288  		// an existing revoked bond could be resurrected.
  4289  		if rep.BondedTier <= int64(rep.Penalties) {
  4290  			dc.acct.unAuth() // acct was marked as authenticated by authDEX above.
  4291  			c.log.Infof("HD account key for %s has tier %d, but %d penalties (not able to trade). Deriving another account key.",
  4292  				dc.acct.host, rep.BondedTier, rep.Penalties)
  4293  			time.Sleep(200 * time.Millisecond) // don't hammer
  4294  			keyIndex++
  4295  			continue
  4296  		}
  4297  
  4298  		break // great, the account at this key index exists
  4299  	}
  4300  
  4301  	err := c.dbCreateOrUpdateAccount(dc, &db.AccountInfo{
  4302  		Host:      dc.acct.host,
  4303  		Cert:      dc.acct.cert,
  4304  		DEXPubKey: dc.acct.dexPubKey,
  4305  		EncKeyV2:  dc.acct.encKey,
  4306  		Bonds:     dc.acct.bonds, // any reported by server
  4307  		BondAsset: dc.acct.bondAsset,
  4308  	})
  4309  	if err != nil {
  4310  		return false, fmt.Errorf("error saving restored account: %w", err)
  4311  	}
  4312  
  4313  	return true, nil // great, just stay connected
  4314  }
  4315  
  4316  // dexWithPubKeyExists checks whether or not there is a non-disabled account
  4317  // for a dex that has pubKey.
  4318  func (c *Core) dexWithPubKeyExists(pubKey *secp256k1.PublicKey) (bool, string) {
  4319  	for _, dc := range c.dexConnections() {
  4320  		if dc.acct.dexPubKey == nil {
  4321  			continue
  4322  		}
  4323  
  4324  		if dc.acct.dexPubKey.IsEqual(pubKey) {
  4325  			return true, dc.acct.host
  4326  		}
  4327  	}
  4328  
  4329  	return false, ""
  4330  }
  4331  
  4332  // upgradeConnection promotes a temporary dex connection and starts listening
  4333  // to the messages it receives.
  4334  func (c *Core) upgradeConnection(dc *dexConnection) {
  4335  	if atomic.CompareAndSwapUint32(&dc.reportingConnects, 0, 1) {
  4336  		c.wg.Add(1)
  4337  		go c.listen(dc)
  4338  		go dc.subPriceFeed()
  4339  	}
  4340  	c.addDexConnection(dc)
  4341  }
  4342  
  4343  // DiscoverAccount fetches the DEX server's config, and if the server supports
  4344  // the new deterministic account derivation scheme by providing its public key
  4345  // in the config response, DiscoverAccount also checks if the account is already
  4346  // paid. If the returned paid value is true, the account is ready for immediate
  4347  // use. If paid is false, Register should be used to complete the registration.
  4348  // For an older server that does not provide its pubkey in the config response,
  4349  // paid will always be false and the user should proceed to use Register.
  4350  //
  4351  // The purpose of DiscoverAccount is existing account discovery when the client
  4352  // has been restored from seed. As such, DiscoverAccount is not strictly necessary
  4353  // to register on a DEX, and Register may be called directly, although it requires
  4354  // the expected fee amount as an additional input and it will pay the fee if the
  4355  // account is not discovered and paid.
  4356  //
  4357  // The Tier and BondsPending fields may be consulted to determine if it is still
  4358  // necessary to PostBond (i.e. Tier == 0 && !BondsPending) before trading. The
  4359  // Connected field should be consulted first.
  4360  func (c *Core) DiscoverAccount(dexAddr string, appPW []byte, certI any) (*Exchange, bool, error) {
  4361  	if !c.IsInitialized() {
  4362  		return nil, false, fmt.Errorf("cannot register DEX because app has not been initialized")
  4363  	}
  4364  
  4365  	host, err := addrHost(dexAddr)
  4366  	if err != nil {
  4367  		return nil, false, newError(addressParseErr, "error parsing address: %w", err)
  4368  	}
  4369  
  4370  	crypter, err := c.encryptionKey(appPW)
  4371  	if err != nil {
  4372  		return nil, false, codedError(passwordErr, err)
  4373  	}
  4374  	defer crypter.Close()
  4375  
  4376  	c.connMtx.RLock()
  4377  	dc, existingConn := c.conns[host]
  4378  	c.connMtx.RUnlock()
  4379  	if existingConn && !dc.acct.isViewOnly() {
  4380  		// Already registered, but connection may be down and/or PostBond needed.
  4381  		return c.exchangeInfo(dc), true, nil // *Exchange has Tier and BondsPending
  4382  	}
  4383  
  4384  	var ready bool
  4385  	if !existingConn {
  4386  		dc, err = c.tempDexConnection(host, certI)
  4387  		if err != nil {
  4388  			return nil, false, err
  4389  		}
  4390  
  4391  		defer func() {
  4392  			// Either disconnect or promote this connection.
  4393  			if !ready {
  4394  				dc.connMaster.Disconnect()
  4395  				return
  4396  			}
  4397  
  4398  			c.upgradeConnection(dc)
  4399  		}()
  4400  	}
  4401  
  4402  	// Older DEX server. We won't allow registering without an HD account key,
  4403  	// but discovery can conclude we do not have an HD account with this DEX.
  4404  	if dc.acct.dexPubKey == nil {
  4405  		return c.exchangeInfo(dc), false, nil
  4406  	}
  4407  
  4408  	// Don't allow registering for another dex with the same pubKey. There can only
  4409  	// be one dex connection per pubKey. UpdateDEXHost must be called to connect to
  4410  	// the same dex using a different host name.
  4411  	if !existingConn {
  4412  		exists, host := c.dexWithPubKeyExists(dc.acct.dexPubKey)
  4413  		if exists {
  4414  			return nil, false,
  4415  				fmt.Errorf("the dex at %v is the same dex as %v. Use Update Host to switch host names", host, dexAddr)
  4416  		}
  4417  	}
  4418  
  4419  	// Setup our account keys and attempt to authorize with the DEX.
  4420  	paid, err := c.discoverAccount(dc, crypter)
  4421  	if err != nil {
  4422  		return nil, false, err
  4423  	}
  4424  	if !paid {
  4425  		return c.exchangeInfo(dc), false, nil // all good, just go register or postbond now
  4426  	}
  4427  
  4428  	ready = true // do not disconnect
  4429  
  4430  	return c.exchangeInfo(dc), true, nil
  4431  }
  4432  
  4433  // IsInitialized checks if the app is already initialized.
  4434  func (c *Core) IsInitialized() bool {
  4435  	c.credMtx.RLock()
  4436  	defer c.credMtx.RUnlock()
  4437  	return c.credentials != nil
  4438  }
  4439  
  4440  // InitializeClient sets the initial app-wide password and app seed for the
  4441  // client. The seed argument should be left nil unless restoring from seed.
  4442  func (c *Core) InitializeClient(pw []byte, restorationSeed *string) (string, error) {
  4443  	if c.IsInitialized() {
  4444  		return "", fmt.Errorf("already initialized, login instead")
  4445  	}
  4446  
  4447  	_, creds, mnemonicSeed, err := c.generateCredentials(pw, restorationSeed)
  4448  	if err != nil {
  4449  		return "", err
  4450  	}
  4451  
  4452  	err = c.db.SetPrimaryCredentials(creds)
  4453  	if err != nil {
  4454  		return "", fmt.Errorf("SetPrimaryCredentials error: %w", err)
  4455  	}
  4456  
  4457  	freshSeed := restorationSeed == nil
  4458  	if freshSeed {
  4459  		now := uint64(time.Now().Unix())
  4460  		err = c.db.SetSeedGenerationTime(now)
  4461  		if err != nil {
  4462  			return "", fmt.Errorf("SetSeedGenerationTime error: %w", err)
  4463  		}
  4464  		c.seedGenerationTime = now
  4465  
  4466  		subject, details := c.formatDetails(TopicSeedNeedsSaving)
  4467  		c.notify(newSecurityNote(TopicSeedNeedsSaving, subject, details, db.Success))
  4468  	}
  4469  
  4470  	c.setCredentials(creds)
  4471  	return mnemonicSeed, nil
  4472  }
  4473  
  4474  // ExportSeed exports the application seed.
  4475  func (c *Core) ExportSeed(pw []byte) (seedStr string, err error) {
  4476  	crypter, err := c.encryptionKey(pw)
  4477  	if err != nil {
  4478  		return "", fmt.Errorf("ExportSeed password error: %w", err)
  4479  	}
  4480  	defer crypter.Close()
  4481  
  4482  	creds := c.creds()
  4483  	if creds == nil {
  4484  		return "", fmt.Errorf("no v2 credentials stored")
  4485  	}
  4486  
  4487  	seed, err := crypter.Decrypt(creds.EncSeed)
  4488  	if err != nil {
  4489  		return "", fmt.Errorf("app seed decryption error: %w", err)
  4490  	}
  4491  
  4492  	if len(seed) == legacySeedLength {
  4493  		seedStr = hex.EncodeToString(seed)
  4494  	} else {
  4495  		seedStr, err = mnemonic.GenerateMnemonic(seed, creds.Birthday)
  4496  		if err != nil {
  4497  			return "", fmt.Errorf("error generating mnemonic: %w", err)
  4498  		}
  4499  	}
  4500  
  4501  	return seedStr, nil
  4502  }
  4503  
  4504  func decodeSeedString(seedStr string) (seed []byte, bday time.Time, err error) {
  4505  	// See if it decodes as a mnemonic seed first.
  4506  	seed, bday, err = mnemonic.DecodeMnemonic(seedStr)
  4507  	if err != nil {
  4508  		// Is it an old-school hex seed?
  4509  		bday = time.Time{}
  4510  		seed, err = hex.DecodeString(strings.Join(strings.Fields(seedStr), ""))
  4511  		if err != nil {
  4512  			return nil, time.Time{}, errors.New("unabled to decode provided seed")
  4513  		}
  4514  		if len(seed) != legacySeedLength {
  4515  			return nil, time.Time{}, errors.New("decoded seed is wrong length")
  4516  		}
  4517  	}
  4518  	return
  4519  }
  4520  
  4521  // generateCredentials generates a new set of *PrimaryCredentials. The
  4522  // credentials are not stored to the database. A restoration seed can be
  4523  // provided, otherwise should be nil.
  4524  func (c *Core) generateCredentials(pw []byte, optionalSeed *string) (encrypt.Crypter, *db.PrimaryCredentials, string, error) {
  4525  	if len(pw) == 0 {
  4526  		return nil, nil, "", fmt.Errorf("empty password not allowed")
  4527  	}
  4528  
  4529  	var seed []byte
  4530  	defer encode.ClearBytes(seed)
  4531  	var bday time.Time
  4532  	var mnemonicSeed string
  4533  	if optionalSeed == nil {
  4534  		bday = time.Now()
  4535  		seed, mnemonicSeed = mnemonic.New()
  4536  	} else {
  4537  		var err error
  4538  		// Is it a mnemonic seed?
  4539  		seed, bday, err = decodeSeedString(*optionalSeed)
  4540  		if err != nil {
  4541  			return nil, nil, "", err
  4542  		}
  4543  	}
  4544  
  4545  	// Generate an inner key and it's Crypter.
  4546  	innerKey := seedInnerKey(seed)
  4547  	innerCrypter := c.newCrypter(innerKey[:])
  4548  	encSeed, err := innerCrypter.Encrypt(seed)
  4549  	if err != nil {
  4550  		return nil, nil, "", fmt.Errorf("client seed encryption error: %w", err)
  4551  	}
  4552  
  4553  	// Generate the outer key.
  4554  	outerCrypter := c.newCrypter(pw)
  4555  	encInnerKey, err := outerCrypter.Encrypt(innerKey[:])
  4556  	if err != nil {
  4557  		return nil, nil, "", fmt.Errorf("inner key encryption error: %w", err)
  4558  	}
  4559  
  4560  	creds := &db.PrimaryCredentials{
  4561  		EncSeed:        encSeed,
  4562  		EncInnerKey:    encInnerKey,
  4563  		InnerKeyParams: innerCrypter.Serialize(),
  4564  		OuterKeyParams: outerCrypter.Serialize(),
  4565  		Birthday:       bday,
  4566  		Version:        1,
  4567  	}
  4568  
  4569  	return innerCrypter, creds, mnemonicSeed, nil
  4570  }
  4571  
  4572  func seedInnerKey(seed []byte) []byte {
  4573  	// keyParam is a domain-specific value to ensure the resulting key is unique
  4574  	// for the specific use case of deriving an inner encryption key from the
  4575  	// seed. Any other uses of derivation from the seed should similarly create
  4576  	// their own domain-specific value to ensure uniqueness.
  4577  	//
  4578  	// It is equal to BLAKE-256([]byte("DCRDEX-InnerKey-v0")).
  4579  	keyParam := [32]byte{
  4580  		0x75, 0x25, 0xb1, 0xb6, 0x53, 0x33, 0x9e, 0x33,
  4581  		0xbe, 0x11, 0x61, 0x45, 0x1a, 0x88, 0x6f, 0x37,
  4582  		0xe7, 0x74, 0xdf, 0xca, 0xb4, 0x8a, 0xee, 0x0e,
  4583  		0x7c, 0x84, 0x60, 0x01, 0xed, 0xe5, 0xf6, 0x97,
  4584  	}
  4585  	key := make([]byte, len(seed)+len(keyParam))
  4586  	copy(key, seed)
  4587  	copy(key[len(seed):], keyParam[:])
  4588  	innerKey := blake256.Sum256(key)
  4589  	return innerKey[:]
  4590  }
  4591  
  4592  func (c *Core) bondKeysReady() bool {
  4593  	c.loginMtx.Lock()
  4594  	defer c.loginMtx.Unlock()
  4595  	return c.bondXPriv != nil && c.bondXPriv.IsPrivate() // infer not Zeroed via IsPrivate
  4596  }
  4597  
  4598  // Login logs the user in. On the first login after startup or after a logout,
  4599  // this function will connect wallets, resolve active trades, and decrypt
  4600  // account keys for all known DEXes. Otherwise, it will only check whether or
  4601  // not the app pass is correct.
  4602  func (c *Core) Login(pw []byte) error {
  4603  	// Make sure the app has been initialized. This condition would error when
  4604  	// attempting to retrieve the encryption key below as well, but the
  4605  	// messaging may be confusing.
  4606  	c.credMtx.RLock()
  4607  	creds := c.credentials
  4608  	c.credMtx.RUnlock()
  4609  
  4610  	if creds == nil {
  4611  		return fmt.Errorf("cannot log in because app has not been initialized")
  4612  	}
  4613  
  4614  	c.notify(newLoginNote("Verifying credentials..."))
  4615  	if len(creds.EncInnerKey) == 0 {
  4616  		err := c.initializePrimaryCredentials(pw, creds.OuterKeyParams)
  4617  		if err != nil {
  4618  			// It's tempting to panic here, since Core and the db are probably
  4619  			// out of sync and the client shouldn't be doing anything else.
  4620  			c.log.Criticalf("v1 upgrade failed: %v", err)
  4621  			return err
  4622  		}
  4623  	}
  4624  
  4625  	crypter, err := c.encryptionKey(pw)
  4626  	if err != nil {
  4627  		return err
  4628  	}
  4629  	defer crypter.Close()
  4630  
  4631  	switch creds.Version {
  4632  	case 0:
  4633  		if crypter, creds, err = c.upgradeV0CredsToV1(pw, *creds); err != nil {
  4634  			return fmt.Errorf("error upgrading primary credentials from version 0 to 1: %w", err)
  4635  		}
  4636  	}
  4637  
  4638  	login := func() (needInit bool, err error) {
  4639  		c.loginMtx.Lock()
  4640  		defer c.loginMtx.Unlock()
  4641  		if !c.loggedIn {
  4642  			// Derive the bond extended key from the seed.
  4643  			seed, err := crypter.Decrypt(creds.EncSeed)
  4644  			if err != nil {
  4645  				return false, fmt.Errorf("seed decryption error: %w", err)
  4646  			}
  4647  			defer encode.ClearBytes(seed)
  4648  			c.bondXPriv, err = deriveBondXPriv(seed)
  4649  			if err != nil {
  4650  				return false, fmt.Errorf("GenDeepChild error: %w", err)
  4651  			}
  4652  			c.loggedIn = true
  4653  			return true, nil
  4654  		}
  4655  		return false, nil
  4656  	}
  4657  
  4658  	if needsInit, err := login(); err != nil {
  4659  		return err
  4660  	} else if needsInit {
  4661  		// It is not an error if we can't connect, unless we need the wallet
  4662  		// for active trades, but that condition is checked later in
  4663  		// resolveActiveTrades. We won't try to unlock here, but if the wallet
  4664  		// is needed for active trades, it will be unlocked in resolveActiveTrades
  4665  		// and the balance updated there.
  4666  		c.notify(newLoginNote("Connecting wallets..."))
  4667  		c.connectWallets(crypter) // initialize reserves
  4668  		c.notify(newLoginNote("Resuming active trades..."))
  4669  		c.resolveActiveTrades(crypter)
  4670  		c.notify(newLoginNote("Connecting to DEX servers..."))
  4671  		c.initializeDEXConnections(crypter)
  4672  	}
  4673  
  4674  	return nil
  4675  }
  4676  
  4677  // upgradeV0CredsToV1 upgrades version 0 credentials to version 1. This update
  4678  // changes the inner key to be derived from the seed.
  4679  func (c *Core) upgradeV0CredsToV1(appPW []byte, creds db.PrimaryCredentials) (encrypt.Crypter, *db.PrimaryCredentials, error) {
  4680  	outerCrypter, err := c.reCrypter(appPW, creds.OuterKeyParams)
  4681  	if err != nil {
  4682  		return nil, nil, fmt.Errorf("app password error: %w", err)
  4683  	}
  4684  	innerKey, err := outerCrypter.Decrypt(creds.EncInnerKey)
  4685  	if err != nil {
  4686  		return nil, nil, fmt.Errorf("inner key decryption error: %w", err)
  4687  	}
  4688  	innerCrypter, err := c.reCrypter(innerKey, creds.InnerKeyParams)
  4689  	if err != nil {
  4690  		return nil, nil, fmt.Errorf("inner key deserialization error: %w", err)
  4691  	}
  4692  	seed, err := innerCrypter.Decrypt(creds.EncSeed)
  4693  	if err != nil {
  4694  		return nil, nil, fmt.Errorf("app seed decryption error: %w", err)
  4695  	}
  4696  
  4697  	// Update all the fields.
  4698  	newInnerKey := seedInnerKey(seed)
  4699  	newInnerCrypter := c.newCrypter(newInnerKey[:])
  4700  	creds.Version = 1
  4701  	creds.InnerKeyParams = newInnerCrypter.Serialize()
  4702  	if creds.EncSeed, err = newInnerCrypter.Encrypt(seed); err != nil {
  4703  		return nil, nil, fmt.Errorf("error encrypting version 1 seed: %w", err)
  4704  	}
  4705  	if creds.EncInnerKey, err = outerCrypter.Encrypt(newInnerKey[:]); err != nil {
  4706  		return nil, nil, fmt.Errorf("error encrypting version 1 inner key: %w", err)
  4707  	}
  4708  	if err := c.recrypt(&creds, innerCrypter, newInnerCrypter); err != nil {
  4709  		return nil, nil, fmt.Errorf("recrypt error during v0 -> v1 credentials upgrade: %w", err)
  4710  	}
  4711  
  4712  	c.log.Infof("Upgraded to version 1 credentials")
  4713  	return newInnerCrypter, &creds, nil
  4714  }
  4715  
  4716  // connectWallets attempts to connect to and retrieve balance from all known
  4717  // wallets. This should be done only ONCE on Login.
  4718  func (c *Core) connectWallets(crypter encrypt.Crypter) {
  4719  	var wg sync.WaitGroup
  4720  	var connectCount uint32
  4721  	connectWallet := func(wallet *xcWallet) {
  4722  		defer wg.Done()
  4723  		// Return early if wallet is disabled.
  4724  		if wallet.isDisabled() {
  4725  			return
  4726  		}
  4727  		if !wallet.connected() {
  4728  			err := c.connectAndUpdateWallet(wallet)
  4729  			if err != nil {
  4730  				c.log.Errorf("Unable to connect to %s wallet (start and sync wallets BEFORE starting dex!): %v",
  4731  					unbip(wallet.AssetID), err)
  4732  				// NOTE: Details for this topic is in the context of fee
  4733  				// payment, but the subject pertains to a failure to connect
  4734  				// to the wallet.
  4735  				subject, _ := c.formatDetails(TopicWalletConnectionWarning)
  4736  				c.notify(newWalletConfigNote(TopicWalletConnectionWarning, subject, err.Error(),
  4737  					db.ErrorLevel, wallet.state()))
  4738  				return
  4739  			}
  4740  			if mw, is := wallet.Wallet.(asset.FundsMixer); is {
  4741  				startMixing := func() error {
  4742  					stats, err := mw.FundsMixingStats()
  4743  					if err != nil {
  4744  						return fmt.Errorf("error checking %s wallet mixing stats: %v", unbip(wallet.AssetID), err)
  4745  					}
  4746  					// If the wallet has no funds to transfer to the default account
  4747  					// and mixing is not enabled unlocking is not required.
  4748  					if !stats.Enabled && stats.MixedFunds == 0 && stats.TradingFunds == 0 {
  4749  						return nil
  4750  					}
  4751  					// Unlocking is required for mixing or to move funds if mixing
  4752  					// was recently turned off without funds being moved yet.
  4753  					if err := c.connectAndUnlock(crypter, wallet); err != nil {
  4754  						return fmt.Errorf("error unlocking %s wallet for mixing: %v", unbip(wallet.AssetID), err)
  4755  					}
  4756  					if err := mw.ConfigureFundsMixer(stats.Enabled); err != nil {
  4757  						return fmt.Errorf("error starting %s wallet mixing: %v", unbip(wallet.AssetID), err)
  4758  					}
  4759  					return nil
  4760  				}
  4761  				if err := startMixing(); err != nil {
  4762  					c.log.Errorf("Failed to start or stop mixing: %v", err)
  4763  				}
  4764  			}
  4765  			if c.cfg.UnlockCoinsOnLogin {
  4766  				if err = wallet.ReturnCoins(nil); err != nil {
  4767  					c.log.Errorf("Failed to unlock all %s wallet coins: %v", unbip(wallet.AssetID), err)
  4768  				}
  4769  			}
  4770  		}
  4771  		atomic.AddUint32(&connectCount, 1)
  4772  	}
  4773  	wallets := c.xcWallets()
  4774  	walletCount := len(wallets)
  4775  	var tokenWallets []*xcWallet
  4776  
  4777  	for _, wallet := range wallets {
  4778  		if asset.TokenInfo(wallet.AssetID) != nil {
  4779  			tokenWallets = append(tokenWallets, wallet)
  4780  			continue
  4781  		}
  4782  		wg.Add(1)
  4783  		go connectWallet(wallet)
  4784  	}
  4785  	wg.Wait()
  4786  
  4787  	for _, wallet := range tokenWallets {
  4788  		wg.Add(1)
  4789  		go connectWallet(wallet)
  4790  	}
  4791  	wg.Wait()
  4792  
  4793  	if walletCount > 0 {
  4794  		c.log.Infof("Connected to %d of %d wallets.", connectCount, walletCount)
  4795  	}
  4796  }
  4797  
  4798  // Notifications loads the latest notifications from the db.
  4799  func (c *Core) Notifications(n int) (notes, pokes []*db.Notification, _ error) {
  4800  	notes, err := c.db.NotificationsN(n)
  4801  	if err != nil {
  4802  		return nil, nil, fmt.Errorf("error getting notifications: %w", err)
  4803  	}
  4804  	return notes, c.pokes(), nil
  4805  }
  4806  
  4807  // pokes returns a time-ordered copy of the pokes cache.
  4808  func (c *Core) pokes() []*db.Notification {
  4809  	return c.pokesCache.pokes()
  4810  }
  4811  
  4812  func (c *Core) recrypt(creds *db.PrimaryCredentials, oldCrypter, newCrypter encrypt.Crypter) error {
  4813  	walletUpdates, acctUpdates, err := c.db.Recrypt(creds, oldCrypter, newCrypter)
  4814  	if err != nil {
  4815  		return err
  4816  	}
  4817  
  4818  	c.setCredentials(creds)
  4819  
  4820  	for assetID, newEncPW := range walletUpdates {
  4821  		w, found := c.wallet(assetID)
  4822  		if !found {
  4823  			c.log.Errorf("no wallet found for v1 upgrade asset ID %d", assetID)
  4824  			continue
  4825  		}
  4826  		w.setEncPW(newEncPW)
  4827  	}
  4828  
  4829  	for host, newEncKey := range acctUpdates {
  4830  		dc, _, err := c.dex(host)
  4831  		if err != nil {
  4832  			c.log.Warnf("no %s dexConnection to update", host)
  4833  			continue
  4834  		}
  4835  		acct := dc.acct
  4836  		acct.keyMtx.Lock()
  4837  		acct.encKey = newEncKey
  4838  		acct.keyMtx.Unlock()
  4839  	}
  4840  
  4841  	return nil
  4842  }
  4843  
  4844  // initializePrimaryCredentials sets the PrimaryCredential fields after the DB
  4845  // upgrade.
  4846  func (c *Core) initializePrimaryCredentials(pw []byte, oldKeyParams []byte) error {
  4847  	oldCrypter, err := c.reCrypter(pw, oldKeyParams)
  4848  	if err != nil {
  4849  		return fmt.Errorf("legacy encryption key deserialization error: %w", err)
  4850  	}
  4851  
  4852  	newCrypter, creds, _, err := c.generateCredentials(pw, nil)
  4853  	if err != nil {
  4854  		return err
  4855  	}
  4856  
  4857  	if err := c.recrypt(creds, oldCrypter, newCrypter); err != nil {
  4858  		return err
  4859  	}
  4860  
  4861  	subject, details := c.formatDetails(TopicUpgradedToSeed)
  4862  	c.notify(newSecurityNote(TopicUpgradedToSeed, subject, details, db.WarningLevel))
  4863  	return nil
  4864  }
  4865  
  4866  // ActiveOrders returns a map of host to all of their active orders from db if
  4867  // core is not yet logged in or from loaded trades map if core is logged in.
  4868  // Inflight orders are also returned for all dex servers if any.
  4869  func (c *Core) ActiveOrders() (map[string][]*Order, map[string][]*InFlightOrder, error) {
  4870  	c.loginMtx.Lock()
  4871  	loggedIn := c.loggedIn
  4872  	c.loginMtx.Unlock()
  4873  
  4874  	dexInflightOrders := make(map[string][]*InFlightOrder)
  4875  	dexActiveOrders := make(map[string][]*Order)
  4876  	for _, dc := range c.dexConnections() {
  4877  		if loggedIn {
  4878  			orders, inflight := dc.activeOrders()
  4879  			dexActiveOrders[dc.acct.host] = append(dexActiveOrders[dc.acct.host], orders...)
  4880  			dexInflightOrders[dc.acct.host] = append(dexInflightOrders[dc.acct.host], inflight...)
  4881  			continue
  4882  		}
  4883  
  4884  		// Not logged in, load from db orders.
  4885  		ords, err := c.dbOrders(dc.acct.host)
  4886  		if err != nil {
  4887  			return nil, nil, err
  4888  		}
  4889  
  4890  		for _, ord := range ords {
  4891  			dexActiveOrders[dc.acct.host] = append(dexActiveOrders[dc.acct.host], coreOrderFromTrade(ord.Order, ord.MetaData))
  4892  		}
  4893  	}
  4894  
  4895  	return dexActiveOrders, dexInflightOrders, nil
  4896  }
  4897  
  4898  // Active indicates if there are any active orders across all configured
  4899  // accounts. This includes booked orders and trades that are settling.
  4900  func (c *Core) Active() bool {
  4901  	for _, dc := range c.dexConnections() {
  4902  		if dc.hasActiveOrders() {
  4903  			return true
  4904  		}
  4905  	}
  4906  	return false
  4907  }
  4908  
  4909  // Logout logs the user out
  4910  func (c *Core) Logout() error {
  4911  	c.loginMtx.Lock()
  4912  	defer c.loginMtx.Unlock()
  4913  
  4914  	if !c.loggedIn {
  4915  		return nil
  4916  	}
  4917  
  4918  	// Check active orders
  4919  	if c.Active() {
  4920  		return codedError(activeOrdersErr, ActiveOrdersLogoutErr)
  4921  	}
  4922  
  4923  	// Lock wallets
  4924  	if !c.cfg.NoAutoWalletLock {
  4925  		// Ensure wallet lock in c.Run waits for c.Logout if this is called
  4926  		// before shutdown.
  4927  		c.wg.Add(1)
  4928  		for _, w := range c.xcWallets() {
  4929  			if w.connected() && w.unlocked() {
  4930  				symb := strings.ToUpper(unbip(w.AssetID))
  4931  				c.log.Infof("Locking %s wallet", symb)
  4932  				if err := w.Lock(walletLockTimeout); err != nil {
  4933  					// A failure to lock the wallet need not block the ability to
  4934  					// lock the DEX accounts or shutdown Core gracefully.
  4935  					c.log.Warnf("Unable to lock %v wallet: %v", unbip(w.AssetID), err)
  4936  				}
  4937  			}
  4938  		}
  4939  		c.wg.Done()
  4940  	}
  4941  
  4942  	// With no open orders for any of the dex connections, and all wallets locked,
  4943  	// lock each dex account.
  4944  	for _, dc := range c.dexConnections() {
  4945  		dc.acct.lock()
  4946  	}
  4947  
  4948  	c.bondXPriv.Zero()
  4949  	c.bondXPriv = nil
  4950  
  4951  	c.loggedIn = false
  4952  
  4953  	return nil
  4954  }
  4955  
  4956  // Orders fetches a batch of user orders, filtered with the provided
  4957  // OrderFilter.
  4958  func (c *Core) Orders(filter *OrderFilter) ([]*Order, error) {
  4959  	var oid order.OrderID
  4960  	if len(filter.Offset) > 0 {
  4961  		if len(filter.Offset) != order.OrderIDSize {
  4962  			return nil, fmt.Errorf("invalid offset order ID length. wanted %d, got %d", order.OrderIDSize, len(filter.Offset))
  4963  		}
  4964  		copy(oid[:], filter.Offset)
  4965  	}
  4966  
  4967  	var mkt *db.OrderFilterMarket
  4968  	if filter.Market != nil {
  4969  		mkt = &db.OrderFilterMarket{
  4970  			Base:  filter.Market.Base,
  4971  			Quote: filter.Market.Quote,
  4972  		}
  4973  	}
  4974  
  4975  	ords, err := c.db.Orders(&db.OrderFilter{
  4976  		N:        filter.N,
  4977  		Offset:   oid,
  4978  		Hosts:    filter.Hosts,
  4979  		Assets:   filter.Assets,
  4980  		Market:   mkt,
  4981  		Statuses: filter.Statuses,
  4982  	})
  4983  	if err != nil {
  4984  		return nil, fmt.Errorf("UserOrders error: %w", err)
  4985  	}
  4986  
  4987  	cords := make([]*Order, 0, len(ords))
  4988  	for _, mOrd := range ords {
  4989  		corder, err := c.coreOrderFromMetaOrder(mOrd)
  4990  		if err != nil {
  4991  			return nil, err
  4992  		}
  4993  		baseWallet, baseOK := c.wallet(corder.BaseID)
  4994  		quoteWallet, quoteOK := c.wallet(corder.QuoteID)
  4995  		corder.ReadyToTick = baseOK && baseWallet.connected() && baseWallet.unlocked() &&
  4996  			quoteOK && quoteWallet.connected() && quoteWallet.unlocked()
  4997  		cords = append(cords, corder)
  4998  	}
  4999  
  5000  	return cords, nil
  5001  }
  5002  
  5003  // coreOrderFromMetaOrder creates an *Order from a *db.MetaOrder, including
  5004  // loading matches from the database. The order is presumed to be inactive, so
  5005  // swap coin confirmations will not be set. For active orders, get the
  5006  // *trackedTrade and use the coreOrder method.
  5007  func (c *Core) coreOrderFromMetaOrder(mOrd *db.MetaOrder) (*Order, error) {
  5008  	corder := coreOrderFromTrade(mOrd.Order, mOrd.MetaData)
  5009  	oid := mOrd.Order.ID()
  5010  	excludeCancels := false // maybe don't include cancel order matches?
  5011  	matches, err := c.db.MatchesForOrder(oid, excludeCancels)
  5012  	if err != nil {
  5013  		return nil, fmt.Errorf("MatchesForOrder error loading matches for %s: %w", oid, err)
  5014  	}
  5015  	corder.Matches = make([]*Match, 0, len(matches))
  5016  	for _, match := range matches {
  5017  		corder.Matches = append(corder.Matches, matchFromMetaMatch(mOrd.Order, match))
  5018  	}
  5019  	return corder, nil
  5020  }
  5021  
  5022  // Order fetches a single user order.
  5023  func (c *Core) Order(oidB dex.Bytes) (*Order, error) {
  5024  	oid, err := order.IDFromBytes(oidB)
  5025  	if err != nil {
  5026  		return nil, err
  5027  	}
  5028  	// See if it's an active order first.
  5029  	for _, dc := range c.dexConnections() {
  5030  		tracker, _ := dc.findOrder(oid)
  5031  		if tracker != nil {
  5032  			return tracker.coreOrder(), nil
  5033  		}
  5034  	}
  5035  	// Must not be an active order. Get it from the database.
  5036  	mOrd, err := c.db.Order(oid)
  5037  	if err != nil {
  5038  		return nil, fmt.Errorf("error retrieving order %s: %w", oid, err)
  5039  	}
  5040  
  5041  	return c.coreOrderFromMetaOrder(mOrd)
  5042  }
  5043  
  5044  // marketWallets gets the 2 *dex.Assets and 2 *xcWallet associated with a
  5045  // market. The wallets will be connected, but not necessarily unlocked.
  5046  func (c *Core) marketWallets(host string, base, quote uint32) (ba, qa *dex.Asset, bw, qw *xcWallet, err error) {
  5047  	c.connMtx.RLock()
  5048  	dc, found := c.conns[host]
  5049  	c.connMtx.RUnlock()
  5050  	if !found {
  5051  		return nil, nil, nil, nil, fmt.Errorf("Unknown host: %s", host)
  5052  	}
  5053  
  5054  	ba, found = dc.assets[base]
  5055  	if !found {
  5056  		return nil, nil, nil, nil, fmt.Errorf("%s not supported by %s", unbip(base), host)
  5057  	}
  5058  	qa, found = dc.assets[quote]
  5059  	if !found {
  5060  		return nil, nil, nil, nil, fmt.Errorf("%s not supported by %s", unbip(quote), host)
  5061  	}
  5062  
  5063  	bw, err = c.connectedWallet(base)
  5064  	if err != nil {
  5065  		return nil, nil, nil, nil, fmt.Errorf("%s wallet error: %v", unbip(base), err)
  5066  	}
  5067  	qw, err = c.connectedWallet(quote)
  5068  	if err != nil {
  5069  		return nil, nil, nil, nil, fmt.Errorf("%s wallet error: %v", unbip(quote), err)
  5070  	}
  5071  	return
  5072  }
  5073  
  5074  // MaxBuy is the maximum-sized *OrderEstimate for a buy order on the specified
  5075  // market. An order rate must be provided, since the number of lots available
  5076  // for trading will vary based on the rate for a buy order (unlike a sell
  5077  // order).
  5078  func (c *Core) MaxBuy(host string, baseID, quoteID uint32, rate uint64) (*MaxOrderEstimate, error) {
  5079  	baseAsset, quoteAsset, baseWallet, quoteWallet, err := c.marketWallets(host, baseID, quoteID)
  5080  	if err != nil {
  5081  		return nil, err
  5082  	}
  5083  
  5084  	dc, _, err := c.dex(host)
  5085  	if err != nil {
  5086  		return nil, err
  5087  	}
  5088  
  5089  	mktID := marketName(baseID, quoteID)
  5090  	mktConf := dc.marketConfig(mktID)
  5091  	if mktConf == nil {
  5092  		return nil, newError(marketErr, "unknown market %q", mktID)
  5093  	}
  5094  
  5095  	lotSize := mktConf.LotSize
  5096  	quoteLotEst := calc.BaseToQuote(rate, lotSize)
  5097  	if quoteLotEst == 0 {
  5098  		return nil, fmt.Errorf("quote lot estimate of zero for market %s", mktID)
  5099  	}
  5100  
  5101  	swapFeeSuggestion := c.feeSuggestion(dc, quoteID)
  5102  	if swapFeeSuggestion == 0 {
  5103  		return nil, fmt.Errorf("failed to get swap fee suggestion for %s at %s", unbip(quoteID), host)
  5104  	}
  5105  
  5106  	redeemFeeSuggestion := c.feeSuggestionAny(baseID)
  5107  	if redeemFeeSuggestion == 0 {
  5108  		return nil, fmt.Errorf("failed to get redeem fee suggestion for %s at %s", unbip(baseID), host)
  5109  	}
  5110  
  5111  	maxBuy, err := quoteWallet.MaxOrder(&asset.MaxOrderForm{
  5112  		LotSize:       quoteLotEst,
  5113  		FeeSuggestion: swapFeeSuggestion,
  5114  		AssetVersion:  quoteAsset.Version, // using the server's asset version, when our wallets support multiple vers
  5115  		MaxFeeRate:    quoteAsset.MaxFeeRate,
  5116  		RedeemVersion: baseAsset.Version,
  5117  		RedeemAssetID: baseWallet.AssetID,
  5118  	})
  5119  	if err != nil {
  5120  		return nil, fmt.Errorf("%s wallet MaxOrder error: %v", unbip(quoteID), err)
  5121  	}
  5122  
  5123  	preRedeem, err := baseWallet.PreRedeem(&asset.PreRedeemForm{
  5124  		Version:       baseAsset.Version,
  5125  		Lots:          maxBuy.Lots,
  5126  		FeeSuggestion: redeemFeeSuggestion,
  5127  	})
  5128  	if err != nil {
  5129  		return nil, fmt.Errorf("%s PreRedeem error: %v", unbip(baseID), err)
  5130  	}
  5131  
  5132  	return &MaxOrderEstimate{
  5133  		Swap:   maxBuy,
  5134  		Redeem: preRedeem.Estimate,
  5135  	}, nil
  5136  }
  5137  
  5138  // MaxSell is the maximum-sized *OrderEstimate for a sell order on the specified
  5139  // market.
  5140  func (c *Core) MaxSell(host string, base, quote uint32) (*MaxOrderEstimate, error) {
  5141  	baseAsset, quoteAsset, baseWallet, quoteWallet, err := c.marketWallets(host, base, quote)
  5142  	if err != nil {
  5143  		return nil, err
  5144  	}
  5145  
  5146  	dc, _, err := c.dex(host)
  5147  	if err != nil {
  5148  		return nil, err
  5149  	}
  5150  	mktID := marketName(base, quote)
  5151  	mktConf := dc.marketConfig(mktID)
  5152  	if mktConf == nil {
  5153  		return nil, newError(marketErr, "unknown market %q", mktID)
  5154  	}
  5155  	lotSize := mktConf.LotSize
  5156  	if lotSize == 0 {
  5157  		return nil, fmt.Errorf("cannot divide by lot size zero for max sell estimate on market %s", mktID)
  5158  	}
  5159  
  5160  	swapFeeSuggestion := c.feeSuggestion(dc, base)
  5161  	if swapFeeSuggestion == 0 {
  5162  		return nil, fmt.Errorf("failed to get swap fee suggestion for %s at %s", unbip(base), host)
  5163  	}
  5164  
  5165  	redeemFeeSuggestion := c.feeSuggestionAny(quote)
  5166  	if redeemFeeSuggestion == 0 {
  5167  		return nil, fmt.Errorf("failed to get redeem fee suggestion for %s at %s", unbip(quote), host)
  5168  	}
  5169  
  5170  	maxSell, err := baseWallet.MaxOrder(&asset.MaxOrderForm{
  5171  		LotSize:       lotSize,
  5172  		FeeSuggestion: swapFeeSuggestion,
  5173  		AssetVersion:  baseAsset.Version, // using the server's asset version, when our wallets support multiple vers
  5174  		MaxFeeRate:    baseAsset.MaxFeeRate,
  5175  		RedeemVersion: quoteAsset.Version,
  5176  		RedeemAssetID: quoteWallet.AssetID,
  5177  	})
  5178  	if err != nil {
  5179  		return nil, fmt.Errorf("%s wallet MaxOrder error: %v", unbip(base), err)
  5180  	}
  5181  
  5182  	preRedeem, err := quoteWallet.PreRedeem(&asset.PreRedeemForm{
  5183  		Version:       quoteAsset.Version,
  5184  		Lots:          maxSell.Lots,
  5185  		FeeSuggestion: redeemFeeSuggestion,
  5186  	})
  5187  	if err != nil {
  5188  		return nil, fmt.Errorf("%s PreRedeem error: %v", unbip(quote), err)
  5189  	}
  5190  
  5191  	return &MaxOrderEstimate{
  5192  		Swap:   maxSell,
  5193  		Redeem: preRedeem.Estimate,
  5194  	}, nil
  5195  }
  5196  
  5197  // initializeDEXConnections connects to the DEX servers in the conns map and
  5198  // authenticates the connection.
  5199  func (c *Core) initializeDEXConnections(crypter encrypt.Crypter) {
  5200  	var wg sync.WaitGroup
  5201  	conns := c.dexConnections()
  5202  	for _, dc := range conns {
  5203  		wg.Add(1)
  5204  		go func(dc *dexConnection) {
  5205  			defer wg.Done()
  5206  			c.initializeDEXConnection(dc, crypter)
  5207  		}(dc)
  5208  	}
  5209  
  5210  	wg.Wait()
  5211  }
  5212  
  5213  // initializeDEXConnection connects to the DEX server in the conns map and
  5214  // authenticates the connection.
  5215  func (c *Core) initializeDEXConnection(dc *dexConnection, crypter encrypt.Crypter) {
  5216  	if dc.acct.isViewOnly() {
  5217  		return // don't attempt authDEX for view-only conn
  5218  	}
  5219  
  5220  	// Unlock before checking auth and continuing, because if the user
  5221  	// logged out and didn't shut down, the account is still authed, but
  5222  	// locked, and needs unlocked.
  5223  	err := dc.acct.unlock(crypter)
  5224  	if err != nil {
  5225  		subject, details := c.formatDetails(TopicAccountUnlockError, dc.acct.host, err)
  5226  		c.notify(newFeePaymentNote(TopicAccountUnlockError, subject, details, db.ErrorLevel, dc.acct.host)) // newDEXAuthNote?
  5227  		return
  5228  	}
  5229  
  5230  	if dc.acct.isDisabled() {
  5231  		return // For disabled account, we only want dc.acct.unlock above to initialize the account ID.
  5232  	}
  5233  
  5234  	// Unlock the bond wallet if a target tier is set.
  5235  	if bondAssetID, targetTier, maxBondedAmt := dc.bondOpts(); targetTier > 0 {
  5236  		c.log.Debugf("Preparing %s wallet to maintain target tier of %d for %v, bonding limit %v",
  5237  			unbip(bondAssetID), targetTier, dc.acct.host, maxBondedAmt)
  5238  		wallet, exists := c.wallet(bondAssetID)
  5239  		if !exists || !wallet.connected() { // connectWallets already run, just fail
  5240  			subject, details := c.formatDetails(TopicBondWalletNotConnected, unbip(bondAssetID))
  5241  			var w *WalletState
  5242  			if exists {
  5243  				w = wallet.state()
  5244  			}
  5245  			c.notify(newWalletConfigNote(TopicBondWalletNotConnected, subject, details, db.ErrorLevel, w))
  5246  		} else if !wallet.unlocked() {
  5247  			err = wallet.Unlock(crypter)
  5248  			if err != nil {
  5249  				subject, details := c.formatDetails(TopicWalletUnlockError, dc.acct.host, err)
  5250  				c.notify(newFeePaymentNote(TopicWalletUnlockError, subject, details, db.ErrorLevel, dc.acct.host))
  5251  			}
  5252  		}
  5253  	}
  5254  
  5255  	if dc.acct.authed() { // should not be possible with newly idempotent login, but there's AccountImport...
  5256  		return // authDEX already done
  5257  	}
  5258  
  5259  	// Pending bonds will be handled by authDEX. Expired bonds will be
  5260  	// refunded by rotateBonds.
  5261  
  5262  	// If the connection is down, authDEX will fail on Send.
  5263  	if dc.IsDown() {
  5264  		c.log.Warnf("Connection to %v not available for authorization. "+
  5265  			"It will automatically authorize when it connects.", dc.acct.host)
  5266  		subject, details := c.formatDetails(TopicDEXDisconnected, dc.acct.host)
  5267  		c.notify(newConnEventNote(TopicDEXDisconnected, subject, dc.acct.host, comms.Disconnected, details, db.ErrorLevel))
  5268  		return
  5269  	}
  5270  
  5271  	// Authenticate dex connection
  5272  	err = c.authDEX(dc)
  5273  	if err != nil {
  5274  		subject, details := c.formatDetails(TopicDexAuthError, dc.acct.host, err)
  5275  		c.notify(newDEXAuthNote(TopicDexAuthError, subject, dc.acct.host, false, details, db.ErrorLevel))
  5276  	}
  5277  }
  5278  
  5279  // resolveActiveTrades loads order and match data from the database. Only active
  5280  // orders and orders with active matches are loaded. Also, only active matches
  5281  // are loaded, even if there are inactive matches for the same order, but it may
  5282  // be desirable to load all matches, so this behavior may change.
  5283  func (c *Core) resolveActiveTrades(crypter encrypt.Crypter) {
  5284  	for _, dc := range c.dexConnections() {
  5285  		err := c.loadDBTrades(dc)
  5286  		if err != nil {
  5287  			c.log.Errorf("failed to load trades from db for dex at %s: %v", dc.acct.host, err)
  5288  		}
  5289  	}
  5290  
  5291  	// resumeTrades will be a no-op if there are no trades in any
  5292  	// dexConnection's trades map that is not ready to tick.
  5293  	c.resumeTrades(crypter)
  5294  }
  5295  
  5296  func (c *Core) wait(coinID []byte, assetID uint32, trigger func() (bool, error), action func(error)) {
  5297  	c.waiterMtx.Lock()
  5298  	defer c.waiterMtx.Unlock()
  5299  	c.blockWaiters[coinIDString(assetID, coinID)] = &blockWaiter{
  5300  		assetID: assetID,
  5301  		trigger: trigger,
  5302  		action:  action,
  5303  	}
  5304  }
  5305  
  5306  func (c *Core) waiting(coinID []byte, assetID uint32) bool {
  5307  	c.waiterMtx.RLock()
  5308  	defer c.waiterMtx.RUnlock()
  5309  	_, found := c.blockWaiters[coinIDString(assetID, coinID)]
  5310  	return found
  5311  }
  5312  
  5313  // removeWaiter removes a blockWaiter from the map.
  5314  func (c *Core) removeWaiter(id string) {
  5315  	c.waiterMtx.Lock()
  5316  	delete(c.blockWaiters, id)
  5317  	c.waiterMtx.Unlock()
  5318  }
  5319  
  5320  // feeSuggestionAny gets a fee suggestion for the given asset from any source
  5321  // with it available. It first checks for a capable wallet, then relevant books
  5322  // for a cached fee rate obtained with an epoch_report message, and falls back
  5323  // to directly requesting a rate from servers with a fee_rate request.
  5324  func (c *Core) feeSuggestionAny(assetID uint32, preferredConns ...*dexConnection) uint64 {
  5325  	// See if the wallet supports fee rates.
  5326  	w, found := c.wallet(assetID)
  5327  	if found && w.connected() {
  5328  		if r := w.feeRate(); r != 0 {
  5329  			return r
  5330  		}
  5331  	}
  5332  
  5333  	// Look for cached rates from epoch_report messages.
  5334  	conns := append(preferredConns, c.dexConnections()...)
  5335  	for _, dc := range conns {
  5336  		feeSuggestion := dc.bestBookFeeSuggestion(assetID)
  5337  		if feeSuggestion > 0 {
  5338  			return feeSuggestion
  5339  		}
  5340  	}
  5341  
  5342  	// Helper function to determine if a server has an active market that pairs
  5343  	// the requested asset.
  5344  	hasActiveMarket := func(dc *dexConnection) bool {
  5345  		dc.cfgMtx.RLock()
  5346  		cfg := dc.cfg
  5347  		dc.cfgMtx.RUnlock()
  5348  		if cfg == nil {
  5349  			return false
  5350  		}
  5351  		for _, mkt := range cfg.Markets {
  5352  			if mkt.Base == assetID || mkt.Quote == assetID && mkt.Running() {
  5353  				return true
  5354  			}
  5355  		}
  5356  		return false
  5357  	}
  5358  
  5359  	// Request a rate with fee_rate.
  5360  	for _, dc := range conns {
  5361  		// The server should have at least one active market with the asset,
  5362  		// otherwise we might get an outdated rate for an asset whose backend
  5363  		// might be supported but not in active use, e.g. down for maintenance.
  5364  		// The fee_rate endpoint will happily return a very old rate without
  5365  		// indication.
  5366  		if !hasActiveMarket(dc) {
  5367  			continue
  5368  		}
  5369  
  5370  		feeSuggestion := dc.fetchFeeRate(assetID)
  5371  		if feeSuggestion > 0 {
  5372  			return feeSuggestion
  5373  		}
  5374  	}
  5375  	return 0
  5376  }
  5377  
  5378  // feeSuggestion gets the best fee suggestion, first from a synced order book,
  5379  // and if not synced, directly from the server.
  5380  func (c *Core) feeSuggestion(dc *dexConnection, assetID uint32) (feeSuggestion uint64) {
  5381  	// Prepare a fee suggestion based on the last reported fee rate in the
  5382  	// order book feed.
  5383  	feeSuggestion = dc.bestBookFeeSuggestion(assetID)
  5384  	if feeSuggestion > 0 {
  5385  		return
  5386  	}
  5387  	return dc.fetchFeeRate(assetID)
  5388  }
  5389  
  5390  // Send initiates either send or withdraw from an exchange wallet. if subtract
  5391  // is true, fees are subtracted from the value else fees are taken from the
  5392  // exchange wallet.
  5393  func (c *Core) Send(pw []byte, assetID uint32, value uint64, address string, subtract bool) (asset.Coin, error) {
  5394  	var crypter encrypt.Crypter
  5395  	// Empty password can be provided if wallet is already unlocked. Webserver
  5396  	// and RPCServer should not allow empty password, but this is used for
  5397  	// bots.
  5398  	if len(pw) > 0 {
  5399  		var err error
  5400  		crypter, err = c.encryptionKey(pw)
  5401  		if err != nil {
  5402  			return nil, fmt.Errorf("Trade password error: %w", err)
  5403  		}
  5404  		defer crypter.Close()
  5405  	}
  5406  
  5407  	if value == 0 {
  5408  		return nil, fmt.Errorf("cannot send/withdraw zero %s", unbip(assetID))
  5409  	}
  5410  	wallet, found := c.wallet(assetID)
  5411  	if !found {
  5412  		return nil, newError(missingWalletErr, "no wallet found for %s", unbip(assetID))
  5413  	}
  5414  	err := c.connectAndUnlock(crypter, wallet)
  5415  	if err != nil {
  5416  		return nil, err
  5417  	}
  5418  
  5419  	if err = wallet.checkPeersAndSyncStatus(); err != nil {
  5420  		return nil, err
  5421  	}
  5422  
  5423  	var coin asset.Coin
  5424  	feeSuggestion := c.feeSuggestionAny(assetID)
  5425  	if !subtract {
  5426  		coin, err = wallet.Wallet.Send(address, value, feeSuggestion)
  5427  	} else {
  5428  		if withdrawer, isWithdrawer := wallet.Wallet.(asset.Withdrawer); isWithdrawer {
  5429  			coin, err = withdrawer.Withdraw(address, value, feeSuggestion)
  5430  		} else {
  5431  			return nil, fmt.Errorf("wallet does not support subtracting network fee from withdraw amount")
  5432  		}
  5433  	}
  5434  	if err != nil {
  5435  		subject, details := c.formatDetails(TopicSendError, unbip(assetID), err)
  5436  		c.notify(newSendNote(TopicSendError, subject, details, db.ErrorLevel))
  5437  		return nil, err
  5438  	}
  5439  
  5440  	sentValue := wallet.Info().UnitInfo.ConventionalString(coin.Value())
  5441  	subject, details := c.formatDetails(TopicSendSuccess, sentValue, unbip(assetID), address, coin)
  5442  	c.notify(newSendNote(TopicSendSuccess, subject, details, db.Success))
  5443  
  5444  	c.updateAssetBalance(assetID)
  5445  
  5446  	return coin, nil
  5447  }
  5448  
  5449  // ValidateAddress checks that the provided address is valid.
  5450  func (c *Core) ValidateAddress(address string, assetID uint32) (bool, error) {
  5451  	if address == "" {
  5452  		return false, nil
  5453  	}
  5454  	wallet, found := c.wallet(assetID)
  5455  	if !found {
  5456  		return false, newError(missingWalletErr, "no wallet found for %s", unbip(assetID))
  5457  	}
  5458  	return wallet.Wallet.ValidateAddress(address), nil
  5459  }
  5460  
  5461  // ApproveToken calls a wallet's ApproveToken method. It approves the version
  5462  // of the token used by the dex at the specified address.
  5463  func (c *Core) ApproveToken(appPW []byte, assetID uint32, dexAddr string, onConfirm func()) (string, error) {
  5464  	crypter, err := c.encryptionKey(appPW)
  5465  	if err != nil {
  5466  		return "", err
  5467  	}
  5468  
  5469  	wallet, err := c.connectedWallet(assetID)
  5470  	if err != nil {
  5471  		return "", err
  5472  	}
  5473  
  5474  	err = wallet.Unlock(crypter)
  5475  	if err != nil {
  5476  		return "", err
  5477  	}
  5478  
  5479  	err = wallet.checkPeersAndSyncStatus()
  5480  	if err != nil {
  5481  		return "", err
  5482  	}
  5483  
  5484  	dex, connected, err := c.dex(dexAddr)
  5485  	if err != nil {
  5486  		return "", err
  5487  	}
  5488  	if !connected {
  5489  		return "", fmt.Errorf("not connected to %s", dexAddr)
  5490  	}
  5491  
  5492  	asset, found := dex.assets[assetID]
  5493  	if !found {
  5494  		return "", fmt.Errorf("asset %d not found for %s", assetID, dexAddr)
  5495  	}
  5496  
  5497  	walletOnConfirm := func() {
  5498  		go onConfirm()
  5499  		go c.notify(newTokenApprovalNote(wallet.state()))
  5500  	}
  5501  
  5502  	txID, err := wallet.ApproveToken(asset.Version, walletOnConfirm)
  5503  	if err != nil {
  5504  		return "", err
  5505  	}
  5506  
  5507  	c.notify(newTokenApprovalNote(wallet.state()))
  5508  	return txID, nil
  5509  }
  5510  
  5511  // UnapproveToken calls a wallet's UnapproveToken method for a specified
  5512  // version of the token.
  5513  func (c *Core) UnapproveToken(appPW []byte, assetID uint32, version uint32) (string, error) {
  5514  	crypter, err := c.encryptionKey(appPW)
  5515  	if err != nil {
  5516  		return "", err
  5517  	}
  5518  
  5519  	wallet, err := c.connectedWallet(assetID)
  5520  	if err != nil {
  5521  		return "", err
  5522  	}
  5523  
  5524  	err = wallet.Unlock(crypter)
  5525  	if err != nil {
  5526  		return "", err
  5527  	}
  5528  
  5529  	err = wallet.checkPeersAndSyncStatus()
  5530  	if err != nil {
  5531  		return "", err
  5532  	}
  5533  
  5534  	onConfirm := func() {
  5535  		go c.notify(newTokenApprovalNote(wallet.state()))
  5536  	}
  5537  
  5538  	txID, err := wallet.UnapproveToken(version, onConfirm)
  5539  	if err != nil {
  5540  		return "", err
  5541  	}
  5542  
  5543  	c.notify(newTokenApprovalNote(wallet.state()))
  5544  	return txID, nil
  5545  }
  5546  
  5547  // ApproveTokenFee returns the fee for a token approval/unapproval.
  5548  func (c *Core) ApproveTokenFee(assetID uint32, version uint32, approval bool) (uint64, error) {
  5549  	wallet, err := c.connectedWallet(assetID)
  5550  	if err != nil {
  5551  		return 0, err
  5552  	}
  5553  
  5554  	return wallet.ApprovalFee(version, approval)
  5555  }
  5556  
  5557  // EstimateSendTxFee returns an estimate of the tx fee needed to send or
  5558  // withdraw the specified amount.
  5559  func (c *Core) EstimateSendTxFee(address string, assetID uint32, amount uint64, subtract, maxWithdraw bool) (fee uint64, isValidAddress bool, err error) {
  5560  	if amount == 0 {
  5561  		return 0, false, fmt.Errorf("cannot check fee for zero %s", unbip(assetID))
  5562  	}
  5563  
  5564  	wallet, found := c.wallet(assetID)
  5565  	if !found {
  5566  		return 0, false, newError(missingWalletErr, "no wallet found for %s", unbip(assetID))
  5567  	}
  5568  
  5569  	if !wallet.traits.IsTxFeeEstimator() {
  5570  		return 0, false, fmt.Errorf("wallet does not support fee estimation")
  5571  	}
  5572  
  5573  	if subtract && !wallet.traits.IsWithdrawer() {
  5574  		return 0, false, fmt.Errorf("wallet does not support checking network fee for withdrawal")
  5575  	}
  5576  	estimator, is := wallet.Wallet.(asset.TxFeeEstimator)
  5577  	if !is {
  5578  		return 0, false, fmt.Errorf("wallet does not support fee estimation")
  5579  	}
  5580  
  5581  	return estimator.EstimateSendTxFee(address, amount, c.feeSuggestionAny(assetID), subtract, maxWithdraw)
  5582  }
  5583  
  5584  // SingleLotFees returns the estimated swap, refund, and redeem fees for a single lot
  5585  // trade.
  5586  func (c *Core) SingleLotFees(form *SingleLotFeesForm) (swapFees, redeemFees, refundFees uint64, err error) {
  5587  	dc, _, err := c.dex(form.Host)
  5588  	if err != nil {
  5589  		return 0, 0, 0, err
  5590  	}
  5591  
  5592  	mktID := marketName(form.Base, form.Quote)
  5593  	mktConf := dc.marketConfig(mktID)
  5594  	if mktConf == nil {
  5595  		return 0, 0, 0, newError(marketErr, "unknown market %q", mktID)
  5596  	}
  5597  
  5598  	wallets, assetConfigs, versCompat, err := c.walletSet(dc, form.Base, form.Quote, form.Sell)
  5599  	if err != nil {
  5600  		return 0, 0, 0, err
  5601  	}
  5602  	if !versCompat { // covers missing asset config, but that's unlikely since there is a market config
  5603  		return 0, 0, 0, fmt.Errorf("client and server asset versions are incompatible for %v", form.Host)
  5604  	}
  5605  
  5606  	var swapFeeRate, redeemFeeRate uint64
  5607  
  5608  	if form.UseMaxFeeRate {
  5609  		dc.assetsMtx.Lock()
  5610  		swapAsset, redeemAsset := dc.assets[wallets.fromWallet.AssetID], dc.assets[wallets.toWallet.AssetID]
  5611  		dc.assetsMtx.Unlock()
  5612  		if swapAsset == nil {
  5613  			return 0, 0, 0, fmt.Errorf("no asset found for %d", wallets.fromWallet.AssetID)
  5614  		}
  5615  		if redeemAsset == nil {
  5616  			return 0, 0, 0, fmt.Errorf("no asset found for %d", wallets.toWallet.AssetID)
  5617  		}
  5618  		swapFeeRate, redeemFeeRate = swapAsset.MaxFeeRate, redeemAsset.MaxFeeRate
  5619  	} else {
  5620  		swapFeeRate = c.feeSuggestion(dc, wallets.fromWallet.AssetID) // server rates only for the swap init
  5621  		if swapFeeRate == 0 {
  5622  			return 0, 0, 0, fmt.Errorf("failed to get swap fee suggestion for %s at %s", wallets.fromWallet.Symbol, form.Host)
  5623  		}
  5624  		redeemFeeRate = c.feeSuggestionAny(wallets.toWallet.AssetID) // wallet rate or server rate
  5625  		if redeemFeeRate == 0 {
  5626  			return 0, 0, 0, fmt.Errorf("failed to get redeem fee suggestion for %s at %s", wallets.toWallet.Symbol, form.Host)
  5627  		}
  5628  	}
  5629  
  5630  	swapFees, refundFees, err = wallets.fromWallet.SingleLotSwapRefundFees(assetConfigs.fromAsset.Version, swapFeeRate, form.UseSafeTxSize)
  5631  	if err != nil {
  5632  		return 0, 0, 0, fmt.Errorf("error calculating swap/refund fees: %w", err)
  5633  	}
  5634  
  5635  	redeemFees, err = wallets.toWallet.SingleLotRedeemFees(assetConfigs.toAsset.Version, redeemFeeRate)
  5636  	if err != nil {
  5637  		return 0, 0, 0, fmt.Errorf("error calculating redeem fees: %w", err)
  5638  	}
  5639  
  5640  	return swapFees, redeemFees, refundFees, nil
  5641  }
  5642  
  5643  // MaxFundingFees gives the max fees required to fund a Trade or MultiTrade.
  5644  // The host is needed to get the MaxFeeRate, which is used to calculate
  5645  // the funding fees.
  5646  func (c *Core) MaxFundingFees(fromAsset uint32, host string, numTrades uint32, options map[string]string) (uint64, error) {
  5647  	wallet, found := c.wallet(fromAsset)
  5648  	if !found {
  5649  		return 0, newError(missingWalletErr, "no wallet found for %s", unbip(fromAsset))
  5650  	}
  5651  
  5652  	exchange, err := c.Exchange(host)
  5653  	if err != nil {
  5654  		return 0, err
  5655  	}
  5656  
  5657  	asset, found := exchange.Assets[fromAsset]
  5658  	if !found {
  5659  		return 0, fmt.Errorf("asset %d not found for %s", fromAsset, host)
  5660  	}
  5661  
  5662  	return wallet.MaxFundingFees(numTrades, asset.MaxFeeRate, options), nil
  5663  }
  5664  
  5665  // PreOrder calculates fee estimates for a trade.
  5666  func (c *Core) PreOrder(form *TradeForm) (*OrderEstimate, error) {
  5667  	dc, err := c.registeredDEX(form.Host)
  5668  	if err != nil {
  5669  		return nil, err
  5670  	}
  5671  
  5672  	mktID := marketName(form.Base, form.Quote)
  5673  	mktConf := dc.marketConfig(mktID)
  5674  	if mktConf == nil {
  5675  		return nil, newError(marketErr, "unknown market %q", mktID)
  5676  	}
  5677  
  5678  	wallets, assetConfigs, versCompat, err := c.walletSet(dc, form.Base, form.Quote, form.Sell)
  5679  	if err != nil {
  5680  		return nil, err
  5681  	}
  5682  	if !versCompat { // covers missing asset config, but that's unlikely since there is a market config
  5683  		return nil, fmt.Errorf("client and server asset versions are incompatible for %v", form.Host)
  5684  	}
  5685  
  5686  	// So here's the thing. Our assets thus far don't require the wallet to be
  5687  	// unlocked to get order estimation (listunspent works on locked wallet),
  5688  	// but if we run into an asset that breaks that assumption, we may need
  5689  	// to require a password here before estimation.
  5690  
  5691  	// We need the wallets to be connected.
  5692  	if !wallets.fromWallet.connected() {
  5693  		err := c.connectAndUpdateWallet(wallets.fromWallet)
  5694  		if err != nil {
  5695  			c.log.Errorf("Error connecting to %s wallet: %v", wallets.fromWallet.Symbol, err)
  5696  			return nil, fmt.Errorf("Error connecting to %s wallet", wallets.fromWallet.Symbol)
  5697  		}
  5698  	}
  5699  
  5700  	if !wallets.toWallet.connected() {
  5701  		err := c.connectAndUpdateWallet(wallets.toWallet)
  5702  		if err != nil {
  5703  			c.log.Errorf("Error connecting to %s wallet: %v", wallets.toWallet.Symbol, err)
  5704  			return nil, fmt.Errorf("Error connecting to %s wallet", wallets.toWallet.Symbol)
  5705  		}
  5706  	}
  5707  
  5708  	// Fund the order and prepare the coins.
  5709  	lotSize := mktConf.LotSize
  5710  	lots := form.Qty / lotSize
  5711  	rate := form.Rate
  5712  
  5713  	if !form.IsLimit {
  5714  		// If this is a market order, we'll predict the fill price.
  5715  		book := dc.bookie(marketName(form.Base, form.Quote))
  5716  		if book == nil {
  5717  			return nil, fmt.Errorf("Cannot estimate market order without a synced book")
  5718  		}
  5719  
  5720  		midGap, err := book.MidGap()
  5721  		if err != nil {
  5722  			return nil, fmt.Errorf("Cannot estimate market order with an empty order book")
  5723  		}
  5724  
  5725  		if !form.Sell && calc.BaseToQuote(lotSize, midGap) > form.Qty {
  5726  			return nil, fmt.Errorf("Market order quantity buys less than a single lot")
  5727  		}
  5728  
  5729  		var fills []*orderbook.Fill
  5730  		var filled bool
  5731  		if form.Sell {
  5732  			fills, filled = book.BestFill(form.Sell, form.Qty)
  5733  		} else {
  5734  			fills, filled = book.BestFillMarketBuy(form.Qty, lotSize)
  5735  		}
  5736  
  5737  		if !filled {
  5738  			return nil, fmt.Errorf("Market is too thin to estimate market order")
  5739  		}
  5740  
  5741  		// Get an average rate.
  5742  		var qtySum, product uint64
  5743  		for _, fill := range fills {
  5744  			product += fill.Quantity * fill.Rate
  5745  			qtySum += fill.Quantity
  5746  		}
  5747  		rate = product / qtySum
  5748  		if !form.Sell {
  5749  			lots = qtySum / lotSize
  5750  		}
  5751  	}
  5752  
  5753  	swapFeeSuggestion := c.feeSuggestion(dc, wallets.fromWallet.AssetID) // server rates only for the swap init
  5754  	if swapFeeSuggestion == 0 {
  5755  		return nil, fmt.Errorf("failed to get swap fee suggestion for %s at %s", wallets.fromWallet.Symbol, form.Host)
  5756  	}
  5757  
  5758  	redeemFeeSuggestion := c.feeSuggestionAny(wallets.toWallet.AssetID) // wallet rate or server rate
  5759  	if redeemFeeSuggestion == 0 {
  5760  		return nil, fmt.Errorf("failed to get redeem fee suggestion for %s at %s", wallets.toWallet.Symbol, form.Host)
  5761  	}
  5762  
  5763  	swapLotSize := lotSize
  5764  	if !form.Sell {
  5765  		swapLotSize = calc.BaseToQuote(rate, lotSize)
  5766  	}
  5767  
  5768  	swapEstimate, err := wallets.fromWallet.PreSwap(&asset.PreSwapForm{
  5769  		Version:         assetConfigs.fromAsset.Version,
  5770  		LotSize:         swapLotSize,
  5771  		Lots:            lots,
  5772  		MaxFeeRate:      assetConfigs.fromAsset.MaxFeeRate,
  5773  		Immediate:       (form.IsLimit && form.TifNow) || !form.IsLimit,
  5774  		FeeSuggestion:   swapFeeSuggestion,
  5775  		SelectedOptions: form.Options,
  5776  		RedeemVersion:   assetConfigs.toAsset.Version,
  5777  		RedeemAssetID:   assetConfigs.toAsset.ID,
  5778  	})
  5779  	if err != nil {
  5780  		return nil, fmt.Errorf("error getting swap estimate: %w", err)
  5781  	}
  5782  
  5783  	redeemEstimate, err := wallets.toWallet.PreRedeem(&asset.PreRedeemForm{
  5784  		Version:         assetConfigs.toAsset.Version,
  5785  		Lots:            lots,
  5786  		FeeSuggestion:   redeemFeeSuggestion,
  5787  		SelectedOptions: form.Options,
  5788  	})
  5789  	if err != nil {
  5790  		return nil, fmt.Errorf("error getting redemption estimate: %v", err)
  5791  	}
  5792  
  5793  	return &OrderEstimate{
  5794  		Swap:   swapEstimate,
  5795  		Redeem: redeemEstimate,
  5796  	}, nil
  5797  }
  5798  
  5799  // MultiTradeResult is returned from MultiTrade. Some orders may be placed
  5800  // successfully, while others may fail.
  5801  type MultiTradeResult struct {
  5802  	Order *Order
  5803  	Error error
  5804  }
  5805  
  5806  // MultiTrade is used to place multiple standing limit orders on the same
  5807  // side of the same market simultaneously.
  5808  func (c *Core) MultiTrade(pw []byte, form *MultiTradeForm) []*MultiTradeResult {
  5809  	results := make([]*MultiTradeResult, 0, len(form.Placements))
  5810  
  5811  	reqs, err := c.prepareMultiTradeRequests(pw, form)
  5812  	if err != nil {
  5813  		for range form.Placements {
  5814  			results = append(results, &MultiTradeResult{Error: err})
  5815  		}
  5816  		return results
  5817  	}
  5818  
  5819  	for i := range form.Placements {
  5820  		if i >= len(reqs) {
  5821  			results = append(results, &MultiTradeResult{Error: errors.New("wallet unable to fund order")})
  5822  			continue
  5823  		}
  5824  
  5825  		req := reqs[i]
  5826  		corder, err := c.sendTradeRequest(req)
  5827  		if err != nil {
  5828  			results = append(results, &MultiTradeResult{Error: err})
  5829  			continue
  5830  		}
  5831  		results = append(results, &MultiTradeResult{Order: corder})
  5832  	}
  5833  
  5834  	return results
  5835  }
  5836  
  5837  // TxHistory returns all the transactions a wallet has made. If refID
  5838  // is nil, then transactions starting from the most recent are returned
  5839  // (past is ignored). If past is true, the transactions prior to the
  5840  // refID are returned, otherwise the transactions after the refID are
  5841  // returned. n is the number of transactions to return. If n is <= 0,
  5842  // all the transactions will be returned
  5843  func (c *Core) TxHistory(assetID uint32, n int, refID *string, past bool) ([]*asset.WalletTransaction, error) {
  5844  	wallet, found := c.wallet(assetID)
  5845  	if !found {
  5846  		return nil, newError(missingWalletErr, "no wallet found for %s", unbip(assetID))
  5847  	}
  5848  
  5849  	return wallet.TxHistory(n, refID, past)
  5850  }
  5851  
  5852  // WalletTransaction returns information about a transaction that the wallet
  5853  // has made or one in which that wallet received funds. This function supports
  5854  // both transaction ID and coin ID.
  5855  func (c *Core) WalletTransaction(assetID uint32, txID string) (*asset.WalletTransaction, error) {
  5856  	wallet, found := c.wallet(assetID)
  5857  	if !found {
  5858  		return nil, newError(missingWalletErr, "no wallet found for %s", unbip(assetID))
  5859  	}
  5860  
  5861  	return wallet.WalletTransaction(c.ctx, txID)
  5862  }
  5863  
  5864  // Trade is used to place a market or limit order.
  5865  func (c *Core) Trade(pw []byte, form *TradeForm) (*Order, error) {
  5866  	req, err := c.prepareTradeRequest(pw, form)
  5867  	if err != nil {
  5868  		return nil, err
  5869  	}
  5870  
  5871  	corder, err := c.sendTradeRequest(req)
  5872  	if err != nil {
  5873  		return nil, err
  5874  	}
  5875  
  5876  	return corder, nil
  5877  }
  5878  
  5879  // TradeAsync is like Trade but a temporary order is returned before order
  5880  // server validation. This helps handle some issues related to UI/UX where
  5881  // server response might take a fairly long time (15 - 20s).
  5882  func (c *Core) TradeAsync(pw []byte, form *TradeForm) (*InFlightOrder, error) {
  5883  	req, err := c.prepareTradeRequest(pw, form)
  5884  	if err != nil {
  5885  		return nil, err
  5886  	}
  5887  
  5888  	// Prepare and store the inflight order.
  5889  	corder := coreOrderFromTrade(req.dbOrder.Order, req.dbOrder.MetaData)
  5890  	corder.ReadyToTick = true
  5891  	tempID := req.dc.storeInFlightOrder(corder)
  5892  	req.tempID = tempID
  5893  
  5894  	// Send silent note for the async order. This improves the UI/UX, so
  5895  	// users don't have to wait for orders especially split tx orders.
  5896  	c.notify(newOrderNoteWithTempID(TopicAsyncOrderSubmitted, "", "", db.Data, corder, tempID))
  5897  
  5898  	c.wg.Add(1)
  5899  	go func() { // so core does not shut down while processing this order.
  5900  		defer func() {
  5901  			// Cleanup when the inflight order has been processed.
  5902  			req.dc.deleteInFlightOrder(tempID)
  5903  			c.wg.Done()
  5904  		}()
  5905  
  5906  		_, err := c.sendTradeRequest(req)
  5907  		if err != nil {
  5908  			// If it's an OrderQuantityTooHigh error, send simplified notification
  5909  			var mErr *msgjson.Error
  5910  			if errors.As(err, &mErr) && mErr.Code == msgjson.OrderQuantityTooHigh {
  5911  				topic := TopicOrderQuantityTooHigh
  5912  				subject, details := c.formatDetails(topic, corder.Host)
  5913  				c.notify(newOrderNoteWithTempID(topic, subject, details, db.ErrorLevel, corder, tempID))
  5914  				return
  5915  			}
  5916  			// Send async order error note.
  5917  			topic := TopicAsyncOrderFailure
  5918  			subject, details := c.formatDetails(topic, tempID, err)
  5919  			c.notify(newOrderNoteWithTempID(topic, subject, details, db.ErrorLevel, corder, tempID))
  5920  		}
  5921  	}()
  5922  
  5923  	return &InFlightOrder{
  5924  		corder,
  5925  		tempID,
  5926  	}, nil
  5927  }
  5928  
  5929  // tradeRequest hold all the information required to send a trade request to a
  5930  // server.
  5931  type tradeRequest struct {
  5932  	mktID, route string
  5933  	dc           *dexConnection
  5934  	preImg       order.Preimage
  5935  	form         *TradeForm
  5936  	dbOrder      *db.MetaOrder
  5937  	msgOrder     msgjson.Stampable
  5938  	coins        asset.Coins
  5939  	recoveryCoin asset.Coin
  5940  	wallets      *walletSet
  5941  	errCloser    *dex.ErrorCloser
  5942  	tempID       uint64
  5943  	commitSig    chan struct{}
  5944  }
  5945  
  5946  func (c *Core) prepareForTradeRequestPrep(pw []byte, base, quote uint32, host string, sell bool) (wallets *walletSet, assetConfig *assetSet, dc *dexConnection, mktConf *msgjson.Market, err error) {
  5947  	fail := func(err error) (*walletSet, *assetSet, *dexConnection, *msgjson.Market, error) {
  5948  		return nil, nil, nil, nil, err
  5949  	}
  5950  
  5951  	// Check the user password. A Trade can be attempted with an empty password,
  5952  	// which should work if both wallets are unlocked. We use this feature for
  5953  	// bots.
  5954  	var crypter encrypt.Crypter
  5955  	if len(pw) > 0 {
  5956  		var err error
  5957  		crypter, err = c.encryptionKey(pw)
  5958  		if err != nil {
  5959  			return fail(fmt.Errorf("Trade password error: %w", err))
  5960  		}
  5961  		defer crypter.Close()
  5962  	}
  5963  
  5964  	dc, err = c.registeredDEX(host)
  5965  	if err != nil {
  5966  		return fail(err)
  5967  	}
  5968  	if dc.acct.suspended() {
  5969  		return fail(newError(suspendedAcctErr, "%w", ErrAccountSuspended))
  5970  	}
  5971  
  5972  	mktID := marketName(base, quote)
  5973  	mktConf = dc.marketConfig(mktID)
  5974  	if mktConf == nil {
  5975  		return fail(newError(marketErr, "order placed for unknown market %q", mktID))
  5976  	}
  5977  
  5978  	// Proceed with the order if there is no trade suspension
  5979  	// scheduled for the market.
  5980  	if !dc.running(mktID) {
  5981  		return fail(newError(marketErr, "%s market trading is suspended", mktID))
  5982  	}
  5983  
  5984  	wallets, assetConfigs, versCompat, err := c.walletSet(dc, base, quote, sell)
  5985  	if err != nil {
  5986  		return fail(err)
  5987  	}
  5988  	if !versCompat { // also covers missing asset config, but that's unlikely since there is a market config
  5989  		return fail(fmt.Errorf("client and server asset versions are incompatible for %v", dc.acct.host))
  5990  	}
  5991  
  5992  	fromWallet, toWallet := wallets.fromWallet, wallets.toWallet
  5993  
  5994  	prepareWallet := func(w *xcWallet) error {
  5995  		// NOTE: If the wallet is already internally unlocked (the decrypted
  5996  		// password cached in xcWallet.pw), this could be done without the
  5997  		// crypter via refreshUnlock.
  5998  		err := c.connectAndUnlock(crypter, w)
  5999  		if err != nil {
  6000  			return fmt.Errorf("%s connectAndUnlock error: %w",
  6001  				assetConfigs.fromAsset.Symbol, err)
  6002  		}
  6003  		w.mtx.RLock()
  6004  		defer w.mtx.RUnlock()
  6005  		if w.peerCount < 1 {
  6006  			return &WalletNoPeersError{w.AssetID}
  6007  		}
  6008  		if !w.syncStatus.Synced {
  6009  			return &WalletSyncError{w.AssetID, w.syncStatus.BlockProgress()}
  6010  		}
  6011  		return nil
  6012  	}
  6013  
  6014  	err = prepareWallet(fromWallet)
  6015  	if err != nil {
  6016  		return fail(err)
  6017  	}
  6018  
  6019  	err = prepareWallet(toWallet)
  6020  	if err != nil {
  6021  		return fail(err)
  6022  	}
  6023  
  6024  	return wallets, assetConfigs, dc, mktConf, nil
  6025  }
  6026  
  6027  func (c *Core) createTradeRequest(wallets *walletSet, coins asset.Coins, redeemScripts []dex.Bytes, dc *dexConnection, redeemAddr string,
  6028  	form *TradeForm, redemptionRefundLots uint64, fundingFees uint64, assetConfigs *assetSet, mktConf *msgjson.Market, errCloser *dex.ErrorCloser) (*tradeRequest, error) {
  6029  	coinIDs := make([]order.CoinID, 0, len(coins))
  6030  	for i := range coins {
  6031  		coinIDs = append(coinIDs, []byte(coins[i].ID()))
  6032  	}
  6033  
  6034  	fromWallet, toWallet := wallets.fromWallet, wallets.toWallet
  6035  	accountRedeemer, isAccountRedemption := toWallet.Wallet.(asset.AccountLocker)
  6036  	accountRefunder, isAccountRefund := fromWallet.Wallet.(asset.AccountLocker)
  6037  
  6038  	// In the special case that there is a single coin that implements
  6039  	// RecoveryCoin, set that as the change coin.
  6040  	var recoveryCoin asset.Coin
  6041  	var changeID []byte
  6042  	if len(coins) == 1 {
  6043  		c := coins[0]
  6044  		if rc, is := c.(asset.RecoveryCoin); is {
  6045  			recoveryCoin = c
  6046  			changeID = rc.RecoveryID()
  6047  		}
  6048  	}
  6049  
  6050  	preImg := newPreimage()
  6051  	prefix := &order.Prefix{
  6052  		AccountID:  dc.acct.ID(),
  6053  		BaseAsset:  form.Base,
  6054  		QuoteAsset: form.Quote,
  6055  		OrderType:  order.MarketOrderType,
  6056  		ClientTime: time.Now(),
  6057  		Commit:     preImg.Commit(),
  6058  	}
  6059  	var ord order.Order
  6060  	if form.IsLimit {
  6061  		prefix.OrderType = order.LimitOrderType
  6062  		tif := order.StandingTiF
  6063  		if form.TifNow {
  6064  			tif = order.ImmediateTiF
  6065  		}
  6066  		ord = &order.LimitOrder{
  6067  			P: *prefix,
  6068  			T: order.Trade{
  6069  				Coins:    coinIDs,
  6070  				Sell:     form.Sell,
  6071  				Quantity: form.Qty,
  6072  				Address:  redeemAddr,
  6073  			},
  6074  			Rate:  form.Rate,
  6075  			Force: tif,
  6076  		}
  6077  	} else {
  6078  		ord = &order.MarketOrder{
  6079  			P: *prefix,
  6080  			T: order.Trade{
  6081  				Coins:    coinIDs,
  6082  				Sell:     form.Sell,
  6083  				Quantity: form.Qty,
  6084  				Address:  redeemAddr,
  6085  			},
  6086  		}
  6087  	}
  6088  
  6089  	err := order.ValidateOrder(ord, order.OrderStatusEpoch, mktConf.LotSize)
  6090  	if err != nil {
  6091  		return nil, fmt.Errorf("ValidateOrder error: %w", err)
  6092  	}
  6093  
  6094  	msgCoins, err := messageCoins(fromWallet, coins, redeemScripts)
  6095  	if err != nil {
  6096  		return nil, fmt.Errorf("%v wallet failed to sign coins: %w", assetConfigs.fromAsset.Symbol, err)
  6097  	}
  6098  
  6099  	// Everything is ready. Send the order.
  6100  	route, msgOrder, msgTrade := messageOrder(ord, msgCoins)
  6101  
  6102  	// If the to asset is an AccountLocker, we need to lock up redemption
  6103  	// funds.
  6104  	var redemptionReserves uint64
  6105  	if isAccountRedemption {
  6106  		pubKeys, sigs, err := toWallet.SignMessage(nil, msgOrder.Serialize())
  6107  		if err != nil {
  6108  			return nil, codedError(signatureErr, fmt.Errorf("SignMessage error: %w", err))
  6109  		}
  6110  		if len(pubKeys) == 0 || len(sigs) == 0 {
  6111  			return nil, newError(signatureErr, "wrong number of pubkeys or signatures, %d & %d", len(pubKeys), len(sigs))
  6112  		}
  6113  		redemptionReserves, err = accountRedeemer.ReserveNRedemptions(redemptionRefundLots,
  6114  			assetConfigs.toAsset.Version, assetConfigs.toAsset.MaxFeeRate)
  6115  		if err != nil {
  6116  			return nil, codedError(walletErr, fmt.Errorf("ReserveNRedemptions error: %w", err))
  6117  		}
  6118  		defer func() {
  6119  			if _, err := c.updateWalletBalance(toWallet); err != nil {
  6120  				c.log.Errorf("updateWalletBalance error: %v", err)
  6121  			}
  6122  			if toToken := asset.TokenInfo(assetConfigs.toAsset.ID); toToken != nil {
  6123  				c.updateAssetBalance(toToken.ParentID)
  6124  			}
  6125  		}()
  6126  
  6127  		msgTrade.RedeemSig = &msgjson.RedeemSig{
  6128  			PubKey: pubKeys[0],
  6129  			Sig:    sigs[0],
  6130  		}
  6131  		errCloser.Add(func() error {
  6132  			accountRedeemer.UnlockRedemptionReserves(redemptionReserves)
  6133  			return nil
  6134  		})
  6135  	}
  6136  
  6137  	// If the from asset is an AccountLocker, we need to lock up refund funds.
  6138  	var refundReserves uint64
  6139  	if isAccountRefund {
  6140  		refundReserves, err = accountRefunder.ReserveNRefunds(redemptionRefundLots,
  6141  			assetConfigs.fromAsset.Version, assetConfigs.fromAsset.MaxFeeRate)
  6142  		if err != nil {
  6143  			return nil, codedError(walletErr, fmt.Errorf("ReserveNRefunds error: %w", err))
  6144  		}
  6145  		errCloser.Add(func() error {
  6146  			accountRefunder.UnlockRefundReserves(refundReserves)
  6147  			return nil
  6148  		})
  6149  	}
  6150  
  6151  	// A non-nil changeID indicates that this is an account based coin. The
  6152  	// first coin is an address and the entire serialized message needs to
  6153  	// be signed with that address's private key.
  6154  	if changeID != nil {
  6155  		if _, msgTrade.Coins[0].Sigs, err = fromWallet.SignMessage(nil, msgOrder.Serialize()); err != nil {
  6156  			return nil, fmt.Errorf("%v wallet failed to sign for redeem: %w",
  6157  				assetConfigs.fromAsset.Symbol, err)
  6158  		}
  6159  	}
  6160  
  6161  	commitSig := make(chan struct{})
  6162  	c.sentCommitsMtx.Lock()
  6163  	c.sentCommits[prefix.Commit] = commitSig
  6164  	c.sentCommitsMtx.Unlock()
  6165  
  6166  	// Prepare order meta data.
  6167  	dbOrder := &db.MetaOrder{
  6168  		MetaData: &db.OrderMetaData{
  6169  			Host:               dc.acct.host,
  6170  			EpochDur:           mktConf.EpochLen, // epochIndex := result.ServerTime / mktConf.EpochLen
  6171  			FromSwapConf:       assetConfigs.fromAsset.SwapConf,
  6172  			ToSwapConf:         assetConfigs.toAsset.SwapConf,
  6173  			MaxFeeRate:         assetConfigs.fromAsset.MaxFeeRate,
  6174  			RedeemMaxFeeRate:   assetConfigs.toAsset.MaxFeeRate,
  6175  			FromVersion:        assetConfigs.fromAsset.Version,
  6176  			ToVersion:          assetConfigs.toAsset.Version, // and we're done with the server's asset configs.
  6177  			Options:            form.Options,
  6178  			RedemptionReserves: redemptionReserves,
  6179  			RefundReserves:     refundReserves,
  6180  			ChangeCoin:         changeID,
  6181  			FundingFeesPaid:    fundingFees,
  6182  		},
  6183  		Order: ord,
  6184  	}
  6185  
  6186  	return &tradeRequest{
  6187  		mktID:        marketName(form.Base, form.Quote),
  6188  		route:        route,
  6189  		dc:           dc,
  6190  		form:         form,
  6191  		dbOrder:      dbOrder,
  6192  		msgOrder:     msgOrder,
  6193  		recoveryCoin: recoveryCoin,
  6194  		coins:        coins,
  6195  		wallets:      wallets,
  6196  		errCloser:    errCloser.Copy(),
  6197  		preImg:       preImg,
  6198  		commitSig:    commitSig,
  6199  	}, nil
  6200  }
  6201  
  6202  // prepareTradeRequest prepares a trade request.
  6203  func (c *Core) prepareTradeRequest(pw []byte, form *TradeForm) (*tradeRequest, error) {
  6204  	wallets, assetConfigs, dc, mktConf, err := c.prepareForTradeRequestPrep(pw, form.Base, form.Quote, form.Host, form.Sell)
  6205  	if err != nil {
  6206  		return nil, err
  6207  	}
  6208  
  6209  	fromWallet, toWallet := wallets.fromWallet, wallets.toWallet
  6210  	mktID := marketName(form.Base, form.Quote)
  6211  
  6212  	rate, qty := form.Rate, form.Qty
  6213  	if form.IsLimit {
  6214  		if rate == 0 {
  6215  			return nil, newError(orderParamsErr, "zero-rate order not allowed")
  6216  		}
  6217  		if minRate := dc.minimumMarketRate(assetConfigs.quoteAsset, mktConf.LotSize); rate < minRate {
  6218  			return nil, newError(orderParamsErr, "order's rate is lower than market's minimum rate. %d < %d", rate, minRate)
  6219  		}
  6220  	}
  6221  
  6222  	// Get an address for the swap contract.
  6223  	redeemAddr, err := toWallet.RedemptionAddress()
  6224  	if err != nil {
  6225  		return nil, codedError(walletErr, fmt.Errorf("%s RedemptionAddress error: %w",
  6226  			assetConfigs.toAsset.Symbol, err))
  6227  	}
  6228  
  6229  	// Fund the order and prepare the coins.
  6230  	lotSize := mktConf.LotSize
  6231  	fundQty := qty
  6232  	lots := qty / lotSize
  6233  	if form.IsLimit && !form.Sell {
  6234  		fundQty = calc.BaseToQuote(rate, fundQty)
  6235  	}
  6236  	redemptionRefundLots := lots
  6237  
  6238  	isImmediate := (!form.IsLimit || form.TifNow)
  6239  
  6240  	// Market buy order
  6241  	if !form.IsLimit && !form.Sell {
  6242  		_, isAccountRedemption := toWallet.Wallet.(asset.AccountLocker)
  6243  
  6244  		// There is some ambiguity here about whether the specified quantity for
  6245  		// a market buy order should include projected fees, or whether fees
  6246  		// should be in addition to the quantity. If the fees should be
  6247  		// additional to the order quantity (the approach taken here), we should
  6248  		// try to estimate the number of lots based on the current market. If
  6249  		// the market is not synced, fall back to a single-lot estimate, with
  6250  		// the knowledge that such an estimate means that the specified amount
  6251  		// might not all be available for matching once fees are considered.
  6252  		lots = 1
  6253  		book := dc.bookie(mktID)
  6254  		if book != nil {
  6255  			midGap, err := book.MidGap()
  6256  			// An error is only returned when there are no orders on the book.
  6257  			// In that case, fall back to the 1 lot estimate for now.
  6258  			if err == nil {
  6259  				baseQty := calc.QuoteToBase(midGap, fundQty)
  6260  				lots = baseQty / lotSize
  6261  				redemptionRefundLots = lots * marketBuyRedemptionSlippageBuffer
  6262  				if lots == 0 {
  6263  					err = newError(orderParamsErr,
  6264  						"order quantity is too low for current market rates. "+
  6265  							"qty = %d %s, mid-gap = %d, base-qty = %d %s, lot size = %d",
  6266  						qty, assetConfigs.quoteAsset.Symbol, midGap, baseQty,
  6267  						assetConfigs.baseAsset.Symbol, lotSize)
  6268  					return nil, err
  6269  				}
  6270  			} else if isAccountRedemption {
  6271  				return nil, newError(orderParamsErr, "cannot estimate redemption count")
  6272  			}
  6273  		}
  6274  	}
  6275  
  6276  	if lots == 0 {
  6277  		return nil, newError(orderParamsErr, "order quantity < 1 lot. qty = %d %s, rate = %d, lot size = %d",
  6278  			qty, assetConfigs.baseAsset.Symbol, rate, mktConf.LotSize)
  6279  	}
  6280  
  6281  	coins, redeemScripts, fundingFees, err := fromWallet.FundOrder(&asset.Order{
  6282  		Version:       assetConfigs.fromAsset.Version,
  6283  		Value:         fundQty,
  6284  		MaxSwapCount:  lots,
  6285  		MaxFeeRate:    assetConfigs.fromAsset.MaxFeeRate,
  6286  		Immediate:     isImmediate,
  6287  		FeeSuggestion: c.feeSuggestion(dc, assetConfigs.fromAsset.ID),
  6288  		Options:       form.Options,
  6289  		RedeemVersion: assetConfigs.toAsset.Version,
  6290  		RedeemAssetID: assetConfigs.toAsset.ID,
  6291  	})
  6292  	if err != nil {
  6293  		return nil, codedError(walletErr, fmt.Errorf("FundOrder error for %s, funding quantity %d (%d lots): %w",
  6294  			assetConfigs.fromAsset.Symbol, fundQty, lots, err))
  6295  	}
  6296  	defer func() {
  6297  		if _, err := c.updateWalletBalance(fromWallet); err != nil {
  6298  			c.log.Errorf("updateWalletBalance error: %v", err)
  6299  		}
  6300  		if fromToken := asset.TokenInfo(assetConfigs.fromAsset.ID); fromToken != nil {
  6301  			c.updateAssetBalance(fromToken.ParentID)
  6302  		}
  6303  	}()
  6304  
  6305  	// The coins selected for this order will need to be unlocked
  6306  	// if the order does not get to the server successfully.
  6307  	errCloser := dex.NewErrorCloser()
  6308  	defer errCloser.Done(c.log)
  6309  	errCloser.Add(func() error {
  6310  		err := fromWallet.ReturnCoins(coins)
  6311  		if err != nil {
  6312  			return fmt.Errorf("Unable to return %s funding coins: %v", unbip(fromWallet.AssetID), err)
  6313  		}
  6314  		return nil
  6315  	})
  6316  
  6317  	tradeRequest, err := c.createTradeRequest(wallets, coins, redeemScripts, dc, redeemAddr, form,
  6318  		redemptionRefundLots, fundingFees, assetConfigs, mktConf, errCloser)
  6319  	if err != nil {
  6320  		return nil, err
  6321  	}
  6322  
  6323  	errCloser.Success()
  6324  
  6325  	return tradeRequest, nil
  6326  }
  6327  
  6328  func (c *Core) prepareMultiTradeRequests(pw []byte, form *MultiTradeForm) ([]*tradeRequest, error) {
  6329  	wallets, assetConfigs, dc, mktConf, err := c.prepareForTradeRequestPrep(pw, form.Base, form.Quote, form.Host, form.Sell)
  6330  	if err != nil {
  6331  		return nil, err
  6332  	}
  6333  	fromWallet, toWallet := wallets.fromWallet, wallets.toWallet
  6334  
  6335  	for _, trade := range form.Placements {
  6336  		if trade.Rate == 0 {
  6337  			return nil, newError(orderParamsErr, "zero rate is invalid")
  6338  		}
  6339  		if trade.Qty == 0 {
  6340  			return nil, newError(orderParamsErr, "zero quantity is invalid")
  6341  		}
  6342  	}
  6343  
  6344  	redeemAddresses := make([]string, 0, len(form.Placements))
  6345  	for range form.Placements {
  6346  		redeemAddr, err := toWallet.RedemptionAddress()
  6347  		if err != nil {
  6348  			return nil, codedError(walletErr, fmt.Errorf("%s RedemptionAddress error: %w",
  6349  				assetConfigs.toAsset.Symbol, err))
  6350  		}
  6351  		redeemAddresses = append(redeemAddresses, redeemAddr)
  6352  	}
  6353  
  6354  	orderValues := make([]*asset.MultiOrderValue, 0, len(form.Placements))
  6355  	for _, trade := range form.Placements {
  6356  		fundQty := trade.Qty
  6357  		lots := fundQty / mktConf.LotSize
  6358  		if lots == 0 {
  6359  			return nil, newError(orderParamsErr, "order quantity < 1 lot")
  6360  		}
  6361  
  6362  		if !form.Sell {
  6363  			fundQty = calc.BaseToQuote(trade.Rate, fundQty)
  6364  		}
  6365  		orderValues = append(orderValues, &asset.MultiOrderValue{
  6366  			MaxSwapCount: lots,
  6367  			Value:        fundQty,
  6368  		})
  6369  	}
  6370  
  6371  	allCoins, allRedeemScripts, fundingFees, err := fromWallet.FundMultiOrder(&asset.MultiOrder{
  6372  		Version:       assetConfigs.fromAsset.Version,
  6373  		Values:        orderValues,
  6374  		MaxFeeRate:    assetConfigs.fromAsset.MaxFeeRate,
  6375  		FeeSuggestion: c.feeSuggestion(dc, assetConfigs.fromAsset.ID),
  6376  		Options:       form.Options,
  6377  		RedeemVersion: assetConfigs.toAsset.Version,
  6378  		RedeemAssetID: assetConfigs.toAsset.ID,
  6379  	}, form.MaxLock)
  6380  	if err != nil {
  6381  		return nil, codedError(walletErr, fmt.Errorf("FundMultiOrder error for %s: %v", assetConfigs.fromAsset.Symbol, err))
  6382  	}
  6383  
  6384  	if len(allCoins) != len(form.Placements) {
  6385  		c.log.Infof("FundMultiOrder only funded %d orders out of %d (options = %+v)", len(allCoins), len(form.Placements), form.Options)
  6386  	}
  6387  	defer func() {
  6388  		if _, err := c.updateWalletBalance(fromWallet); err != nil {
  6389  			c.log.Errorf("updateWalletBalance error: %v", err)
  6390  		}
  6391  		if fromToken := asset.TokenInfo(assetConfigs.fromAsset.ID); fromToken != nil {
  6392  			c.updateAssetBalance(fromToken.ParentID)
  6393  		}
  6394  	}()
  6395  
  6396  	errClosers := make([]*dex.ErrorCloser, 0, len(allCoins))
  6397  	for _, coins := range allCoins {
  6398  		theseCoins := coins
  6399  		errCloser := dex.NewErrorCloser()
  6400  		defer errCloser.Done(c.log)
  6401  		errCloser.Add(func() error {
  6402  			err := fromWallet.ReturnCoins(theseCoins)
  6403  			if err != nil {
  6404  				return fmt.Errorf("unable to return %s funding coins: %v", unbip(fromWallet.AssetID), err)
  6405  			}
  6406  			return nil
  6407  		})
  6408  		errClosers = append(errClosers, errCloser)
  6409  	}
  6410  
  6411  	tradeRequests := make([]*tradeRequest, 0, len(allCoins))
  6412  	for i, coins := range allCoins {
  6413  		tradeForm := &TradeForm{
  6414  			Host:    form.Host,
  6415  			IsLimit: true,
  6416  			Sell:    form.Sell,
  6417  			Base:    form.Base,
  6418  			Quote:   form.Quote,
  6419  			Qty:     form.Placements[i].Qty,
  6420  			Rate:    form.Placements[i].Rate,
  6421  			Options: form.Options,
  6422  		}
  6423  		// Only count the funding fees once.
  6424  		var fees uint64
  6425  		if i == 0 {
  6426  			fees = fundingFees
  6427  		}
  6428  		req, err := c.createTradeRequest(wallets, coins, allRedeemScripts[i], dc, redeemAddresses[i], tradeForm,
  6429  			orderValues[i].MaxSwapCount, fees, assetConfigs, mktConf, errClosers[i])
  6430  		if err != nil {
  6431  			return nil, err
  6432  		}
  6433  		tradeRequests = append(tradeRequests, req)
  6434  	}
  6435  
  6436  	for _, errCloser := range errClosers {
  6437  		errCloser.Success()
  6438  	}
  6439  
  6440  	return tradeRequests, nil
  6441  }
  6442  
  6443  // sendTradeRequest sends an order, processes the result, then prepares and
  6444  // stores the trackedTrade.
  6445  func (c *Core) sendTradeRequest(tr *tradeRequest) (*Order, error) {
  6446  	dc, dbOrder, wallets, form, route := tr.dc, tr.dbOrder, tr.wallets, tr.form, tr.route
  6447  	mktID, msgOrder, preImg, recoveryCoin, coins := tr.mktID, tr.msgOrder, tr.preImg, tr.recoveryCoin, tr.coins
  6448  	defer tr.errCloser.Done(c.log)
  6449  	defer close(tr.commitSig) // signals on both success and failure
  6450  
  6451  	// Send and get the result.
  6452  	result := new(msgjson.OrderResult)
  6453  	err := dc.signAndRequest(msgOrder, route, result, fundingTxWait+DefaultResponseTimeout)
  6454  	if err != nil {
  6455  		// At this point there is a possibility that the server got the request
  6456  		// and created the trade order, but we lost the connection before
  6457  		// receiving the response with the trade's order ID. Any preimage
  6458  		// request will be unrecognized. This order is ABANDONED.
  6459  		return nil, fmt.Errorf("new order request with DEX server %v market %v failed: %w", dc.acct.host, mktID, err)
  6460  	}
  6461  
  6462  	ord := dbOrder.Order
  6463  	err = validateOrderResponse(dc, result, ord, msgOrder) // stamps the order, giving it a valid ID
  6464  	if err != nil {
  6465  		c.log.Errorf("Abandoning order. preimage: %x, server time: %d: %v",
  6466  			preImg[:], result.ServerTime, fmt.Sprintf("order response validation failure: %v", err))
  6467  		return nil, fmt.Errorf("validateOrderResponse error: %w", err)
  6468  	}
  6469  
  6470  	// TODO: Need xcWallet fields for acceptable SwapConf values: a min
  6471  	// acceptable for security, and even a max confs override to act sooner.
  6472  
  6473  	// Store the order.
  6474  	tr.dbOrder.MetaData.Status = order.OrderStatusEpoch
  6475  	tr.dbOrder.MetaData.Proof = db.OrderProof{
  6476  		DEXSig:   result.Sig,
  6477  		Preimage: tr.preImg[:],
  6478  	}
  6479  
  6480  	err = c.db.UpdateOrder(dbOrder)
  6481  	if err != nil {
  6482  		c.log.Errorf("Abandoning order. preimage: %x, server time: %d: %v",
  6483  			preImg[:], result.ServerTime, fmt.Sprintf("failed to store order in database: %v", err))
  6484  		return nil, fmt.Errorf("db.UpdateOrder error: %w", err)
  6485  	}
  6486  
  6487  	// Prepare and store the tracker and get the core.Order to return.
  6488  	tracker := newTrackedTrade(dbOrder, preImg, dc, c.lockTimeTaker, c.lockTimeMaker,
  6489  		c.db, c.latencyQ, wallets, coins, c.notify, c.formatDetails)
  6490  
  6491  	tracker.redemptionLocked = tracker.redemptionReserves
  6492  	tracker.refundLocked = tracker.refundReserves
  6493  
  6494  	if recoveryCoin != nil {
  6495  		tracker.change = recoveryCoin
  6496  		tracker.coinsLocked = false
  6497  		tracker.changeLocked = true
  6498  	}
  6499  
  6500  	dc.tradeMtx.Lock()
  6501  	dc.trades[tracker.ID()] = tracker
  6502  	dc.tradeMtx.Unlock()
  6503  
  6504  	// Send a low-priority notification.
  6505  	corder := tracker.coreOrder()
  6506  	if !form.IsLimit && !form.Sell {
  6507  		ui := wallets.quoteWallet.Info().UnitInfo
  6508  		subject, details := c.formatDetails(TopicYoloPlaced,
  6509  			ui.ConventionalString(corder.Qty), ui.Conventional.Unit, makeOrderToken(tracker.token()))
  6510  		c.notify(newOrderNoteWithTempID(TopicYoloPlaced, subject, details, db.Poke, corder, tr.tempID))
  6511  	} else {
  6512  		rateString := "market"
  6513  		if form.IsLimit {
  6514  			rateString = wallets.trimmedConventionalRateString(corder.Rate)
  6515  		}
  6516  		ui := wallets.baseWallet.Info().UnitInfo
  6517  		topic := TopicBuyOrderPlaced
  6518  		if corder.Sell {
  6519  			topic = TopicSellOrderPlaced
  6520  		}
  6521  		subject, details := c.formatDetails(topic, ui.ConventionalString(corder.Qty), ui.Conventional.Unit, rateString, makeOrderToken(tracker.token()))
  6522  		c.notify(newOrderNoteWithTempID(topic, subject, details, db.Poke, corder, tr.tempID))
  6523  	}
  6524  
  6525  	tr.errCloser.Success()
  6526  
  6527  	return corder, nil
  6528  }
  6529  
  6530  // walletSet is a pair of wallets with asset configurations identified in useful
  6531  // ways.
  6532  type walletSet struct {
  6533  	fromWallet  *xcWallet
  6534  	toWallet    *xcWallet
  6535  	baseWallet  *xcWallet
  6536  	quoteWallet *xcWallet
  6537  }
  6538  
  6539  // assetSet bundles a server's asset "config" for a pair of assets.
  6540  type assetSet struct {
  6541  	baseAsset  *dex.Asset
  6542  	quoteAsset *dex.Asset
  6543  	fromAsset  *dex.Asset
  6544  	toAsset    *dex.Asset
  6545  }
  6546  
  6547  // conventionalRate converts the message-rate encoded rate to a rate in
  6548  // conventional units.
  6549  func (w *walletSet) conventionalRate(msgRate uint64) float64 {
  6550  	return calc.ConventionalRate(msgRate, w.baseWallet.Info().UnitInfo, w.quoteWallet.Info().UnitInfo)
  6551  }
  6552  
  6553  func (w *walletSet) trimmedConventionalRateString(r uint64) string {
  6554  	s := strconv.FormatFloat(w.conventionalRate(r), 'f', 8, 64)
  6555  	return strings.TrimRight(strings.TrimRight(s, "0"), ".")
  6556  }
  6557  
  6558  // walletSet constructs a walletSet and an assetSet for a certain DEX server and
  6559  // asset pair, with the trade direction (sell) used to assign to/from aliases in
  6560  // the returned structs. It is not an error if one or both asset configurations
  6561  // are missing on the DEX, so the caller must nil check the fields. This also
  6562  // returns if our wallet versions and the server's asset versions are
  6563  // compatible.
  6564  func (c *Core) walletSet(dc *dexConnection, baseID, quoteID uint32, sell bool) (*walletSet, *assetSet, bool, error) {
  6565  	// Connect and open the wallets if needed.
  6566  	baseWallet, found := c.wallet(baseID)
  6567  	if !found {
  6568  		return nil, nil, false, newError(missingWalletErr, "no wallet found for %s", unbip(baseID))
  6569  	}
  6570  	quoteWallet, found := c.wallet(quoteID)
  6571  	if !found {
  6572  		return nil, nil, false, newError(missingWalletErr, "no wallet found for %s", unbip(quoteID))
  6573  	}
  6574  
  6575  	dc.assetsMtx.RLock()
  6576  	baseAsset := dc.assets[baseID]
  6577  	quoteAsset := dc.assets[quoteID]
  6578  	dc.assetsMtx.RUnlock()
  6579  
  6580  	var versCompat bool
  6581  	if baseAsset == nil {
  6582  		c.log.Warnf("Base asset server configuration not available for %s (asset %s).",
  6583  			dc.acct.host, unbip(baseID))
  6584  	} else {
  6585  		versCompat = baseWallet.supportsVer(baseAsset.Version)
  6586  	}
  6587  	if quoteAsset == nil {
  6588  		c.log.Warnf("Quote asset server configuration not available for %s (asset %s).",
  6589  			dc.acct.host, unbip(quoteID))
  6590  	} else {
  6591  		versCompat = versCompat && quoteWallet.supportsVer(quoteAsset.Version)
  6592  	}
  6593  
  6594  	// We actually care less about base/quote, and more about from/to, which
  6595  	// depends on whether this is a buy or sell order.
  6596  	fromAsset, toAsset := baseAsset, quoteAsset
  6597  	fromWallet, toWallet := baseWallet, quoteWallet
  6598  	if !sell {
  6599  		fromAsset, toAsset = quoteAsset, baseAsset
  6600  		fromWallet, toWallet = quoteWallet, baseWallet
  6601  	}
  6602  
  6603  	return &walletSet{
  6604  			fromWallet:  fromWallet,
  6605  			toWallet:    toWallet,
  6606  			baseWallet:  baseWallet,
  6607  			quoteWallet: quoteWallet,
  6608  		}, &assetSet{
  6609  			baseAsset:  baseAsset,
  6610  			quoteAsset: quoteAsset,
  6611  			fromAsset:  fromAsset,
  6612  			toAsset:    toAsset,
  6613  		}, versCompat, nil
  6614  }
  6615  
  6616  func (c *Core) Cancel(oidB dex.Bytes) error {
  6617  	oid, err := order.IDFromBytes(oidB)
  6618  	if err != nil {
  6619  		return err
  6620  	}
  6621  	return c.cancelOrder(oid)
  6622  }
  6623  
  6624  func (c *Core) cancelOrder(oid order.OrderID) error {
  6625  	for _, dc := range c.dexConnections() {
  6626  		found, err := c.tryCancel(dc, oid)
  6627  		if err != nil {
  6628  			return err
  6629  		}
  6630  		if found {
  6631  			return nil
  6632  		}
  6633  	}
  6634  
  6635  	return fmt.Errorf("Cancel: failed to find order %s", oid)
  6636  }
  6637  
  6638  func assetBond(bond *db.Bond) *asset.Bond {
  6639  	return &asset.Bond{
  6640  		Version:    bond.Version,
  6641  		AssetID:    bond.AssetID,
  6642  		Amount:     bond.Amount,
  6643  		CoinID:     bond.CoinID,
  6644  		Data:       bond.Data,
  6645  		SignedTx:   bond.SignedTx,
  6646  		UnsignedTx: bond.UnsignedTx,
  6647  		RedeemTx:   bond.RefundTx,
  6648  	}
  6649  }
  6650  
  6651  // bondKey creates a unique map key for a bond by its asset ID and coin ID.
  6652  func bondKey(assetID uint32, coinID []byte) string {
  6653  	return string(append(encode.Uint32Bytes(assetID), coinID...))
  6654  }
  6655  
  6656  // updateReputation sets the account's reputation-related fields.
  6657  // updateReputation must be called with the authMtx locked.
  6658  func (dc *dexConnection) updateReputation(
  6659  	newReputation *account.Reputation,
  6660  ) {
  6661  	dc.acct.rep = *newReputation
  6662  }
  6663  
  6664  // findBondKeyIdx will attempt to find the address index whose public key hashes
  6665  // to a specific hash.
  6666  func (c *Core) findBondKeyIdx(pkhEqualFn func(bondKey *secp256k1.PrivateKey) bool, assetID uint32) (idx uint32, err error) {
  6667  	if !c.bondKeysReady() {
  6668  		return 0, errors.New("bond key is not initialized")
  6669  	}
  6670  	nbki, err := c.db.NextBondKeyIndex(assetID)
  6671  	if err != nil {
  6672  		return 0, fmt.Errorf("unable to get next bond key index: %v", err)
  6673  	}
  6674  	maxIdx := nbki + 10_000
  6675  	for i := uint32(0); i < maxIdx; i++ {
  6676  		bondKey, err := c.bondKeyIdx(assetID, i)
  6677  		if err != nil {
  6678  			return 0, fmt.Errorf("error getting bond key at idx %d: %v", i, err)
  6679  		}
  6680  		equal := pkhEqualFn(bondKey)
  6681  		bondKey.Zero()
  6682  		if equal {
  6683  			return i, nil
  6684  		}
  6685  	}
  6686  	return 0, fmt.Errorf("searched until idx %d but did not find a pubkey match", maxIdx)
  6687  }
  6688  
  6689  // findBond will attempt to find an unknown bond and add it to the live bonds
  6690  // slice and db for refunding later. Returns the bond strength if no error.
  6691  func (c *Core) findBond(dc *dexConnection, bond *msgjson.Bond) (strength, bondAssetID uint32) {
  6692  	if bond.AssetID == account.PrepaidBondID {
  6693  		c.insertPrepaidBond(dc, bond)
  6694  		return bond.Strength, bond.AssetID
  6695  	}
  6696  	symb := dex.BipIDSymbol(bond.AssetID)
  6697  	bondIDStr := coinIDString(bond.AssetID, bond.CoinID)
  6698  	c.log.Warnf("Unknown bond reported by server: %v (%s)", bondIDStr, symb)
  6699  
  6700  	wallet, err := c.connectedWallet(bond.AssetID)
  6701  	if err != nil {
  6702  		c.log.Errorf("%d -> %s wallet error: %w", bond.AssetID, unbip(bond.AssetID), err)
  6703  		return 0, 0
  6704  	}
  6705  
  6706  	// The server will only tell us about active bonds, so we only need
  6707  	// search in the possible active timeframe before that. Server will tell
  6708  	// us when the expiry is, so can subtract from that. Add a day out of
  6709  	// caution.
  6710  	bondExpiry := int64(dc.config().BondExpiry)
  6711  	activeBondTimeframe := minBondLifetime(c.net, bondExpiry) - time.Second*time.Duration(bondExpiry) + time.Second*(60*60*24) // seconds * minutes * hours
  6712  
  6713  	bondDetails, err := wallet.FindBond(c.ctx, bond.CoinID, time.Unix(int64(bond.Expiry), 0).Add(-activeBondTimeframe))
  6714  	if err != nil {
  6715  		c.log.Errorf("Unable to find active bond reported by the server: %v", err)
  6716  		return 0, 0
  6717  	}
  6718  
  6719  	bondAsset, _ := dc.bondAsset(bond.AssetID)
  6720  	if bondAsset == nil {
  6721  		// Probably not possible since the dex told us about it. Keep
  6722  		// going to refund it later.
  6723  		c.log.Warnf("Dex does not support fidelity bonds in asset %s", symb)
  6724  		strength = bond.Strength
  6725  	} else {
  6726  		strength = uint32(bondDetails.Amount / bondAsset.Amt)
  6727  	}
  6728  
  6729  	idx, err := c.findBondKeyIdx(bondDetails.CheckPrivKey, bond.AssetID)
  6730  	if err != nil {
  6731  		c.log.Warnf("Unable to find bond key index for unknown bond %s, will not be able to refund: %v", bondIDStr, err)
  6732  		idx = math.MaxUint32
  6733  	}
  6734  
  6735  	dbBond := &db.Bond{
  6736  		Version:   bondDetails.Version,
  6737  		AssetID:   bondDetails.AssetID,
  6738  		CoinID:    bondDetails.CoinID,
  6739  		Data:      bondDetails.Data,
  6740  		Amount:    bondDetails.Amount,
  6741  		LockTime:  uint64(bondDetails.LockTime.Unix()),
  6742  		KeyIndex:  idx,
  6743  		Strength:  strength,
  6744  		Confirmed: true,
  6745  	}
  6746  
  6747  	err = c.db.AddBond(dc.acct.host, dbBond)
  6748  	if err != nil {
  6749  		c.log.Errorf("Failed to store bond %s (%s) for dex %v: %w",
  6750  			bondIDStr, unbip(bond.AssetID), dc.acct.host, err)
  6751  		return 0, 0
  6752  	}
  6753  
  6754  	dc.acct.authMtx.Lock()
  6755  	dc.acct.bonds = append(dc.acct.bonds, dbBond)
  6756  	dc.acct.authMtx.Unlock()
  6757  	c.log.Infof("Restored unknown bond %s", bondIDStr)
  6758  	return strength, bondDetails.AssetID
  6759  }
  6760  
  6761  func (c *Core) insertPrepaidBond(dc *dexConnection, bond *msgjson.Bond) {
  6762  	lockTime := bond.Expiry + dc.config().BondExpiry
  6763  	dbBond := &db.Bond{
  6764  		Version:   bond.Version,
  6765  		AssetID:   bond.AssetID,
  6766  		CoinID:    bond.CoinID,
  6767  		LockTime:  lockTime,
  6768  		Strength:  bond.Strength,
  6769  		Confirmed: true,
  6770  	}
  6771  
  6772  	err := c.db.AddBond(dc.acct.host, dbBond)
  6773  	if err != nil {
  6774  		c.log.Errorf("Failed to store pre-paid bond dex %v: %w", dc.acct.host, err)
  6775  	}
  6776  
  6777  	dc.acct.authMtx.Lock()
  6778  	dc.acct.bonds = append(dc.acct.bonds, dbBond)
  6779  	dc.acct.authMtx.Unlock()
  6780  }
  6781  
  6782  func (dc *dexConnection) maxScore() uint32 {
  6783  	if maxScore := dc.config().MaxScore; maxScore > 0 {
  6784  		return maxScore
  6785  	}
  6786  	return 60 // Assume the default for < v2 servers.
  6787  }
  6788  
  6789  // authDEX authenticates the connection for a DEX.
  6790  func (c *Core) authDEX(dc *dexConnection) error {
  6791  	bondAssets, bondExpiry := dc.bondAssets()
  6792  	if bondAssets == nil { // reconnect loop may be running
  6793  		return fmt.Errorf("dex connection not usable prior to config request")
  6794  	}
  6795  
  6796  	// Copy the local bond slices since bondConfirmed will modify them.
  6797  	dc.acct.authMtx.RLock()
  6798  	localActiveBonds := make([]*db.Bond, len(dc.acct.bonds))
  6799  
  6800  	copy(localActiveBonds, dc.acct.bonds)
  6801  	localPendingBonds := make([]*db.Bond, len(dc.acct.pendingBonds))
  6802  	copy(localPendingBonds, dc.acct.pendingBonds)
  6803  	dc.acct.authMtx.RUnlock()
  6804  
  6805  	// Prepare and sign the message for the 'connect' route.
  6806  	acctID := dc.acct.ID()
  6807  	payload := &msgjson.Connect{
  6808  		AccountID:  acctID[:],
  6809  		APIVersion: 0,
  6810  		Time:       uint64(time.Now().UnixMilli()),
  6811  	}
  6812  	sigMsg := payload.Serialize()
  6813  	sig, err := dc.acct.sign(sigMsg)
  6814  	if err != nil {
  6815  		return fmt.Errorf("signing error: %w", err)
  6816  	}
  6817  	payload.SetSig(sig)
  6818  
  6819  	// Send the 'connect' request.
  6820  	req, err := msgjson.NewRequest(dc.NextID(), msgjson.ConnectRoute, payload)
  6821  	if err != nil {
  6822  		return fmt.Errorf("error encoding 'connect' request: %w", err)
  6823  	}
  6824  	errChan := make(chan error, 1)
  6825  	result := new(msgjson.ConnectResult)
  6826  	err = dc.RequestWithTimeout(req, func(msg *msgjson.Message) {
  6827  		errChan <- msg.UnmarshalResult(result)
  6828  	}, DefaultResponseTimeout, func() {
  6829  		errChan <- fmt.Errorf("timed out waiting for '%s' response", msgjson.ConnectRoute)
  6830  	})
  6831  	// Check the request error.
  6832  	if err != nil {
  6833  		return err
  6834  	}
  6835  
  6836  	// Check the response error.
  6837  	err = <-errChan
  6838  	// AccountNotFoundError may signal we have an initial bond to post.
  6839  	var mErr *msgjson.Error
  6840  	if errors.As(err, &mErr) && mErr.Code == msgjson.AccountNotFoundError {
  6841  		for _, dbBond := range localPendingBonds {
  6842  			bondAsset := bondAssets[dbBond.AssetID]
  6843  			if bondAsset == nil {
  6844  				c.log.Warnf("authDEX: No info on bond asset %s. Cannot start postbond waiter.",
  6845  					dex.BipIDSymbol(dbBond.AssetID))
  6846  				continue
  6847  			}
  6848  			c.monitorBondConfs(dc, assetBond(dbBond), bondAsset.Confs)
  6849  		}
  6850  	}
  6851  	if err != nil {
  6852  		return fmt.Errorf("'connect' error: %w", err)
  6853  	}
  6854  
  6855  	// Check the servers response signature.
  6856  	err = dc.acct.checkSig(sigMsg, result.Sig)
  6857  	if err != nil {
  6858  		return newError(signatureErr, "DEX signature validation error: %w", err)
  6859  	}
  6860  
  6861  	// Check active and pending bonds, comparing against result.ActiveBonds. For
  6862  	// pendingBonds, rebroadcast and start waiter to postBond. For
  6863  	// (locally-confirmed) bonds that are not in connectResp.Bonds, postBond.
  6864  
  6865  	// Start by mapping the server-reported bonds:
  6866  	remoteLiveBonds := make(map[string]*msgjson.Bond)
  6867  	for _, bond := range result.ActiveBonds {
  6868  		remoteLiveBonds[bondKey(bond.AssetID, bond.CoinID)] = bond
  6869  	}
  6870  
  6871  	type queuedBond struct {
  6872  		bond  *asset.Bond
  6873  		confs uint32
  6874  	}
  6875  	var toPost, toConfirmLocally []queuedBond
  6876  
  6877  	// Identify bonds we consider live that are either pending or missing from
  6878  	// server. In either case, do c.monitorBondConfs (will be immediate postBond
  6879  	// and bondConfirmed if at required confirmations).
  6880  	for _, bond := range localActiveBonds {
  6881  		if bond.AssetID == account.PrepaidBondID {
  6882  			continue
  6883  		}
  6884  
  6885  		symb := dex.BipIDSymbol(bond.AssetID)
  6886  		bondIDStr := coinIDString(bond.AssetID, bond.CoinID)
  6887  		bondAsset := bondAssets[bond.AssetID]
  6888  		if bondAsset == nil {
  6889  			c.log.Warnf("Server no longer supports %d as a bond asset!", bond.AssetID)
  6890  			continue
  6891  		}
  6892  
  6893  		key := bondKey(bond.AssetID, bond.CoinID)
  6894  		_, found := remoteLiveBonds[key]
  6895  		if found {
  6896  			continue // good, it's live server-side too
  6897  		} // else needs post retry or it's expired
  6898  
  6899  		// Double check bond expiry. It will be moved to the expiredBonds slice
  6900  		// by the rotateBonds goroutine shortly after.
  6901  		if bond.LockTime <= uint64(time.Now().Unix())+bondExpiry+2 {
  6902  			c.log.Debugf("Recently expired bond not reported by server (OK): %s (%s)", bondIDStr, symb)
  6903  			continue
  6904  		}
  6905  
  6906  		c.log.Warnf("Locally-active bond %v (%s) not reported by server",
  6907  			bondIDStr, symb) // unexpected, but postbond again
  6908  
  6909  		// Unknown on server. postBond at required confs.
  6910  		c.log.Infof("Preparing to post locally-confirmed bond %v (%s).", bondIDStr, symb)
  6911  		toPost = append(toPost, queuedBond{assetBond(bond), bondAsset.Confs})
  6912  		continue
  6913  	}
  6914  
  6915  	// Identify bonds we consider pending that are either live or missing from
  6916  	// server. If live on server, do c.bondConfirmed. If missing, do
  6917  	// c.monitorBondConfs.
  6918  	for _, bond := range localPendingBonds {
  6919  		key := bondKey(bond.AssetID, bond.CoinID)
  6920  		symb := dex.BipIDSymbol(bond.AssetID)
  6921  		bondIDStr := coinIDString(bond.AssetID, bond.CoinID)
  6922  
  6923  		bondAsset := bondAssets[bond.AssetID]
  6924  		if bondAsset == nil {
  6925  			c.log.Warnf("Server no longer supports %v as a bond asset!", symb)
  6926  			continue // will retry, eventually refund
  6927  		}
  6928  
  6929  		_, found := remoteLiveBonds[key]
  6930  		if found {
  6931  			// It's live server-side. Confirm it locally (db and slices).
  6932  			toConfirmLocally = append(toConfirmLocally, queuedBond{assetBond(bond), 0})
  6933  			continue
  6934  		}
  6935  
  6936  		c.log.Debugf("Starting coin waiter for pending bond %v (%s)", bondIDStr, symb)
  6937  
  6938  		// Still pending on server. Start waiting for confs.
  6939  		c.log.Debugf("Preparing to post pending bond %v (%s).", bondIDStr, symb)
  6940  		toPost = append(toPost, queuedBond{assetBond(bond), bondAsset.Confs})
  6941  	}
  6942  
  6943  	updatedAssets := make(assetMap)
  6944  	// Flag as authenticated before bondConfirmed and monitorBondConfs, which
  6945  	// may call authDEX if not flagged as such.
  6946  	dc.acct.authMtx.Lock()
  6947  	// Reasons we are here: (1) first auth after login, (2) re-auth on
  6948  	// reconnect, (3) bondConfirmed for the initial bond for the account.
  6949  	// totalReserved is non-zero in #3, but zero in #1. There are no reserves
  6950  	// actions to take in #3 since PostBond reserves prior to post.
  6951  	dc.updateReputation(result.Reputation)
  6952  	rep := dc.acct.rep
  6953  	effectiveTier := rep.EffectiveTier()
  6954  	c.log.Infof("Authenticated connection to %s, acct %v, %d active bonds, %d active orders, %d active matches, score %d, tier %d",
  6955  		dc.acct.host, acctID, len(result.ActiveBonds), len(result.ActiveOrderStatuses), len(result.ActiveMatches), result.Score, effectiveTier)
  6956  	dc.acct.isAuthed = true
  6957  
  6958  	c.log.Debugf("Tier/bonding with %v: effectiveTier = %d, targetTier = %d, bondedTiers = %d, revokedTiers = %d",
  6959  		dc.acct.host, effectiveTier, dc.acct.targetTier, rep.BondedTier, rep.Penalties)
  6960  	dc.acct.authMtx.Unlock()
  6961  
  6962  	c.notify(newReputationNote(dc.acct.host, rep))
  6963  
  6964  	for _, pending := range toPost {
  6965  		c.monitorBondConfs(dc, pending.bond, pending.confs, true)
  6966  	}
  6967  	for _, confirmed := range toConfirmLocally {
  6968  		bond := confirmed.bond
  6969  		bondIDStr := coinIDString(bond.AssetID, bond.CoinID)
  6970  		c.log.Debugf("Confirming pending bond %v that is confirmed server side", bondIDStr)
  6971  		if err = c.bondConfirmed(dc, bond.AssetID, bond.CoinID, &msgjson.PostBondResult{Reputation: &rep} /* no change */); err != nil {
  6972  			c.log.Errorf("Unable to confirm bond %s: %v", bondIDStr, err)
  6973  		}
  6974  	}
  6975  
  6976  	localBondMap := make(map[string]struct{}, len(localActiveBonds)+len(localPendingBonds))
  6977  	for _, dbBond := range localActiveBonds {
  6978  		localBondMap[bondKey(dbBond.AssetID, dbBond.CoinID)] = struct{}{}
  6979  	}
  6980  	for _, dbBond := range localPendingBonds {
  6981  		localBondMap[bondKey(dbBond.AssetID, dbBond.CoinID)] = struct{}{}
  6982  	}
  6983  
  6984  	var unknownBondStrength uint32
  6985  	unknownBondAssetID := -1
  6986  	for _, bond := range result.ActiveBonds {
  6987  		key := bondKey(bond.AssetID, bond.CoinID)
  6988  		if _, found := localBondMap[key]; found {
  6989  			continue
  6990  		}
  6991  		// Server reported a bond we do not know about.
  6992  		ubs, ubaID := c.findBond(dc, bond)
  6993  		unknownBondStrength += ubs
  6994  		if unknownBondAssetID != -1 && ubs != 0 && uint32(unknownBondAssetID) != ubaID {
  6995  			c.log.Warnf("Found unknown bonds for different assets. %s and %s.",
  6996  				unbip(uint32(unknownBondAssetID)), unbip(ubaID))
  6997  		}
  6998  		if ubs != 0 {
  6999  			unknownBondAssetID = int(ubaID)
  7000  		}
  7001  	}
  7002  
  7003  	// If there were unknown bonds and tier is zero, this may be a restored
  7004  	// client and so requires action by the user to set their target bond
  7005  	// tier. Warn the user of this.
  7006  	if unknownBondStrength > 0 && dc.acct.targetTier == 0 {
  7007  		subject, details := c.formatDetails(TopicUnknownBondTierZero, unbip(uint32(unknownBondAssetID)), dc.acct.host)
  7008  		c.notify(newUnknownBondTierZeroNote(subject, details))
  7009  		c.log.Warnf("Unknown bonds for asset %s found for dex %s while target tier is zero.",
  7010  			unbip(uint32(unknownBondAssetID)), dc.acct.host)
  7011  	}
  7012  
  7013  	// Associate the matches with known trades.
  7014  	matches, _, err := dc.parseMatches(result.ActiveMatches, false)
  7015  	if err != nil {
  7016  		c.log.Error(err)
  7017  	}
  7018  
  7019  	exceptions, matchConflicts := dc.compareServerMatches(matches)
  7020  	for oid, matchAnomalies := range exceptions {
  7021  		trade := matchAnomalies.trade
  7022  		missing, extras := matchAnomalies.missing, matchAnomalies.extra
  7023  
  7024  		trade.mtx.Lock()
  7025  
  7026  		// Flag each of the missing matches as revoked.
  7027  		for _, match := range missing {
  7028  			c.log.Warnf("DEX %s did not report active match %s on order %s - assuming revoked, status %v.",
  7029  				dc.acct.host, match, oid, match.Status)
  7030  			// Must have been revoked while we were gone. Flag to allow recovery
  7031  			// and subsequent retirement of the match and parent trade.
  7032  			match.MetaData.Proof.SelfRevoked = true
  7033  			if err := c.db.UpdateMatch(&match.MetaMatch); err != nil {
  7034  				c.log.Errorf("Failed to update missing/revoked match: %v", err)
  7035  			}
  7036  		}
  7037  
  7038  		// Send a "Missing matches" order note if there are missing match message.
  7039  		// Also, check if the now-Revoked matches were the last set of matches that
  7040  		// required sending swaps, and unlock coins if so.
  7041  		if len(missing) > 0 {
  7042  			if trade.maybeReturnCoins() {
  7043  				updatedAssets.count(trade.wallets.fromWallet.AssetID)
  7044  			}
  7045  
  7046  			subject, details := c.formatDetails(TopicMissingMatches,
  7047  				len(missing), makeOrderToken(trade.token()), dc.acct.host)
  7048  			c.notify(newOrderNote(TopicMissingMatches, subject, details, db.ErrorLevel, trade.coreOrderInternal()))
  7049  		}
  7050  
  7051  		// Start negotiation for extra matches for this trade.
  7052  		if len(extras) > 0 {
  7053  			err := trade.negotiate(extras)
  7054  			if err != nil {
  7055  				c.log.Errorf("Error negotiating one or more previously unknown matches for order %s reported by %s on connect: %v",
  7056  					oid, dc.acct.host, err)
  7057  				subject, details := c.formatDetails(TopicMatchResolutionError, len(extras), dc.acct.host, makeOrderToken(trade.token()))
  7058  				c.notify(newOrderNote(TopicMatchResolutionError, subject, details, db.ErrorLevel, trade.coreOrderInternal()))
  7059  			} else {
  7060  				// For taker matches in MakerSwapCast, queue up match status
  7061  				// resolution to retrieve the maker's contract and coin.
  7062  				for _, extra := range extras {
  7063  					if order.MatchSide(extra.Side) == order.Taker && order.MatchStatus(extra.Status) == order.MakerSwapCast {
  7064  						var matchID order.MatchID
  7065  						copy(matchID[:], extra.MatchID)
  7066  						match, found := trade.matches[matchID]
  7067  						if !found {
  7068  							c.log.Errorf("Extra match %v was not registered by negotiate (db error?)", matchID)
  7069  							continue
  7070  						}
  7071  						c.log.Infof("Queueing match status resolution for newly discovered match %v (%s) "+
  7072  							"as taker to MakerSwapCast status.", matchID, match.Status) // had better be NewlyMatched!
  7073  
  7074  						oid := trade.ID()
  7075  						conflicts := matchConflicts[oid]
  7076  						if conflicts == nil {
  7077  							conflicts = &matchStatusConflict{trade: trade}
  7078  							matchConflicts[oid] = conflicts
  7079  						}
  7080  						conflicts.matches = append(conflicts.matches, trade.matches[matchID])
  7081  					}
  7082  				}
  7083  			}
  7084  		}
  7085  
  7086  		trade.mtx.Unlock()
  7087  	}
  7088  
  7089  	// Compare the server-returned active orders with tracked trades, updating
  7090  	// the trade statuses where necessary. This is done after processing the
  7091  	// connect resp matches so that where possible, available match data can be
  7092  	// used to properly set order statuses and filled amount.
  7093  	unknownOrders, reconciledOrdersCount := dc.reconcileTrades(result.ActiveOrderStatuses)
  7094  	if len(unknownOrders) > 0 {
  7095  		subject, details := c.formatDetails(TopicUnknownOrders, len(unknownOrders), dc.acct.host)
  7096  		c.notify(newDEXAuthNote(TopicUnknownOrders, subject, dc.acct.host, false, details, db.Poke))
  7097  	}
  7098  	if reconciledOrdersCount > 0 {
  7099  		subject, details := c.formatDetails(TopicOrdersReconciled, reconciledOrdersCount)
  7100  		c.notify(newDEXAuthNote(TopicOrdersReconciled, subject, dc.acct.host, false, details, db.Poke))
  7101  	}
  7102  
  7103  	if len(matchConflicts) > 0 {
  7104  		var n int
  7105  		for _, c := range matchConflicts {
  7106  			n += len(c.matches)
  7107  		}
  7108  		c.log.Warnf("Beginning match status resolution for %d matches...", n)
  7109  		c.resolveMatchConflicts(dc, matchConflicts)
  7110  	}
  7111  
  7112  	// List and cancel standing limit orders that are in epoch or booked status,
  7113  	// but without funding coins for new matches. This should be done after the
  7114  	// order status resolution done above.
  7115  	var brokenTrades []*trackedTrade
  7116  	dc.tradeMtx.RLock()
  7117  	for _, trade := range dc.trades {
  7118  		if lo, ok := trade.Order.(*order.LimitOrder); !ok || lo.Force != order.StandingTiF {
  7119  			continue // only standing limit orders need to be canceled
  7120  		}
  7121  		trade.mtx.RLock()
  7122  		status := trade.metaData.Status
  7123  		if (status == order.OrderStatusEpoch || status == order.OrderStatusBooked) &&
  7124  			!trade.hasFundingCoins() {
  7125  			brokenTrades = append(brokenTrades, trade)
  7126  		}
  7127  		trade.mtx.RUnlock()
  7128  	}
  7129  	dc.tradeMtx.RUnlock()
  7130  	for _, trade := range brokenTrades {
  7131  		c.log.Warnf("Canceling unfunded standing limit order %v", trade.ID())
  7132  		if err = c.tryCancelTrade(dc, trade); err != nil {
  7133  			c.log.Warnf("Unable to cancel unfunded trade %v: %v", trade.ID(), err)
  7134  		}
  7135  	}
  7136  
  7137  	if len(updatedAssets) > 0 {
  7138  		c.updateBalances(updatedAssets)
  7139  	}
  7140  
  7141  	// Try to cancel unknown orders.
  7142  	for _, oid := range unknownOrders {
  7143  		// Even if we have a record of this order, it is inactive from our
  7144  		// perspective, so we don't try to track it as a trackedTrade.
  7145  		var base, quote uint32
  7146  		if metaUnknown, _ := c.db.Order(oid); metaUnknown != nil {
  7147  			if metaUnknown.Order.Type() != order.LimitOrderType {
  7148  				continue // can't cancel a cancel or market order, it should just go away from server
  7149  			}
  7150  			base, quote = metaUnknown.Order.Base(), metaUnknown.Order.Quote()
  7151  		} else {
  7152  			c.log.Warnf("Order %v not found in DB, so cancelling may fail.", oid)
  7153  			// Otherwise try with (42,0) and hope server will dig for it based
  7154  			// on just the targeted order ID if that market is incorrect.
  7155  			base, quote = 42, 0
  7156  		}
  7157  		preImg, co, _, commitSig, err := c.sendCancelOrder(dc, oid, base, quote)
  7158  		if err != nil {
  7159  			c.log.Errorf("Failed to send cancel for unknown order %v: %v", oid, err)
  7160  			continue
  7161  		}
  7162  		c.log.Warnf("Sent request to cancel unknown order %v, cancel order ID %v", oid, co.ID())
  7163  		dc.blindCancelsMtx.Lock()
  7164  		dc.blindCancels[co.ID()] = preImg
  7165  		dc.blindCancelsMtx.Unlock()
  7166  		close(commitSig) // ready to handle the preimage request
  7167  	}
  7168  
  7169  	return nil
  7170  }
  7171  
  7172  // AssetBalance retrieves and updates the current wallet balance.
  7173  func (c *Core) AssetBalance(assetID uint32) (*WalletBalance, error) {
  7174  	wallet, err := c.connectedWallet(assetID)
  7175  	if err != nil {
  7176  		return nil, fmt.Errorf("%d -> %s wallet error: %w", assetID, unbip(assetID), err)
  7177  	}
  7178  	return c.updateWalletBalance(wallet)
  7179  }
  7180  
  7181  func pluralize(n int) string {
  7182  	if n == 1 {
  7183  		return ""
  7184  	}
  7185  	return "s"
  7186  }
  7187  
  7188  // initialize pulls the known DEXes from the database and attempts to connect
  7189  // and retrieve the DEX configuration.
  7190  func (c *Core) initialize() error {
  7191  	accts, err := c.db.Accounts()
  7192  	if err != nil {
  7193  		return fmt.Errorf("failed to retrieve accounts from database: %w", err)
  7194  	}
  7195  
  7196  	pokes, err := c.db.LoadPokes()
  7197  	c.pokesCache = newPokesCache(pokesCapacity)
  7198  	if err != nil {
  7199  		c.log.Errorf("Error loading pokes from db: %v", err)
  7200  	} else {
  7201  		c.pokesCache.init(pokes)
  7202  	}
  7203  
  7204  	// Start connecting to DEX servers.
  7205  	var liveConns uint32
  7206  	var wg sync.WaitGroup
  7207  	for _, acct := range accts {
  7208  		wg.Add(1)
  7209  		go func(acct *db.AccountInfo) {
  7210  			defer wg.Done()
  7211  			if _, connected := c.connectAccount(acct); connected {
  7212  				atomic.AddUint32(&liveConns, 1)
  7213  			}
  7214  		}(acct)
  7215  	}
  7216  
  7217  	// Load wallet configurations. Actual connections are established on Login.
  7218  	dbWallets, err := c.db.Wallets()
  7219  	if err != nil {
  7220  		c.log.Errorf("error loading wallets from database: %v", err)
  7221  	}
  7222  
  7223  	// Wait for dexConnections to be loaded to ensure they are ready for
  7224  	// authentication when Login is triggered. NOTE/TODO: Login could just as
  7225  	// easily make the connection, but arguably configured DEXs should be
  7226  	// available for unauthenticated operations such as watching market feeds.
  7227  	//
  7228  	// loadWallet requires dexConnections loaded to set proper locked balances
  7229  	// (contracts and bonds), so we don't wait after the dbWallets loop.
  7230  	wg.Wait()
  7231  	c.log.Infof("Connected to %d of %d DEX servers", liveConns, len(accts))
  7232  
  7233  	for _, dbWallet := range dbWallets {
  7234  		if asset.Asset(dbWallet.AssetID) == nil && asset.TokenInfo(dbWallet.AssetID) == nil {
  7235  			c.log.Infof("Wallet for asset %s no longer supported", dex.BipIDSymbol(dbWallet.AssetID))
  7236  			continue
  7237  		}
  7238  		assetID := dbWallet.AssetID
  7239  		wallet, err := c.loadWallet(dbWallet)
  7240  		if err != nil {
  7241  			c.log.Errorf("error loading %d -> %s wallet: %v", assetID, unbip(assetID), err)
  7242  			continue
  7243  		}
  7244  		// Wallet is loaded from the DB, but not yet connected.
  7245  		c.log.Tracef("Loaded %s wallet configuration.", unbip(assetID))
  7246  		c.updateWallet(assetID, wallet)
  7247  	}
  7248  
  7249  	// Check DB for active orders on any DEX.
  7250  	for _, acct := range accts {
  7251  		host, _ := addrHost(acct.Host)
  7252  		activeOrders, _ := c.dbOrders(host) // non-nil error will load 0 orders, and any subsequent db error will cause a shutdown on dex auth or sooner
  7253  		if n := len(activeOrders); n > 0 {
  7254  			c.log.Warnf("\n\n\t ****  IMPORTANT: You have %d active order%s on %s. LOGIN immediately!  **** \n",
  7255  				n, pluralize(n), host)
  7256  		}
  7257  	}
  7258  
  7259  	return nil
  7260  }
  7261  
  7262  // connectAccount makes a connection to the DEX for the given account. If a
  7263  // non-nil dexConnection is returned from newDEXConnection, it was inserted into
  7264  // the conns map even if the connection attempt failed (connected == false), and
  7265  // the connect retry / keepalive loop is active. The intial connection attempt
  7266  // or keepalive loop will not run if acct is disabled.
  7267  func (c *Core) connectAccount(acct *db.AccountInfo) (dc *dexConnection, connected bool) {
  7268  	host, err := addrHost(acct.Host)
  7269  	if err != nil {
  7270  		c.log.Errorf("skipping loading of %s due to address parse error: %v", host, err)
  7271  		return
  7272  	}
  7273  
  7274  	if c.cfg.TheOneHost != "" && c.cfg.TheOneHost != host {
  7275  		c.log.Infof("Running with --onehost = %q.", c.cfg.TheOneHost)
  7276  		return
  7277  	}
  7278  
  7279  	var connectFlag connectDEXFlag
  7280  	if acct.ViewOnly() {
  7281  		connectFlag |= connectDEXFlagViewOnly
  7282  	}
  7283  
  7284  	dc, err = c.newDEXConnection(acct, connectFlag)
  7285  	if err != nil {
  7286  		c.log.Errorf("Unable to prepare DEX %s: %v", host, err)
  7287  		return
  7288  	}
  7289  
  7290  	err = c.startDexConnection(acct, dc)
  7291  	if err != nil {
  7292  		c.log.Errorf("Trouble establishing connection to %s (will retry). Error: %v", host, err)
  7293  	}
  7294  
  7295  	// Connected or not, the dexConnection goes in the conns map now.
  7296  	c.addDexConnection(dc)
  7297  	return dc, err == nil
  7298  }
  7299  
  7300  func (c *Core) dbOrders(host string) ([]*db.MetaOrder, error) {
  7301  	// Prepare active orders, according to the DB.
  7302  	dbOrders, err := c.db.ActiveDEXOrders(host)
  7303  	if err != nil {
  7304  		return nil, fmt.Errorf("database error when fetching orders for %s: %w", host, err)
  7305  	}
  7306  	c.log.Infof("Loaded %d active orders.", len(dbOrders))
  7307  
  7308  	// It's possible for an order to not be active, but still have active matches.
  7309  	// Grab the orders for those too.
  7310  	haveOrder := func(oid order.OrderID) bool {
  7311  		for _, dbo := range dbOrders {
  7312  			if dbo.Order.ID() == oid {
  7313  				return true
  7314  			}
  7315  		}
  7316  		return false
  7317  	}
  7318  
  7319  	activeMatchOrders, err := c.db.DEXOrdersWithActiveMatches(host)
  7320  	if err != nil {
  7321  		return nil, fmt.Errorf("database error fetching active match orders for %s: %w", host, err)
  7322  	}
  7323  	c.log.Infof("Loaded %d active match orders", len(activeMatchOrders))
  7324  	for _, oid := range activeMatchOrders {
  7325  		if haveOrder(oid) {
  7326  			continue
  7327  		}
  7328  		dbOrder, err := c.db.Order(oid)
  7329  		if err != nil {
  7330  			return nil, fmt.Errorf("database error fetching order %s for %s: %w", oid, host, err)
  7331  		}
  7332  		dbOrders = append(dbOrders, dbOrder)
  7333  	}
  7334  
  7335  	return dbOrders, nil
  7336  }
  7337  
  7338  // dbTrackers prepares trackedTrades based on active orders and matches in the
  7339  // database. Since dbTrackers is during the login process when wallets are not yet
  7340  // connected or unlocked, wallets and coins are not added to the returned trackers.
  7341  // Use resumeTrades with the app Crypter to prepare wallets and coins.
  7342  func (c *Core) dbTrackers(dc *dexConnection) (map[order.OrderID]*trackedTrade, error) {
  7343  	// Prepare active orders, according to the DB.
  7344  	dbOrders, err := c.dbOrders(dc.acct.host)
  7345  	if err != nil {
  7346  		return nil, err
  7347  	}
  7348  
  7349  	// Index all of the cancel orders so we can account for them when loading
  7350  	// the trade orders. Whatever remains is orphaned.
  7351  	unknownCancels := make(map[order.OrderID]struct{})
  7352  	for _, dbOrder := range dbOrders {
  7353  		if dbOrder.Order.Type() == order.CancelOrderType {
  7354  			unknownCancels[dbOrder.Order.ID()] = struct{}{}
  7355  		}
  7356  	}
  7357  
  7358  	// For older orders, we'll attempt to get the SwapConf from the server's
  7359  	// asset config. Newer orders will have it stored in the DB.
  7360  	assetSwapConf := func(assetID uint32) uint32 {
  7361  		if asset := dc.assetConfig(assetID); asset != nil {
  7362  			return asset.SwapConf
  7363  		}
  7364  		return 0 // server may be gone
  7365  	}
  7366  
  7367  	trackers := make(map[order.OrderID]*trackedTrade, len(dbOrders))
  7368  	excludeCancelMatches := true
  7369  	for _, dbOrder := range dbOrders {
  7370  		ord := dbOrder.Order
  7371  		oid := ord.ID()
  7372  		// Ignore cancel orders here. They'll be retrieved from LinkedOrder for
  7373  		// trade orders below.
  7374  		if ord.Type() == order.CancelOrderType {
  7375  			continue
  7376  		}
  7377  
  7378  		mktID := marketName(ord.Base(), ord.Quote())
  7379  		if mktConf := dc.marketConfig(mktID); mktConf == nil {
  7380  			c.log.Warnf("Active %s order retrieved for unknown market %s at %v (server status: %v). Loading it anyway.",
  7381  				oid, mktID, dc.acct.host, dc.status())
  7382  		} else {
  7383  			if dbOrder.MetaData.EpochDur == 0 { // do our best for old orders + down dex
  7384  				dbOrder.MetaData.EpochDur = mktConf.EpochLen
  7385  			}
  7386  		}
  7387  		if dbOrder.MetaData.ToSwapConf == 0 { // upgraded with active order :/
  7388  			if dbOrder.Order.Trade().Sell {
  7389  				dbOrder.MetaData.ToSwapConf = assetSwapConf(ord.Quote())
  7390  			} else {
  7391  				dbOrder.MetaData.ToSwapConf = assetSwapConf(ord.Base())
  7392  			}
  7393  		}
  7394  		if dbOrder.MetaData.FromSwapConf == 0 {
  7395  			if dbOrder.Order.Trade().Sell {
  7396  				dbOrder.MetaData.FromSwapConf = assetSwapConf(ord.Base())
  7397  			} else {
  7398  				dbOrder.MetaData.FromSwapConf = assetSwapConf(ord.Quote())
  7399  			}
  7400  		}
  7401  
  7402  		var preImg order.Preimage
  7403  		copy(preImg[:], dbOrder.MetaData.Proof.Preimage)
  7404  		tracker := newTrackedTrade(dbOrder, preImg, dc, c.lockTimeTaker, c.lockTimeMaker,
  7405  			c.db, c.latencyQ, nil, nil, c.notify, c.formatDetails)
  7406  		tracker.readyToTick = false
  7407  		trackers[dbOrder.Order.ID()] = tracker
  7408  
  7409  		// Get matches.
  7410  		dbMatches, err := c.db.MatchesForOrder(oid, excludeCancelMatches)
  7411  		if err != nil {
  7412  			return nil, fmt.Errorf("error loading matches for order %s: %w", oid, err)
  7413  		}
  7414  		var makerCancel *msgjson.Match
  7415  		for _, dbMatch := range dbMatches {
  7416  			// Only trade matches are added to the matches map. Detect and skip
  7417  			// cancel order matches, which have an empty Address field.
  7418  			if dbMatch.Address == "" { // only correct for maker's cancel match
  7419  				// tracker.cancel is set from LinkedOrder with cancelTrade.
  7420  				makerCancel = &msgjson.Match{
  7421  					OrderID:  oid[:],
  7422  					MatchID:  dbMatch.MatchID[:],
  7423  					Quantity: dbMatch.Quantity,
  7424  				}
  7425  				continue
  7426  			}
  7427  			// Make sure that a taker will not prematurely send an
  7428  			// initialization until it is confirmed with the server
  7429  			// that the match is not revoked.
  7430  			checkServerRevoke := dbMatch.Side == order.Taker && dbMatch.Status == order.MakerSwapCast
  7431  			tracker.matches[dbMatch.MatchID] = &matchTracker{
  7432  				prefix:    tracker.Prefix(),
  7433  				trade:     tracker.Trade(),
  7434  				MetaMatch: *dbMatch,
  7435  				// Ensure logging on the first check of counterparty contract
  7436  				// confirms and own contract expiry.
  7437  				counterConfirms:   -1,
  7438  				lastExpireDur:     365 * 24 * time.Hour,
  7439  				checkServerRevoke: checkServerRevoke,
  7440  			}
  7441  		}
  7442  
  7443  		// Load any linked cancel order.
  7444  		cancelID := tracker.metaData.LinkedOrder
  7445  		if cancelID.IsZero() {
  7446  			continue
  7447  		}
  7448  		metaCancel, err := c.db.Order(cancelID)
  7449  		if err != nil {
  7450  			c.log.Errorf("cancel order %s not found for trade %s", cancelID, oid)
  7451  			continue
  7452  		}
  7453  		co, ok := metaCancel.Order.(*order.CancelOrder)
  7454  		if !ok {
  7455  			c.log.Errorf("linked order %s is not a cancel order", cancelID)
  7456  			continue
  7457  		}
  7458  		epochDur := metaCancel.MetaData.EpochDur
  7459  		if epochDur == 0 {
  7460  			epochDur = dbOrder.MetaData.EpochDur // could still be zero this is an old order and server down
  7461  		}
  7462  		var pimg order.Preimage
  7463  		copy(pimg[:], metaCancel.MetaData.Proof.Preimage)
  7464  		err = tracker.cancelTrade(co, pimg, epochDur) // set tracker.cancel and link
  7465  		if err != nil {
  7466  			c.log.Errorf("Error setting cancel order info %s: %v", co.ID(), err)
  7467  		} else {
  7468  			tracker.cancel.matches.maker = makerCancel
  7469  		}
  7470  		delete(unknownCancels, cancelID) // this one is known
  7471  		c.log.Debugf("Loaded cancel order %v for trade %v", cancelID, oid)
  7472  		// TODO: The trackedTrade.cancel.matches is not being repopulated on
  7473  		// startup. The consequences are that the Filled value will not include
  7474  		// the canceled portion, and the *CoreOrder generated by
  7475  		// coreOrderInternal will be Cancelling, but not Canceled. Instead of
  7476  		// using the matchTracker.matches msgjson.Match fields, we should be
  7477  		// storing the match data in the OrderMetaData so that it can be
  7478  		// tracked across sessions.
  7479  	}
  7480  
  7481  	// Retire any remaining cancel orders that don't have active target orders.
  7482  	// This means we somehow already retired the trade, but not the cancel.
  7483  	for cid := range unknownCancels {
  7484  		c.log.Warnf("Retiring orphaned cancel order %v", cid)
  7485  		err = c.db.UpdateOrderStatus(cid, order.OrderStatusRevoked)
  7486  		if err != nil {
  7487  			c.log.Errorf("Failed to update status of orphaned cancel order %v: %v", cid, err)
  7488  		}
  7489  	}
  7490  
  7491  	return trackers, nil
  7492  }
  7493  
  7494  // loadDBTrades load's the active trades from the db, populates the trade's
  7495  // wallets field and some other metadata, and adds the trade to the
  7496  // dexConnection's trades map. Every trade added to the trades map will
  7497  // have wallets set. readyToTick will still be set to false, so resumeTrades
  7498  // must be run before the trades will be processed.
  7499  func (c *Core) loadDBTrades(dc *dexConnection) error {
  7500  	trackers, err := c.dbTrackers(dc)
  7501  	if err != nil {
  7502  		return fmt.Errorf("error retrieving active matches: %w", err)
  7503  	}
  7504  
  7505  	var tradesLoaded uint32
  7506  	for _, tracker := range trackers {
  7507  		if !tracker.isActive() {
  7508  			// In this event, there is a discrepancy between the active criteria
  7509  			// between dbTrackers and isActive that should be resolved.
  7510  			c.log.Warnf("Loaded inactive trade %v from the DB.", tracker.ID())
  7511  			continue
  7512  		}
  7513  
  7514  		trade := tracker.Trade()
  7515  
  7516  		walletSet, assetConfigs, versCompat, err := c.walletSet(dc, tracker.Base(), tracker.Quote(), trade.Sell)
  7517  		if err != nil {
  7518  			err = fmt.Errorf("failed to load wallets for trade ID %s: %w", tracker.ID(), err)
  7519  			subject, details := c.formatDetails(TopicOrderLoadFailure, err)
  7520  			c.notify(newOrderNote(TopicOrderLoadFailure, subject, details, db.ErrorLevel, nil))
  7521  			continue
  7522  		}
  7523  
  7524  		// Every trade in the trades map must have wallets set.
  7525  		tracker.wallets = walletSet
  7526  		dc.tradeMtx.Lock()
  7527  		if _, found := dc.trades[tracker.ID()]; found {
  7528  			dc.tradeMtx.Unlock()
  7529  			continue
  7530  		}
  7531  		dc.trades[tracker.ID()] = tracker
  7532  		dc.tradeMtx.Unlock()
  7533  
  7534  		mktConf := dc.marketConfig(tracker.mktID)
  7535  		if tracker.metaData.EpochDur == 0 { // upgraded with live orders... smart :/
  7536  			if mktConf != nil { // may remain zero if market also vanished
  7537  				tracker.metaData.EpochDur = mktConf.EpochLen
  7538  			}
  7539  		}
  7540  		if tracker.metaData.FromSwapConf == 0 && assetConfigs.fromAsset != nil {
  7541  			tracker.metaData.FromSwapConf = assetConfigs.fromAsset.SwapConf
  7542  		}
  7543  		if tracker.metaData.ToSwapConf == 0 && assetConfigs.toAsset != nil {
  7544  			tracker.metaData.ToSwapConf = assetConfigs.toAsset.SwapConf
  7545  		}
  7546  
  7547  		c.notify(newOrderNote(TopicOrderLoaded, "", "", db.Data, tracker.coreOrder()))
  7548  
  7549  		if mktConf == nil || !versCompat {
  7550  			if tracker.status() < order.OrderStatusExecuted {
  7551  				// Either we couldn't connect at startup and we don't have the
  7552  				// server config, or we have a server config and the market no
  7553  				// longer exists. Either way, revoke the order.
  7554  				tracker.revoke()
  7555  			}
  7556  			tracker.setSelfGoverned(true) // redeem and refund only
  7557  			c.log.Warnf("No server market or incompatible/missing asset configurations for trade %v, market %v, host %v!",
  7558  				tracker.Order.ID(), tracker.mktID, dc.acct.host)
  7559  		} else {
  7560  			tracker.setSelfGoverned(false)
  7561  		}
  7562  
  7563  		tradesLoaded++
  7564  	}
  7565  
  7566  	c.log.Infof("Loaded %d incomplete orders with DEX %v", tradesLoaded, dc.acct.host)
  7567  	return nil
  7568  }
  7569  
  7570  // resumeTrade recovers the state of active matches including loading audit info
  7571  // needed to finish swaps and funding coins needed to create new matches on an order.
  7572  // If both of the wallets needed for this trade are able to be connected and unlocked,
  7573  // readyToTick will be set to true, even if the funding coins for the order could
  7574  // not be found or the audit info could not be loaded.
  7575  func (c *Core) resumeTrade(tracker *trackedTrade, crypter encrypt.Crypter, failed map[uint32]bool, relocks assetMap) bool {
  7576  	notifyErr := func(tracker *trackedTrade, topic Topic, args ...any) {
  7577  		subject, detail := c.formatDetails(topic, args...)
  7578  		c.notify(newOrderNote(topic, subject, detail, db.ErrorLevel, tracker.coreOrderInternal()))
  7579  	}
  7580  
  7581  	// markUnfunded is used to allow an unfunded order to enter the trades map
  7582  	// so that status resolution and match negotiation for unaffected matches
  7583  	// may continue. By not self-revoking, the user may have the opportunity to
  7584  	// resolve any wallet issues that may have lead to a failure to find the
  7585  	// funding coins. Otherwise the server will (or already did) revoke some or
  7586  	// all of the matches and the order itself.
  7587  	markUnfunded := func(trade *trackedTrade, matches []*matchTracker) {
  7588  		// Block negotiating new matches.
  7589  		trade.changeLocked = false
  7590  		trade.coinsLocked = false
  7591  		// Block swap txn attempts on matches needing funds.
  7592  		for _, match := range matches {
  7593  			match.swapErr = errors.New("no funding coins for swap")
  7594  		}
  7595  		// Will not be retired until revoke or cancel of the order and all
  7596  		// matches, which may happen on status resolution after authenticating
  7597  		// with the DEX server, or from a revoke_match/revoke_order notification
  7598  		// after timeout. However, the order should be unconditionally canceled.
  7599  	}
  7600  
  7601  	lockStuff := func() {
  7602  		trade := tracker.Trade()
  7603  		wallets := tracker.wallets
  7604  
  7605  		// Find the least common multiplier to use as the denom for adding
  7606  		// reserve fractions.
  7607  		denom, marketMult, limitMult := lcm(uint64(len(tracker.matches)), tracker.Trade().Quantity)
  7608  		var refundNum, redeemNum uint64
  7609  
  7610  		addMatchRedemption := func(match *matchTracker) {
  7611  			if tracker.isMarketBuy() {
  7612  				redeemNum += marketMult // * 1
  7613  			} else {
  7614  				redeemNum += match.Quantity * limitMult
  7615  			}
  7616  		}
  7617  
  7618  		addMatchRefund := func(match *matchTracker) {
  7619  			if tracker.isMarketBuy() {
  7620  				refundNum += marketMult // * 1
  7621  			} else {
  7622  				refundNum += match.Quantity * limitMult
  7623  			}
  7624  		}
  7625  
  7626  		// If matches haven't redeemed, but the counter-swap has been received,
  7627  		// reload the audit info.
  7628  		var matchesNeedingCoins []*matchTracker
  7629  		for _, match := range tracker.matches {
  7630  			var needsAuditInfo bool
  7631  			var counterSwap []byte
  7632  			if match.Side == order.Maker {
  7633  				if match.Status < order.MakerSwapCast {
  7634  					matchesNeedingCoins = append(matchesNeedingCoins, match)
  7635  				}
  7636  				if match.Status >= order.TakerSwapCast && match.Status < order.MatchConfirmed {
  7637  					needsAuditInfo = true // maker needs AuditInfo for takers contract
  7638  					counterSwap = match.MetaData.Proof.TakerSwap
  7639  				}
  7640  				if match.Status < order.MakerRedeemed {
  7641  					addMatchRedemption(match)
  7642  					addMatchRefund(match)
  7643  				}
  7644  			} else { // Taker
  7645  				if match.Status < order.TakerSwapCast {
  7646  					matchesNeedingCoins = append(matchesNeedingCoins, match)
  7647  				}
  7648  				if match.Status < order.MatchConfirmed && match.Status >= order.MakerSwapCast {
  7649  					needsAuditInfo = true // taker needs AuditInfo for maker's contract
  7650  					counterSwap = match.MetaData.Proof.MakerSwap
  7651  				}
  7652  				if match.Status < order.MakerRedeemed {
  7653  					addMatchRefund(match)
  7654  				}
  7655  				if match.Status < order.MatchComplete {
  7656  					addMatchRedemption(match)
  7657  				}
  7658  			}
  7659  			c.log.Tracef("Trade %v match %v needs coins = %v, needs audit info = %v",
  7660  				tracker.ID(), match.MatchID, len(matchesNeedingCoins) > 0, needsAuditInfo)
  7661  			if needsAuditInfo {
  7662  				// Check for unresolvable states.
  7663  				if len(counterSwap) == 0 {
  7664  					match.swapErr = fmt.Errorf("missing counter-swap, order %s, match %s", tracker.ID(), match)
  7665  					notifyErr(tracker, TopicMatchErrorCoin, match.Side, tracker.token(), match.Status)
  7666  					continue
  7667  				}
  7668  				counterContract := match.MetaData.Proof.CounterContract
  7669  				if len(counterContract) == 0 {
  7670  					match.swapErr = fmt.Errorf("missing counter-contract, order %s, match %s", tracker.ID(), match)
  7671  					notifyErr(tracker, TopicMatchErrorContract, match.Side, tracker.token(), match.Status)
  7672  					continue
  7673  				}
  7674  				counterTxData := match.MetaData.Proof.CounterTxData
  7675  
  7676  				// Note that this does not actually audit the contract's value,
  7677  				// recipient, expiration, or secret hash (if maker), as that was
  7678  				// already done when it was initially stored as CounterScript.
  7679  				auditInfo, err := wallets.toWallet.AuditContract(counterSwap, counterContract, counterTxData, true)
  7680  				if err != nil {
  7681  					// This case is unlikely to happen since the original audit
  7682  					// message handling would have passed the audit based on the
  7683  					// tx data, but it depends on the asset backend.
  7684  					toAssetID := wallets.toWallet.AssetID
  7685  					contractStr := coinIDString(toAssetID, counterSwap)
  7686  					c.log.Warnf("Starting search for counterparty contract %v (%s)", contractStr, unbip(toAssetID))
  7687  					// Start the audit retry waiter. Set swapErr to block tick
  7688  					// actions like counterSwap.Confirmations checks while it is
  7689  					// searching since matchTracker.counterSwap is not yet set.
  7690  					// We may consider removing this if AuditContract is an
  7691  					// offline action for all wallet implementations.
  7692  					match.swapErr = fmt.Errorf("audit in progress, please wait") // don't frighten the users
  7693  					go func(tracker *trackedTrade, match *matchTracker) {
  7694  						auditInfo, err := tracker.searchAuditInfo(match, counterSwap, counterContract, counterTxData)
  7695  						tracker.mtx.Lock()
  7696  						defer tracker.mtx.Unlock()
  7697  						if err != nil { // contract data could be bad, or just already spent (refunded)
  7698  							match.swapErr = fmt.Errorf("audit error: %w", err)
  7699  							// NOTE: This behaviour differs from the audit request handler behaviour for failed audits.
  7700  							// handleAuditRoute does NOT set a swapErr in case a revised audit request is received from
  7701  							// the server. Audit requests are currently NOT resent, so this difference is trivial. IF
  7702  							// a revised audit request did come through though, no further actions will be taken for this
  7703  							// match even if the revised audit passes validation.
  7704  							c.log.Debugf("AuditContract error for match %v status %v, refunded = %v, revoked = %v: %v",
  7705  								match, match.Status, len(match.MetaData.Proof.RefundCoin) > 0,
  7706  								match.MetaData.Proof.IsRevoked(), err)
  7707  							subject, detail := c.formatDetails(TopicMatchRecoveryError,
  7708  								unbip(toAssetID), contractStr, tracker.token(), err)
  7709  							c.notify(newOrderNote(TopicMatchRecoveryError, subject, detail,
  7710  								db.ErrorLevel, tracker.coreOrderInternal())) // tracker.mtx already locked
  7711  							// The match may be revoked by server. Only refund possible now.
  7712  							return
  7713  						}
  7714  						match.counterSwap = auditInfo
  7715  						match.swapErr = nil // unblock tick actions
  7716  						c.log.Infof("Successfully re-validated counterparty contract %v (%s)",
  7717  							contractStr, unbip(toAssetID))
  7718  					}(tracker, match)
  7719  
  7720  					continue // leave auditInfo nil
  7721  				}
  7722  				match.counterSwap = auditInfo
  7723  				continue
  7724  			}
  7725  		}
  7726  
  7727  		if refundNum != 0 {
  7728  			tracker.lockRefundFraction(refundNum, denom)
  7729  		}
  7730  		if redeemNum != 0 {
  7731  			tracker.lockRedemptionFraction(redeemNum, denom)
  7732  		}
  7733  
  7734  		// Active orders and orders with matches with unsent swaps need funding
  7735  		// coin(s). If they are not found, block new matches and swap attempts.
  7736  		needsCoins := len(matchesNeedingCoins) > 0
  7737  		isActive := tracker.metaData.Status == order.OrderStatusBooked || tracker.metaData.Status == order.OrderStatusEpoch
  7738  		if isActive || needsCoins {
  7739  			coinIDs := trade.Coins
  7740  			if len(tracker.metaData.ChangeCoin) != 0 {
  7741  				coinIDs = []order.CoinID{tracker.metaData.ChangeCoin}
  7742  			}
  7743  			tracker.coins = map[string]asset.Coin{} // should already be
  7744  			if len(coinIDs) == 0 {
  7745  				notifyErr(tracker, TopicOrderCoinError, tracker.token())
  7746  				markUnfunded(tracker, matchesNeedingCoins) // bug - no user resolution
  7747  			} else {
  7748  				byteIDs := make([]dex.Bytes, 0, len(coinIDs))
  7749  				for _, cid := range coinIDs {
  7750  					byteIDs = append(byteIDs, []byte(cid))
  7751  				}
  7752  				coins, err := wallets.fromWallet.FundingCoins(byteIDs)
  7753  				if err != nil || len(coins) == 0 {
  7754  					notifyErr(tracker, TopicOrderCoinFetchError, tracker.token(), unbip(wallets.fromWallet.AssetID), err)
  7755  					// Block matches needing funding coins.
  7756  					markUnfunded(tracker, matchesNeedingCoins)
  7757  					// Note: tracker is still added to trades map for (1) status
  7758  					// resolution, (2) continued settlement of matches that no
  7759  					// longer require funding coins, and (3) cancellation in
  7760  					// authDEX if the order is booked.
  7761  					c.log.Warnf("Check the status of your %s wallet and the coins logged above! "+
  7762  						"Resolve the wallet issue if possible and restart Bison Wallet.",
  7763  						strings.ToUpper(unbip(wallets.fromWallet.AssetID)))
  7764  					c.log.Warnf("Unfunded order %v will be canceled on connect, but %d active matches need funding coins!",
  7765  						tracker.ID(), len(matchesNeedingCoins))
  7766  					// If the funding coins are spent or inaccessible, the user
  7767  					// can only wait for match revocation.
  7768  				} else {
  7769  					// NOTE: change and changeLocked are not set even if the
  7770  					// funding coins were loaded from the DB's ChangeCoin.
  7771  					tracker.coinsLocked = true
  7772  					tracker.coins = mapifyCoins(coins)
  7773  				}
  7774  			}
  7775  		}
  7776  
  7777  		tracker.recalcFilled()
  7778  
  7779  		if isActive {
  7780  			tracker.lockRedemptionFraction(trade.Remaining(), trade.Quantity)
  7781  			tracker.lockRefundFraction(trade.Remaining(), trade.Quantity)
  7782  		}
  7783  
  7784  		// Balances should be updated for any orders with locked wallet coins,
  7785  		// or orders with funds locked in contracts.
  7786  		if isActive || needsCoins || tracker.unspentContractAmounts() > 0 {
  7787  			relocks.count(tracker.wallets.fromWallet.AssetID)
  7788  			if _, is := tracker.accountRedeemer(); is {
  7789  				relocks.count(tracker.wallets.toWallet.AssetID)
  7790  			}
  7791  		}
  7792  	}
  7793  
  7794  	tracker.mtx.Lock()
  7795  	defer tracker.mtx.Unlock()
  7796  
  7797  	if tracker.readyToTick {
  7798  		return true
  7799  	}
  7800  
  7801  	if failed[tracker.Base()] || failed[tracker.Quote()] {
  7802  		return false
  7803  	}
  7804  
  7805  	// This should never happen as every wallet added to the trades map has a
  7806  	// walletSet, but this is a good sanity check and also allows tests which
  7807  	// don't have the wallets set to not panic.
  7808  	if tracker.wallets == nil || tracker.wallets.baseWallet == nil || tracker.wallets.quoteWallet == nil {
  7809  		return false
  7810  	}
  7811  
  7812  	err := c.connectAndUnlockResumeTrades(crypter, tracker.wallets.baseWallet, false)
  7813  	if err != nil {
  7814  		failed[tracker.Base()] = true
  7815  		return false
  7816  	}
  7817  
  7818  	err = c.connectAndUnlockResumeTrades(crypter, tracker.wallets.quoteWallet, false)
  7819  	if err != nil {
  7820  		failed[tracker.Quote()] = true
  7821  		return false
  7822  	}
  7823  
  7824  	lockStuff()
  7825  	tracker.readyToTick = true
  7826  	return true
  7827  }
  7828  
  7829  // resumeTrades recovers the states of active trades and matches for all
  7830  // trades in all dexConnection's that are not yet readyToTick. If there are no
  7831  // trades that are not readyToTick, this will be a no-op.
  7832  func (c *Core) resumeTrades(crypter encrypt.Crypter) {
  7833  
  7834  	failed := make(map[uint32]bool)
  7835  	relocks := make(assetMap)
  7836  
  7837  	for _, dc := range c.dexConnections() {
  7838  		for _, tracker := range dc.trackedTrades() {
  7839  			tracker.mtx.RLock()
  7840  			if tracker.readyToTick {
  7841  				tracker.mtx.RUnlock()
  7842  				continue
  7843  			}
  7844  			tracker.mtx.RUnlock()
  7845  
  7846  			if c.resumeTrade(tracker, crypter, failed, relocks) {
  7847  				c.notify(newOrderNote(TopicOrderLoaded, "", "", db.Data, tracker.coreOrder()))
  7848  			} else {
  7849  				tracker.mtx.RLock()
  7850  				err := fmt.Errorf("failed to connect and unlock wallets for trade ID %s", tracker.ID())
  7851  				tracker.mtx.RUnlock()
  7852  				subject, details := c.formatDetails(TopicOrderResumeFailure, err)
  7853  				c.notify(newOrderNote(TopicOrderResumeFailure, subject, details, db.ErrorLevel, nil))
  7854  			}
  7855  		}
  7856  	}
  7857  
  7858  	c.updateBalances(relocks)
  7859  }
  7860  
  7861  // reReserveFunding reserves funding coins for a newly instantiated wallet.
  7862  // reReserveFunding is closely modeled on resumeTrades, so see resumeTrades for
  7863  // docs.
  7864  func (c *Core) reReserveFunding(w *xcWallet) {
  7865  
  7866  	markUnfunded := func(trade *trackedTrade, matches []*matchTracker) {
  7867  		trade.changeLocked = false
  7868  		trade.coinsLocked = false
  7869  		for _, match := range matches {
  7870  			match.swapErr = errors.New("no funding coins for swap")
  7871  		}
  7872  	}
  7873  
  7874  	c.updateBondReserves(w.AssetID)
  7875  
  7876  	for _, dc := range c.dexConnections() {
  7877  		for _, tracker := range dc.trackedTrades() {
  7878  			// TODO: Consider tokens
  7879  			if tracker.Base() != w.AssetID && tracker.Quote() != w.AssetID {
  7880  				continue
  7881  			}
  7882  
  7883  			notifyErr := func(topic Topic, args ...any) {
  7884  				subject, detail := c.formatDetails(topic, args...)
  7885  				c.notify(newOrderNote(topic, subject, detail, db.ErrorLevel, tracker.coreOrderInternal()))
  7886  			}
  7887  
  7888  			trade := tracker.Trade()
  7889  
  7890  			fromID := tracker.Quote()
  7891  			if trade.Sell {
  7892  				fromID = tracker.Base()
  7893  			}
  7894  
  7895  			denom, marketMult, limitMult := lcm(uint64(len(tracker.matches)), trade.Quantity)
  7896  			var refundNum, redeemNum uint64
  7897  
  7898  			addMatchRedemption := func(match *matchTracker) {
  7899  				if tracker.isMarketBuy() {
  7900  					redeemNum += marketMult // * 1
  7901  				} else {
  7902  					redeemNum += match.Quantity * limitMult
  7903  				}
  7904  			}
  7905  
  7906  			addMatchRefund := func(match *matchTracker) {
  7907  				if tracker.isMarketBuy() {
  7908  					refundNum += marketMult // * 1
  7909  				} else {
  7910  					refundNum += match.Quantity * limitMult
  7911  				}
  7912  			}
  7913  
  7914  			isActive := tracker.metaData.Status == order.OrderStatusBooked || tracker.metaData.Status == order.OrderStatusEpoch
  7915  			var matchesNeedingCoins []*matchTracker
  7916  			for _, match := range tracker.matches {
  7917  				if match.Side == order.Maker {
  7918  					if match.Status < order.MakerSwapCast {
  7919  						matchesNeedingCoins = append(matchesNeedingCoins, match)
  7920  					}
  7921  					if match.Status < order.MakerRedeemed {
  7922  						addMatchRedemption(match)
  7923  						addMatchRefund(match)
  7924  					}
  7925  				} else { // Taker
  7926  					if match.Status < order.TakerSwapCast {
  7927  						matchesNeedingCoins = append(matchesNeedingCoins, match)
  7928  					}
  7929  					if match.Status < order.MakerRedeemed {
  7930  						addMatchRefund(match)
  7931  					}
  7932  					if match.Status < order.MatchComplete {
  7933  						addMatchRedemption(match)
  7934  					}
  7935  				}
  7936  			}
  7937  
  7938  			if c.ctx.Err() != nil {
  7939  				return
  7940  			}
  7941  
  7942  			// Prepare funding coins, but don't update tracker until the mutex
  7943  			// is locked.
  7944  			needsCoins := len(matchesNeedingCoins) > 0
  7945  			// nil coins = no locking required, empty coins = something went
  7946  			// wrong, non-empty means locking required.
  7947  			var coins asset.Coins
  7948  			if fromID == w.AssetID && (isActive || needsCoins) {
  7949  				coins = []asset.Coin{} // should already be
  7950  				coinIDs := trade.Coins
  7951  				if len(tracker.metaData.ChangeCoin) != 0 {
  7952  					coinIDs = []order.CoinID{tracker.metaData.ChangeCoin}
  7953  				}
  7954  				if len(coinIDs) == 0 {
  7955  					notifyErr(TopicOrderCoinError, tracker.token())
  7956  					markUnfunded(tracker, matchesNeedingCoins) // bug - no user resolution
  7957  				} else {
  7958  					byteIDs := make([]dex.Bytes, 0, len(coinIDs))
  7959  					for _, cid := range coinIDs {
  7960  						byteIDs = append(byteIDs, []byte(cid))
  7961  					}
  7962  					var err error
  7963  					coins, err = w.FundingCoins(byteIDs)
  7964  					if err != nil || len(coins) == 0 {
  7965  						notifyErr(TopicOrderCoinFetchError, tracker.token(), unbip(fromID), err)
  7966  						c.log.Warnf("(re-reserve) Check the status of your %s wallet and the coins logged above! "+
  7967  							"Resolve the wallet issue if possible and restart Bison Wallet.",
  7968  							strings.ToUpper(unbip(fromID)))
  7969  						c.log.Warnf("(re-reserve) Unfunded order %v will be revoked if %d active matches don't get funding coins!",
  7970  							tracker.ID(), len(matchesNeedingCoins))
  7971  					}
  7972  				}
  7973  			}
  7974  
  7975  			tracker.mtx.Lock()
  7976  
  7977  			// Refund and redemption reserves for active matches. Doing this
  7978  			// under mutex lock, but noting that the underlying calls to
  7979  			// ReReserveRedemption and ReReserveRefund could potentially involve
  7980  			// long-running RPC calls.
  7981  			if fromID == w.AssetID {
  7982  				tracker.refundLocked = 0
  7983  				if refundNum != 0 {
  7984  					tracker.lockRefundFraction(refundNum, denom)
  7985  				}
  7986  			} else {
  7987  				tracker.redemptionLocked = 0
  7988  				if redeemNum != 0 {
  7989  					tracker.lockRedemptionFraction(redeemNum, denom)
  7990  				}
  7991  			}
  7992  
  7993  			// Funding coins
  7994  			if coins != nil {
  7995  				tracker.coinsLocked = len(coins) > 0
  7996  				tracker.coins = mapifyCoins(coins)
  7997  			}
  7998  
  7999  			// Refund and redemption reserves for booked orders.
  8000  
  8001  			tracker.recalcFilled() // Make sure Remaining is accurate.
  8002  
  8003  			if isActive {
  8004  				if fromID == w.AssetID {
  8005  					tracker.lockRefundFraction(trade.Remaining(), trade.Quantity)
  8006  				} else {
  8007  					tracker.lockRedemptionFraction(trade.Remaining(), trade.Quantity)
  8008  				}
  8009  			}
  8010  
  8011  			tracker.mtx.Unlock()
  8012  		}
  8013  	}
  8014  }
  8015  
  8016  // generateDEXMaps creates the associated assets, market and epoch maps of the
  8017  // DEXs from the provided configuration.
  8018  func generateDEXMaps(host string, cfg *msgjson.ConfigResult) (map[uint32]*dex.Asset, map[string]uint64, error) {
  8019  	assets := make(map[uint32]*dex.Asset, len(cfg.Assets))
  8020  	for _, asset := range cfg.Assets {
  8021  		assets[asset.ID] = convertAssetInfo(asset)
  8022  	}
  8023  	// Validate the markets so we don't have to check every time later.
  8024  	for _, mkt := range cfg.Markets {
  8025  		_, ok := assets[mkt.Base]
  8026  		if !ok {
  8027  			return nil, nil, fmt.Errorf("%s reported a market with base "+
  8028  				"asset %d, but did not provide the asset info.", host, mkt.Base)
  8029  		}
  8030  		_, ok = assets[mkt.Quote]
  8031  		if !ok {
  8032  			return nil, nil, fmt.Errorf("%s reported a market with quote "+
  8033  				"asset %d, but did not provide the asset info.", host, mkt.Quote)
  8034  		}
  8035  	}
  8036  
  8037  	epochMap := make(map[string]uint64)
  8038  	for _, mkt := range cfg.Markets {
  8039  		epochMap[mkt.Name] = 0
  8040  	}
  8041  
  8042  	return assets, epochMap, nil
  8043  }
  8044  
  8045  // runMatches runs the sorted matches returned from parseMatches.
  8046  func (c *Core) runMatches(tradeMatches map[order.OrderID]*serverMatches) (assetMap, error) {
  8047  	runMatch := func(sm *serverMatches) (assetMap, error) {
  8048  		updatedAssets := make(assetMap)
  8049  		tracker := sm.tracker
  8050  		oid := tracker.ID()
  8051  
  8052  		// Verify and record any cancel Match targeting this trade.
  8053  		if sm.cancel != nil {
  8054  			err := tracker.processCancelMatch(sm.cancel)
  8055  			if err != nil {
  8056  				return updatedAssets, fmt.Errorf("processCancelMatch for cancel order %v targeting order %v failed: %w",
  8057  					sm.cancel.OrderID, oid, err)
  8058  			}
  8059  		}
  8060  
  8061  		// Begin negotiation for any trade Matches.
  8062  		if len(sm.msgMatches) > 0 {
  8063  			tracker.mtx.Lock()
  8064  			err := tracker.negotiate(sm.msgMatches)
  8065  			tracker.mtx.Unlock()
  8066  			if err != nil {
  8067  				return updatedAssets, fmt.Errorf("negotiate order %v matches failed: %w", oid, err)
  8068  			}
  8069  
  8070  			// Coins may be returned for canceled orders.
  8071  			tracker.mtx.RLock()
  8072  			if tracker.metaData.Status == order.OrderStatusCanceled {
  8073  				updatedAssets.count(tracker.fromAssetID)
  8074  				if _, is := tracker.wallets.toWallet.Wallet.(asset.AccountLocker); is {
  8075  					updatedAssets.count(tracker.wallets.toWallet.AssetID)
  8076  				}
  8077  			}
  8078  			tracker.mtx.RUnlock()
  8079  
  8080  			// Try to tick the trade now, but do not interrupt on error. The
  8081  			// trade will tick again automatically.
  8082  			tickUpdatedAssets, err := c.tick(tracker)
  8083  			updatedAssets.merge(tickUpdatedAssets)
  8084  			if err != nil {
  8085  				return updatedAssets, fmt.Errorf("tick of order %v failed: %w", oid, err)
  8086  			}
  8087  		}
  8088  
  8089  		return updatedAssets, nil
  8090  	}
  8091  
  8092  	// Process the trades concurrently.
  8093  	type runMatchResult struct {
  8094  		updatedAssets assetMap
  8095  		err           error
  8096  	}
  8097  	resultChan := make(chan *runMatchResult)
  8098  	for _, trade := range tradeMatches {
  8099  		go func(trade *serverMatches) {
  8100  			assetsUpdated, err := runMatch(trade)
  8101  			resultChan <- &runMatchResult{assetsUpdated, err}
  8102  		}(trade)
  8103  	}
  8104  
  8105  	errs := newErrorSet("runMatches - ")
  8106  	assetsUpdated := make(assetMap)
  8107  	for range tradeMatches {
  8108  		result := <-resultChan
  8109  		assetsUpdated.merge(result.updatedAssets) // assets might be updated even if an error occurs
  8110  		if result.err != nil {
  8111  			errs.addErr(result.err)
  8112  		}
  8113  	}
  8114  
  8115  	return assetsUpdated, errs.ifAny()
  8116  }
  8117  
  8118  // sendOutdatedClientNotification will send a notification to the UI that
  8119  // indicates the client should be updated to be used with this DEX server.
  8120  func sendOutdatedClientNotification(c *Core, dc *dexConnection) {
  8121  	subject, details := c.formatDetails(TopicUpgradeNeeded, dc.acct.host)
  8122  	c.notify(newUpgradeNote(TopicUpgradeNeeded, subject, details, db.WarningLevel))
  8123  }
  8124  
  8125  func isOnionHost(addr string) bool {
  8126  	host, _, err := net.SplitHostPort(addr)
  8127  	if err != nil {
  8128  		return false
  8129  	}
  8130  	return strings.HasSuffix(host, ".onion")
  8131  }
  8132  
  8133  type connectDEXFlag uint8
  8134  
  8135  const (
  8136  	connectDEXFlagTemporary connectDEXFlag = 1 << iota
  8137  	connectDEXFlagViewOnly
  8138  )
  8139  
  8140  // connectDEX is like connectDEXWithFlag but always creates a full connection
  8141  // for use with a trading account. For a temporary or view-only dexConnection,
  8142  // use connectDEXWithFlag.
  8143  func (c *Core) connectDEX(acctInfo *db.AccountInfo) (*dexConnection, error) {
  8144  	return c.connectDEXWithFlag(acctInfo, 0)
  8145  }
  8146  
  8147  // connectDEXWithFlag establishes a ws connection to a DEX server using the
  8148  // provided account info, but does not authenticate the connection through the
  8149  // 'connect' route. If the connectDEXFlagTemporary bit is set in flag, the
  8150  // c.listen(dc) goroutine is not started so that associated trades are not
  8151  // processed and no incoming requests and notifications are handled. A temporary
  8152  // dexConnection may be used to inspect the config response or check if a (paid)
  8153  // HD account exists with a DEX. If connecting fails, there are no retries. To
  8154  // allow an initial connection error to begin a reconnect loop, either use the
  8155  // connectAccount method, or manually use newDEXConnection and
  8156  // startDexConnection to tolerate initial connection failure.
  8157  func (c *Core) connectDEXWithFlag(acctInfo *db.AccountInfo, flag connectDEXFlag) (*dexConnection, error) {
  8158  	dc, err := c.newDEXConnection(acctInfo, flag)
  8159  	if err != nil {
  8160  		return nil, err
  8161  	}
  8162  
  8163  	err = c.startDexConnection(acctInfo, dc)
  8164  	if err != nil {
  8165  		dc.connMaster.Disconnect() // stop any retry loop for this new connection.
  8166  		return nil, err
  8167  	}
  8168  
  8169  	return dc, nil
  8170  }
  8171  
  8172  // newDEXConnection creates a new valid instance of *dexConnection.
  8173  func (c *Core) newDEXConnection(acctInfo *db.AccountInfo, flag connectDEXFlag) (*dexConnection, error) {
  8174  	// Get the host from the DEX URL.
  8175  	host, err := addrHost(acctInfo.Host)
  8176  	if err != nil {
  8177  		return nil, newError(addressParseErr, "error parsing address: %v", err)
  8178  	}
  8179  	wsURL, err := url.Parse("wss://" + host + "/ws")
  8180  	if err != nil {
  8181  		return nil, newError(addressParseErr, "error parsing ws address from host %s: %w", host, err)
  8182  	}
  8183  
  8184  	listen := flag&connectDEXFlagTemporary == 0
  8185  	viewOnly := flag&connectDEXFlagViewOnly != 0
  8186  	var reporting uint32
  8187  	if listen {
  8188  		reporting = 1
  8189  	}
  8190  
  8191  	dc := &dexConnection{
  8192  		log:               c.log,
  8193  		acct:              newDEXAccount(acctInfo, viewOnly),
  8194  		notify:            c.notify,
  8195  		ticker:            newDexTicker(defaultTickInterval), // updated when server config obtained
  8196  		books:             make(map[string]*bookie),
  8197  		trades:            make(map[order.OrderID]*trackedTrade),
  8198  		cancels:           make(map[order.OrderID]order.OrderID),
  8199  		inFlightOrders:    make(map[uint64]*InFlightOrder),
  8200  		blindCancels:      make(map[order.OrderID]order.Preimage),
  8201  		apiVer:            -1,
  8202  		reportingConnects: reporting,
  8203  		spots:             make(map[string]*msgjson.Spot),
  8204  		connectionStatus:  uint32(comms.Disconnected),
  8205  		// On connect, must set: cfg, epoch, and assets.
  8206  	}
  8207  
  8208  	wsCfg := comms.WsCfg{
  8209  		URL:      wsURL.String(),
  8210  		PingWait: 20 * time.Second, // larger than server's pingPeriod (server/comms/server.go)
  8211  		Cert:     acctInfo.Cert,
  8212  		Logger:   c.log.SubLogger(wsURL.String()),
  8213  	}
  8214  
  8215  	isOnionHost := isOnionHost(wsURL.Host)
  8216  	if isOnionHost || c.cfg.TorProxy != "" {
  8217  		proxyAddr := c.cfg.TorProxy
  8218  		if isOnionHost {
  8219  			if c.cfg.Onion == "" {
  8220  				return nil, errors.New("tor must be configured for .onion addresses")
  8221  			}
  8222  			proxyAddr = c.cfg.Onion
  8223  
  8224  			wsURL.Scheme = "ws"
  8225  			wsCfg.URL = wsURL.String()
  8226  		}
  8227  		proxy := &socks.Proxy{
  8228  			Addr:         proxyAddr,
  8229  			TorIsolation: c.cfg.TorIsolation, // need socks.NewPool with isolation???
  8230  		}
  8231  		wsCfg.NetDialContext = proxy.DialContext
  8232  	}
  8233  
  8234  	wsCfg.ConnectEventFunc = func(status comms.ConnectionStatus) {
  8235  		c.handleConnectEvent(dc, status)
  8236  	}
  8237  	wsCfg.ReconnectSync = func() {
  8238  		go c.handleReconnect(host)
  8239  	}
  8240  
  8241  	// Create a websocket "connection" to the server. (Don't actually connect.)
  8242  	conn, err := c.wsConstructor(&wsCfg)
  8243  	if err != nil {
  8244  		return nil, err
  8245  	}
  8246  
  8247  	dc.WsConn = conn
  8248  	dc.connMaster = dex.NewConnectionMaster(conn)
  8249  
  8250  	return dc, nil
  8251  }
  8252  
  8253  // startDexConnection attempts to connect the provided dexConnection. dc must be
  8254  // a new dexConnection returned from newDEXConnection above. Callers can choose
  8255  // to stop reconnect retries and any current goroutine for the provided
  8256  // dexConnection using dc.connMaster.Disconnect().
  8257  func (c *Core) startDexConnection(acctInfo *db.AccountInfo, dc *dexConnection) error {
  8258  	// Start listening for messages. The listener stops when core shuts down or
  8259  	// the dexConnection's ConnectionMaster is shut down. This goroutine should
  8260  	// be started as long as the reconnect loop is running. It only returns when
  8261  	// the wsConn is stopped.
  8262  	listen := dc.broadcastingConnect() && !dc.acct.isDisabled()
  8263  	if listen {
  8264  		c.wg.Add(1)
  8265  		go c.listen(dc)
  8266  	}
  8267  
  8268  	// Categorize bonds now for sake of expired bonds that need to be refunded.
  8269  	categorizeBonds := func(lockTimeThresh int64) {
  8270  		dc.acct.authMtx.Lock()
  8271  		defer dc.acct.authMtx.Unlock()
  8272  
  8273  		for _, dbBond := range acctInfo.Bonds {
  8274  			if dbBond.Refunded { // maybe don't even load these, but it may be of use for record keeping
  8275  				continue
  8276  			}
  8277  
  8278  			// IDEA: unspent bonds to register with wallet on first connect, if
  8279  			// we need wallet Disconnect to *not* clear reserves.
  8280  			// dc.acct.unreserved = append(dc.acct.unreserved, dbBond)
  8281  
  8282  			bondIDStr := coinIDString(dbBond.AssetID, dbBond.CoinID)
  8283  
  8284  			if int64(dbBond.LockTime) <= lockTimeThresh {
  8285  				c.log.Infof("Loaded expired bond %v. Refund tx: %v", bondIDStr, dbBond.RefundTx)
  8286  				dc.acct.expiredBonds = append(dc.acct.expiredBonds, dbBond)
  8287  				continue
  8288  			}
  8289  
  8290  			if dbBond.Confirmed {
  8291  				// This bond has already been confirmed by the server.
  8292  				c.log.Infof("Loaded active bond %v. BACKUP refund tx: %v", bondIDStr, dbBond.RefundTx)
  8293  				dc.acct.bonds = append(dc.acct.bonds, dbBond)
  8294  				continue
  8295  			}
  8296  
  8297  			// Server has not yet confirmed this bond.
  8298  			c.log.Infof("Loaded pending bond %v. Refund tx: %v", bondIDStr, dbBond.RefundTx)
  8299  			dc.acct.pendingBonds = append(dc.acct.pendingBonds, dbBond)
  8300  
  8301  			// We need to start monitorBondConfs on login since postbond
  8302  			// requires the account keys.
  8303  		}
  8304  
  8305  		// Now in authDEX, we must reconcile the above categorized bonds
  8306  		// according to ConnectResult.Bonds slice.
  8307  	}
  8308  
  8309  	if dc.acct.isDisabled() {
  8310  		// Sort out the bonds with current time to indicate refundable bonds.
  8311  		categorizeBonds(time.Now().Unix())
  8312  		return nil // nothing else to do
  8313  	}
  8314  
  8315  	err := dc.connMaster.Connect(c.ctx)
  8316  	if err != nil {
  8317  		// Sort out the bonds with current time to indicate refundable bonds.
  8318  		categorizeBonds(time.Now().Unix())
  8319  		// Not connected, but reconnect cycle is running. Caller should track
  8320  		// this dexConnection, and a listen goroutine must be running to handle
  8321  		// messages received when the connection is eventually established.
  8322  		return err
  8323  	}
  8324  
  8325  	// Request the market configuration.
  8326  	cfg, err := dc.refreshServerConfig() // handleReconnect must too
  8327  	if err != nil {
  8328  		// Sort out the bonds with current time to indicate refundable bonds.
  8329  		categorizeBonds(time.Now().Unix())
  8330  		if errors.Is(err, outdatedClientErr) {
  8331  			sendOutdatedClientNotification(c, dc)
  8332  		}
  8333  		return err // no dc.acct.dexPubKey
  8334  	}
  8335  	// handleConnectEvent sets dc.connected, even on first connect
  8336  
  8337  	// Given bond config, sort through our db.Bond slice.
  8338  	categorizeBonds(time.Now().Unix() + int64(cfg.BondExpiry))
  8339  
  8340  	if listen {
  8341  		c.log.Infof("Connected to DEX server at %s and listening for messages.", dc.acct.host)
  8342  		go dc.subPriceFeed()
  8343  	} else {
  8344  		c.log.Infof("Connected to DEX server at %s but NOT listening for messages.", dc.acct.host)
  8345  	}
  8346  
  8347  	return nil
  8348  }
  8349  
  8350  // handleReconnect is called when a WsConn indicates that a lost connection has
  8351  // been re-established.
  8352  func (c *Core) handleReconnect(host string) {
  8353  	c.connMtx.RLock()
  8354  	dc, found := c.conns[host]
  8355  	c.connMtx.RUnlock()
  8356  	if !found {
  8357  		c.log.Errorf("handleReconnect: Unable to find previous connection to DEX at %s", host)
  8358  		return
  8359  	}
  8360  
  8361  	// The server's configuration may have changed, so retrieve the current
  8362  	// server configuration.
  8363  	cfg, err := dc.refreshServerConfig()
  8364  	if err != nil {
  8365  		if errors.Is(err, outdatedClientErr) {
  8366  			sendOutdatedClientNotification(c, dc)
  8367  		}
  8368  		c.log.Errorf("handleReconnect: Unable to apply new configuration for DEX at %s: %v", host, err)
  8369  		return
  8370  	}
  8371  	c.notify(newServerConfigUpdateNote(host))
  8372  
  8373  	type market struct { // for book re-subscribe
  8374  		name  string
  8375  		base  uint32
  8376  		quote uint32
  8377  	}
  8378  	mkts := make(map[string]*market, len(dc.cfg.Markets))
  8379  	for _, m := range cfg.Markets {
  8380  		mkts[m.Name] = &market{
  8381  			name:  m.Name,
  8382  			base:  m.Base,
  8383  			quote: m.Quote,
  8384  		}
  8385  	}
  8386  
  8387  	// Update the orders' selfGoverned flag according to the configured markets.
  8388  	for _, trade := range dc.trackedTrades() {
  8389  		// If the server's market is gone, we're on our own, otherwise we are
  8390  		// now free to swap for this order.
  8391  		auto := mkts[trade.mktID] == nil
  8392  		if !auto { // market exists, now check asset config and version
  8393  			baseCfg := dc.assetConfig(trade.Base())
  8394  			auto = baseCfg == nil || !trade.wallets.baseWallet.supportsVer(baseCfg.Version)
  8395  		}
  8396  		if !auto {
  8397  			quoteCfg := dc.assetConfig(trade.Quote())
  8398  			auto = quoteCfg == nil || !trade.wallets.quoteWallet.supportsVer(quoteCfg.Version)
  8399  		}
  8400  
  8401  		if trade.setSelfGoverned(auto) {
  8402  			if auto {
  8403  				c.log.Warnf("DEX %v is MISSING/INCOMPATIBLE market %v for trade %v!", host, trade.mktID, trade.ID())
  8404  			} else {
  8405  				c.log.Infof("DEX %v with market %v restored for trade %v", host, trade.mktID, trade.ID())
  8406  			}
  8407  		}
  8408  		// We could refresh the asset configs in the walletSet, but we'll stick
  8409  		// to what we have recorded in OrderMetaData at time of order placement.
  8410  	}
  8411  
  8412  	go dc.subPriceFeed()
  8413  
  8414  	// If this isn't a view-only connection, authenticate.
  8415  	if !dc.acct.isViewOnly() {
  8416  		if !dc.acct.locked() /* && dc.acct.feePaid() */ {
  8417  			err = c.authDEX(dc)
  8418  			if err != nil {
  8419  				c.log.Errorf("handleReconnect: Unable to authorize DEX at %s: %v", host, err)
  8420  				return
  8421  			}
  8422  		} else {
  8423  			c.log.Infof("Connection to %v established, but you still need to login.", host)
  8424  			// Continue to resubscribe to market fees.
  8425  		}
  8426  	}
  8427  
  8428  	// Now that reconcileTrades has been run in authDEX, make a list of epoch
  8429  	// status orders that should be re-checked in the next epoch because we may
  8430  	// have missed the preimage request while disconnected.
  8431  	epochOrders := make(map[string][]*trackedTrade)
  8432  	for _, trade := range dc.trackedTrades() {
  8433  		if trade.status() == order.OrderStatusEpoch {
  8434  			epochOrders[trade.mktID] = append(epochOrders[trade.mktID], trade)
  8435  		}
  8436  	}
  8437  	for mkt := range epochOrders {
  8438  		trades := epochOrders[mkt] // don't capture loop var below
  8439  		time.AfterFunc(
  8440  			preimageReqTimeout+time.Duration(dc.marketEpochDuration(mkt))*time.Millisecond,
  8441  			func() {
  8442  				if c.ctx.Err() != nil {
  8443  					return // core shut down
  8444  				}
  8445  				var stillEpochOrders []*trackedTrade
  8446  				for _, trade := range trades {
  8447  					if trade.status() == order.OrderStatusEpoch {
  8448  						stillEpochOrders = append(stillEpochOrders, trade)
  8449  					}
  8450  				}
  8451  				if len(stillEpochOrders) > 0 {
  8452  					dc.syncOrderStatuses(stillEpochOrders)
  8453  				}
  8454  			},
  8455  		)
  8456  	}
  8457  
  8458  	resubMkt := func(mkt *market) {
  8459  		// Locate any bookie for this market.
  8460  		booky := dc.bookie(mkt.name)
  8461  		if booky == nil {
  8462  			// Was not previously subscribed with the server for this market.
  8463  			return
  8464  		}
  8465  
  8466  		// Resubscribe since our old subscription was probably lost by the
  8467  		// server when the connection dropped.
  8468  		snap, err := dc.subscribe(mkt.base, mkt.quote)
  8469  		if err != nil {
  8470  			c.log.Errorf("handleReconnect: Failed to Subscribe to market %q 'orderbook': %v", mkt.name, err)
  8471  			return
  8472  		}
  8473  
  8474  		// Create a fresh OrderBook for the bookie.
  8475  		err = booky.Reset(snap)
  8476  		if err != nil {
  8477  			c.log.Errorf("handleReconnect: Failed to Sync market %q order book snapshot: %v", mkt.name, err)
  8478  		}
  8479  
  8480  		// Send a FreshBookAction to the subscribers.
  8481  		booky.send(&BookUpdate{
  8482  			Action:   FreshBookAction,
  8483  			Host:     dc.acct.host,
  8484  			MarketID: mkt.name,
  8485  			Payload: &MarketOrderBook{
  8486  				Base:  mkt.base,
  8487  				Quote: mkt.quote,
  8488  				Book:  booky.book(),
  8489  			},
  8490  		})
  8491  	}
  8492  
  8493  	// For each market, resubscribe to any market books.
  8494  	for _, mkt := range mkts {
  8495  		resubMkt(mkt)
  8496  	}
  8497  }
  8498  
  8499  func (dc *dexConnection) broadcastingConnect() bool {
  8500  	return atomic.LoadUint32(&dc.reportingConnects) == 1
  8501  }
  8502  
  8503  // handleConnectEvent is called when a WsConn indicates that a connection was
  8504  // lost or established.
  8505  //
  8506  // NOTE: Disconnect event notifications may lag behind actual disconnections.
  8507  func (c *Core) handleConnectEvent(dc *dexConnection, status comms.ConnectionStatus) {
  8508  	atomic.StoreUint32(&dc.connectionStatus, uint32(status))
  8509  
  8510  	topic := TopicDEXDisconnected
  8511  	if status == comms.Connected {
  8512  		topic = TopicDEXConnected
  8513  		dc.lastConnectMtx.Lock()
  8514  		dc.lastConnect = time.Now()
  8515  		dc.lastConnectMtx.Unlock()
  8516  	} else {
  8517  		dc.lastConnectMtx.RLock()
  8518  		lastConnect := dc.lastConnect
  8519  		dc.lastConnectMtx.RUnlock()
  8520  		if time.Since(lastConnect) < wsAnomalyDuration {
  8521  			// Increase anomalies count for this connection.
  8522  			count := atomic.AddUint32(&dc.anomaliesCount, 1)
  8523  			if count%wsMaxAnomalyCount == 0 {
  8524  				// Send notification to check connectivity.
  8525  				subject, details := c.formatDetails(TopicDexConnectivity, dc.acct.host)
  8526  				c.notify(newConnEventNote(TopicDexConnectivity, subject, dc.acct.host, dc.status(), details, db.Poke))
  8527  			}
  8528  		} else {
  8529  			atomic.StoreUint32(&dc.anomaliesCount, 0)
  8530  		}
  8531  
  8532  		for _, tracker := range dc.trackedTrades() {
  8533  			tracker.setSelfGoverned(true) // reconnect handles unflagging based on fresh market config
  8534  
  8535  			tracker.mtx.RLock()
  8536  			for _, match := range tracker.matches {
  8537  				// Make sure that a taker will not prematurely send an
  8538  				// initialization until it is confirmed with the server
  8539  				// that the match is not revoked.
  8540  				if match.Side == order.Taker && match.Status == order.MakerSwapCast {
  8541  					match.exceptionMtx.Lock()
  8542  					match.checkServerRevoke = true
  8543  					match.exceptionMtx.Unlock()
  8544  				}
  8545  			}
  8546  			tracker.mtx.RUnlock()
  8547  		}
  8548  	}
  8549  
  8550  	if dc.broadcastingConnect() {
  8551  		subject, details := c.formatDetails(topic, dc.acct.host)
  8552  		dc.notify(newConnEventNote(topic, subject, dc.acct.host, status, details, db.Poke))
  8553  	}
  8554  }
  8555  
  8556  // handleMatchProofMsg is called when a match_proof notification is received.
  8557  func handleMatchProofMsg(c *Core, dc *dexConnection, msg *msgjson.Message) error {
  8558  	var note msgjson.MatchProofNote
  8559  	err := msg.Unmarshal(&note)
  8560  	if err != nil {
  8561  		return fmt.Errorf("match proof note unmarshal error: %w", err)
  8562  	}
  8563  
  8564  	// Expire the epoch
  8565  	dc.setEpoch(note.MarketID, note.Epoch+1)
  8566  
  8567  	book := dc.bookie(note.MarketID)
  8568  	if book == nil {
  8569  		return fmt.Errorf("no order book found with market id %q",
  8570  			note.MarketID)
  8571  	}
  8572  
  8573  	err = book.ValidateMatchProof(note)
  8574  	if err != nil {
  8575  		return fmt.Errorf("match proof validation failed: %w", err)
  8576  	}
  8577  
  8578  	// Validate match_proof commitment checksum for client orders in this epoch.
  8579  	for _, trade := range dc.trackedTrades() {
  8580  		if note.MarketID != trade.mktID {
  8581  			continue
  8582  		}
  8583  
  8584  		// Validation can fail either due to server trying to cheat (by
  8585  		// requesting a preimage before closing the epoch to more orders), or
  8586  		// client losing trades' epoch csums (e.g. due to restarting, since we
  8587  		// don't persistently store these at the moment).
  8588  		//
  8589  		// Just warning the user for now, later on we might wanna revoke the
  8590  		// order if this happens.
  8591  		if err = trade.verifyCSum(note.CSum, note.Epoch); err != nil {
  8592  			c.log.Warnf("Failed to validate commitment checksum for %s epoch %d at %s: %v",
  8593  				note.MarketID, note.Epoch, dc.acct.host, err)
  8594  		}
  8595  	}
  8596  
  8597  	return nil
  8598  }
  8599  
  8600  // handleRevokeOrderMsg is called when a revoke_order message is received.
  8601  func handleRevokeOrderMsg(c *Core, dc *dexConnection, msg *msgjson.Message) error {
  8602  	var revocation msgjson.RevokeOrder
  8603  	err := msg.Unmarshal(&revocation)
  8604  	if err != nil {
  8605  		return fmt.Errorf("revoke order unmarshal error: %w", err)
  8606  	}
  8607  
  8608  	var oid order.OrderID
  8609  	copy(oid[:], revocation.OrderID)
  8610  
  8611  	tracker, isCancel := dc.findOrder(oid)
  8612  	if tracker == nil {
  8613  		return fmt.Errorf("no order found with id %s", oid.String())
  8614  	}
  8615  
  8616  	if isCancel {
  8617  		// Cancel order revoked (e.g. we missed the preimage request). Don't
  8618  		// revoke the targeted order, just unlink the cancel order.
  8619  		c.log.Warnf("Deleting failed cancel order %v that targeted trade order %v", oid, tracker.ID())
  8620  		tracker.deleteCancelOrder()
  8621  		subject, details := c.formatDetails(TopicFailedCancel, tracker.token())
  8622  		c.notify(newOrderNote(TopicFailedCancel, subject, details, db.WarningLevel, tracker.coreOrder()))
  8623  		return nil
  8624  	}
  8625  
  8626  	if tracker.status() == order.OrderStatusRevoked {
  8627  		// Already revoked is expected if entire book was purged in a suspend
  8628  		// ntfn, which emits a gentler and more informative notification.
  8629  		// However, we may not be subscribed to orderbook notifications.
  8630  		return nil
  8631  	}
  8632  	tracker.revoke()
  8633  
  8634  	subject, details := c.formatDetails(TopicOrderRevoked, tracker.token(), tracker.mktID, dc.acct.host)
  8635  	c.notify(newOrderNote(TopicOrderRevoked, subject, details, db.ErrorLevel, tracker.coreOrder()))
  8636  
  8637  	// Update market orders, and the balance to account for unlocked coins.
  8638  	c.updateAssetBalance(tracker.fromAssetID)
  8639  	return nil
  8640  }
  8641  
  8642  // handleRevokeMatchMsg is called when a revoke_match message is received.
  8643  func handleRevokeMatchMsg(c *Core, dc *dexConnection, msg *msgjson.Message) error {
  8644  	var revocation msgjson.RevokeMatch
  8645  	err := msg.Unmarshal(&revocation)
  8646  	if err != nil {
  8647  		return fmt.Errorf("revoke match unmarshal error: %w", err)
  8648  	}
  8649  
  8650  	var oid order.OrderID
  8651  	copy(oid[:], revocation.OrderID)
  8652  
  8653  	tracker, _ := dc.findOrder(oid)
  8654  	if tracker == nil {
  8655  		return fmt.Errorf("no order found with id %s (not an error if you've completed your side of the swap)", oid.String())
  8656  	}
  8657  
  8658  	if len(revocation.MatchID) != order.MatchIDSize {
  8659  		return fmt.Errorf("invalid match ID %v", revocation.MatchID)
  8660  	}
  8661  
  8662  	var matchID order.MatchID
  8663  	copy(matchID[:], revocation.MatchID)
  8664  
  8665  	tracker.mtx.Lock()
  8666  	err = tracker.revokeMatch(matchID, true)
  8667  	tracker.mtx.Unlock()
  8668  	if err != nil {
  8669  		return fmt.Errorf("unable to revoke match %s for order %s: %w", matchID, tracker.ID(), err)
  8670  	}
  8671  
  8672  	// Update market orders, and the balance to account for unlocked coins.
  8673  	c.updateAssetBalance(tracker.fromAssetID)
  8674  	return nil
  8675  }
  8676  
  8677  // handleNotifyMsg is called when a notify notification is received.
  8678  func handleNotifyMsg(c *Core, dc *dexConnection, msg *msgjson.Message) error {
  8679  	var txt string
  8680  	err := msg.Unmarshal(&txt)
  8681  	if err != nil {
  8682  		return fmt.Errorf("notify unmarshal error: %w", err)
  8683  	}
  8684  	subject, details := c.formatDetails(TopicDEXNotification, dc.acct.host, txt)
  8685  	c.notify(newServerNotifyNote(TopicDEXNotification, subject, details, db.WarningLevel))
  8686  	return nil
  8687  }
  8688  
  8689  // handlePenaltyMsg is called when a Penalty notification is received.
  8690  //
  8691  // TODO: Consider other steps needed to take immediately after being banned.
  8692  func handlePenaltyMsg(c *Core, dc *dexConnection, msg *msgjson.Message) error {
  8693  	var note msgjson.PenaltyNote
  8694  	err := msg.Unmarshal(&note)
  8695  	if err != nil {
  8696  		return fmt.Errorf("penalty note unmarshal error: %w", err)
  8697  	}
  8698  	// Check the signature.
  8699  	err = dc.acct.checkSig(note.Serialize(), note.Sig)
  8700  	if err != nil {
  8701  		return newError(signatureErr, "handlePenaltyMsg: DEX signature validation error: %w", err)
  8702  	}
  8703  	t := time.UnixMilli(int64(note.Penalty.Time))
  8704  
  8705  	subject, details := c.formatDetails(TopicPenalized, dc.acct.host, note.Penalty.Rule, t, note.Penalty.Details)
  8706  	c.notify(newServerNotifyNote(TopicPenalized, subject, details, db.WarningLevel))
  8707  	return nil
  8708  }
  8709  
  8710  func handleTierChangeMsg(c *Core, dc *dexConnection, msg *msgjson.Message) error {
  8711  	var tierChanged *msgjson.TierChangedNotification
  8712  	err := msg.Unmarshal(&tierChanged)
  8713  	if err != nil {
  8714  		return fmt.Errorf("tier changed note unmarshal error: %w", err)
  8715  	}
  8716  	if tierChanged == nil {
  8717  		return errors.New("empty message")
  8718  	}
  8719  	// Check the signature.
  8720  	err = dc.acct.checkSig(tierChanged.Serialize(), tierChanged.Sig)
  8721  	if err != nil {
  8722  		return newError(signatureErr, "handleTierChangeMsg: DEX signature validation error: %v", err) // warn?
  8723  	}
  8724  	dc.acct.authMtx.Lock()
  8725  	dc.updateReputation(tierChanged.Reputation)
  8726  	targetTier := dc.acct.targetTier
  8727  	rep := dc.acct.rep
  8728  	dc.acct.authMtx.Unlock()
  8729  	c.log.Infof("Received tierchanged notification from %v for account %v. New tier = %v (target = %d)",
  8730  		dc.acct.host, dc.acct.ID(), tierChanged.Tier, targetTier)
  8731  	c.notify(newReputationNote(dc.acct.host, rep))
  8732  	return nil
  8733  }
  8734  
  8735  func handleScoreChangeMsg(c *Core, dc *dexConnection, msg *msgjson.Message) error {
  8736  	var scoreChange *msgjson.ScoreChangedNotification
  8737  	err := msg.Unmarshal(&scoreChange)
  8738  	if err != nil {
  8739  		return fmt.Errorf("tier changed note unmarshal error: %w", err)
  8740  	}
  8741  	if scoreChange == nil {
  8742  		return errors.New("empty message")
  8743  	}
  8744  
  8745  	// Check the signature.
  8746  	err = dc.acct.checkSig(scoreChange.Serialize(), scoreChange.Sig)
  8747  	if err != nil {
  8748  		return newError(signatureErr, "handleScoreChangeMsg: DEX signature validation error: %v", err) // warn?
  8749  	}
  8750  
  8751  	r := scoreChange.Reputation
  8752  	tier := r.EffectiveTier()
  8753  
  8754  	dc.acct.authMtx.Lock()
  8755  	dc.updateReputation(&r)
  8756  	dc.acct.authMtx.Unlock()
  8757  
  8758  	dc.log.Debugf("Score changed at %s. New score is %d / %d, tier = %d, penalties = %d",
  8759  		dc.acct.host, r.Score, dc.maxScore(), tier, r.Penalties)
  8760  
  8761  	c.notify(newReputationNote(dc.acct.host, r))
  8762  	return nil
  8763  }
  8764  
  8765  func handleBondExpiredMsg(c *Core, dc *dexConnection, msg *msgjson.Message) error {
  8766  	var bondExpired *msgjson.BondExpiredNotification
  8767  	err := msg.Unmarshal(&bondExpired)
  8768  	if err != nil {
  8769  		return fmt.Errorf("bond expired note unmarshal error: %w", err)
  8770  	}
  8771  	if bondExpired == nil {
  8772  		return errors.New("empty message")
  8773  	}
  8774  	// Check the signature.
  8775  	err = dc.acct.checkSig(bondExpired.Serialize(), bondExpired.Sig)
  8776  	if err != nil {
  8777  		return newError(signatureErr, "handleBondExpiredMsg: DEX signature validation error: %v", err) // warn?
  8778  	}
  8779  
  8780  	acctID := dc.acct.ID()
  8781  	if !bytes.Equal(bondExpired.AccountID, acctID[:]) {
  8782  		return fmt.Errorf("invalid account ID %v, expected %v", bondExpired.AccountID, acctID)
  8783  	}
  8784  
  8785  	c.log.Infof("Received bondexpired notification from %v for account %v...", dc.acct.host, acctID)
  8786  
  8787  	return c.bondExpired(dc, bondExpired.AssetID, bondExpired.BondCoinID, bondExpired)
  8788  }
  8789  
  8790  // routeHandler is a handler for a message from the DEX.
  8791  type routeHandler func(*Core, *dexConnection, *msgjson.Message) error
  8792  
  8793  var reqHandlers = map[string]routeHandler{
  8794  	msgjson.PreimageRoute:   handlePreimageRequest,
  8795  	msgjson.MatchRoute:      handleMatchRoute,
  8796  	msgjson.AuditRoute:      handleAuditRoute,
  8797  	msgjson.RedemptionRoute: handleRedemptionRoute, // TODO: to ntfn
  8798  }
  8799  
  8800  var noteHandlers = map[string]routeHandler{
  8801  	msgjson.MatchProofRoute:      handleMatchProofMsg,
  8802  	msgjson.BookOrderRoute:       handleBookOrderMsg,
  8803  	msgjson.EpochOrderRoute:      handleEpochOrderMsg,
  8804  	msgjson.UnbookOrderRoute:     handleUnbookOrderMsg,
  8805  	msgjson.PriceUpdateRoute:     handlePriceUpdateNote,
  8806  	msgjson.UpdateRemainingRoute: handleUpdateRemainingMsg,
  8807  	msgjson.EpochReportRoute:     handleEpochReportMsg,
  8808  	msgjson.SuspensionRoute:      handleTradeSuspensionMsg,
  8809  	msgjson.ResumptionRoute:      handleTradeResumptionMsg,
  8810  	msgjson.NotifyRoute:          handleNotifyMsg,
  8811  	msgjson.PenaltyRoute:         handlePenaltyMsg,
  8812  	msgjson.NoMatchRoute:         handleNoMatchRoute,
  8813  	msgjson.RevokeOrderRoute:     handleRevokeOrderMsg,
  8814  	msgjson.RevokeMatchRoute:     handleRevokeMatchMsg,
  8815  	msgjson.TierChangeRoute:      handleTierChangeMsg,
  8816  	msgjson.ScoreChangeRoute:     handleScoreChangeMsg,
  8817  	msgjson.BondExpiredRoute:     handleBondExpiredMsg,
  8818  }
  8819  
  8820  // listen monitors the DEX websocket connection for server requests and
  8821  // notifications. This should be run as a goroutine. listen will return when
  8822  // either c.ctx is canceled or the Message channel from the dexConnection's
  8823  // MessageSource method is closed. The latter would be the case when the
  8824  // dexConnection's WsConn is shut down / ConnectionMaster stopped.
  8825  func (c *Core) listen(dc *dexConnection) {
  8826  	defer c.wg.Done()
  8827  	msgs := dc.MessageSource() // dc.connMaster.Disconnect closes it e.g. cancel of client/comms.(*wsConn).Connect
  8828  
  8829  	defer dc.ticker.Stop()
  8830  	lastTick := time.Now()
  8831  
  8832  	// Messages must be run in the order in which they are received, but they
  8833  	// should not be blocking or run concurrently. TODO: figure out which if any
  8834  	// can run asynchronously, maybe all.
  8835  	type msgJob struct {
  8836  		hander routeHandler
  8837  		msg    *msgjson.Message
  8838  	}
  8839  	runJob := func(job *msgJob) {
  8840  		tStart := time.Now()
  8841  		defer func() {
  8842  			if pv := recover(); pv != nil {
  8843  				c.log.Criticalf("Uh-oh! Panic while handling message from %v.\n\n"+
  8844  					"Message:\n\n%#v\n\nPanic:\n\n%v\n\nStack:\n\n%v\n\n",
  8845  					dc.acct.host, job.msg, pv, string(debug.Stack()))
  8846  			}
  8847  			if eTime := time.Since(tStart); eTime > 250*time.Millisecond {
  8848  				c.log.Infof("runJob(%v) completed in %v", job.msg.Route, eTime)
  8849  			}
  8850  		}()
  8851  		if err := job.hander(c, dc, job.msg); err != nil {
  8852  			c.log.Errorf("Route '%v' %v handler error (DEX %s): %v", job.msg.Route,
  8853  				job.msg.Type, dc.acct.host, err)
  8854  		}
  8855  	}
  8856  	// Start a single runner goroutine to run jobs one at a time in the order
  8857  	// that they were received. Include the handler goroutine in the WaitGroup
  8858  	// to allow it to complete if the connection master desires.
  8859  	nextJob := make(chan *msgJob, 1024) // start blocking at this cap
  8860  	defer close(nextJob)
  8861  	c.wg.Add(1)
  8862  	go func() {
  8863  		defer c.wg.Done()
  8864  		for job := range nextJob {
  8865  			runJob(job)
  8866  		}
  8867  	}()
  8868  
  8869  	checkTrades := func() {
  8870  		var doneTrades, activeTrades []*trackedTrade
  8871  		// NOTE: Don't lock tradeMtx while also locking a trackedTrade's mtx
  8872  		// since we risk blocking access to the trades map if there is lock
  8873  		// contention for even one trade.
  8874  		for _, trade := range dc.trackedTrades() {
  8875  			if trade.isActive() {
  8876  				activeTrades = append(activeTrades, trade)
  8877  				continue
  8878  			}
  8879  			doneTrades = append(doneTrades, trade)
  8880  		}
  8881  
  8882  		if len(doneTrades) > 0 {
  8883  			dc.tradeMtx.Lock()
  8884  
  8885  			for _, trade := range doneTrades {
  8886  				// Log an error if redemption funds are still reserved.
  8887  				trade.mtx.RLock()
  8888  				redeemLocked := trade.redemptionLocked
  8889  				refundLocked := trade.refundLocked
  8890  				trade.mtx.RUnlock()
  8891  				if redeemLocked > 0 {
  8892  					dc.log.Errorf("retiring order %s with %d > 0 redemption funds locked", trade.ID(), redeemLocked)
  8893  				}
  8894  				if refundLocked > 0 {
  8895  					dc.log.Errorf("retiring order %s with %d > 0 refund funds locked", trade.ID(), refundLocked)
  8896  				}
  8897  
  8898  				c.notify(newOrderNote(TopicOrderRetired, "", "", db.Data, trade.coreOrder()))
  8899  				delete(dc.trades, trade.ID())
  8900  			}
  8901  			dc.tradeMtx.Unlock()
  8902  		}
  8903  
  8904  		// Unlock funding coins for retired orders for good measure, in case
  8905  		// there were not unlocked at an earlier time.
  8906  		updatedAssets := make(assetMap)
  8907  		for _, trade := range doneTrades {
  8908  			trade.mtx.Lock()
  8909  			c.log.Debugf("Retiring inactive order %v in status %v", trade.ID(), trade.metaData.Status)
  8910  			trade.returnCoins()
  8911  			trade.mtx.Unlock()
  8912  			updatedAssets.count(trade.wallets.fromWallet.AssetID)
  8913  		}
  8914  
  8915  		for _, trade := range activeTrades {
  8916  			if c.ctx.Err() != nil { // don't fail each one in sequence if shutting down
  8917  				return
  8918  			}
  8919  			newUpdates, err := c.tick(trade)
  8920  			if err != nil {
  8921  				c.log.Error(err)
  8922  			}
  8923  			updatedAssets.merge(newUpdates)
  8924  		}
  8925  
  8926  		if len(updatedAssets) > 0 {
  8927  			c.updateBalances(updatedAssets)
  8928  		}
  8929  	}
  8930  
  8931  	stopTicks := make(chan struct{})
  8932  	defer close(stopTicks)
  8933  	c.wg.Add(1)
  8934  	go func() {
  8935  		defer c.wg.Done()
  8936  		for {
  8937  			select {
  8938  			case <-dc.ticker.C:
  8939  				sinceLast := time.Since(lastTick)
  8940  				lastTick = time.Now()
  8941  				if sinceLast >= 2*dc.ticker.Dur() {
  8942  					// The app likely just woke up from being suspended. Skip this
  8943  					// tick to let DEX connections reconnect and resync matches.
  8944  					c.log.Warnf("Long delay since previous trade check (just resumed?): %v. "+
  8945  						"Skipping this check to allow reconnect.", sinceLast)
  8946  					continue
  8947  				}
  8948  
  8949  				checkTrades()
  8950  			case <-stopTicks:
  8951  				return
  8952  			case <-c.ctx.Done():
  8953  				return
  8954  			}
  8955  		}
  8956  	}()
  8957  
  8958  out:
  8959  	for {
  8960  		select {
  8961  		case msg, ok := <-msgs:
  8962  			if !ok {
  8963  				c.log.Debugf("listen(dc): Connection terminated for %s.", dc.acct.host)
  8964  				// TODO: This just means that wsConn, which created the
  8965  				// MessageSource channel, was shut down before this loop
  8966  				// returned via ctx.Done. It may be necessary to investigate the
  8967  				// most appropriate normal shutdown sequence (i.e. close all
  8968  				// connections before stopping Core).
  8969  				return
  8970  			}
  8971  
  8972  			var handler routeHandler
  8973  			var found bool
  8974  			switch msg.Type {
  8975  			case msgjson.Request:
  8976  				handler, found = reqHandlers[msg.Route]
  8977  			case msgjson.Notification:
  8978  				handler, found = noteHandlers[msg.Route]
  8979  			case msgjson.Response:
  8980  				// client/comms.wsConn handles responses to requests we sent.
  8981  				c.log.Errorf("A response was received in the message queue: %s", msg)
  8982  				continue
  8983  			default:
  8984  				c.log.Errorf("Invalid message type %d from MessageSource", msg.Type)
  8985  				continue
  8986  			}
  8987  			// Until all the routes have handlers, check for nil too.
  8988  			if !found || handler == nil {
  8989  				c.log.Errorf("No handler found for route '%s'", msg.Route)
  8990  				continue
  8991  			}
  8992  
  8993  			// Queue the handling of this message.
  8994  			nextJob <- &msgJob{handler, msg}
  8995  
  8996  		case <-c.ctx.Done():
  8997  			break out
  8998  		}
  8999  	}
  9000  }
  9001  
  9002  // handlePreimageRequest handles a DEX-originating request for an order
  9003  // preimage. If the order id in the request is not known, it may launch a
  9004  // goroutine to wait for a market/limit/cancel request to finish processing.
  9005  func handlePreimageRequest(c *Core, dc *dexConnection, msg *msgjson.Message) error {
  9006  	req := new(msgjson.PreimageRequest)
  9007  	err := msg.Unmarshal(req)
  9008  	if err != nil {
  9009  		return fmt.Errorf("preimage request parsing error: %w", err)
  9010  	}
  9011  
  9012  	oid, err := order.IDFromBytes(req.OrderID)
  9013  	if err != nil {
  9014  		return err
  9015  	}
  9016  
  9017  	if len(req.Commitment) != order.CommitmentSize {
  9018  		return fmt.Errorf("received preimage request for %s with no corresponding order submission response", oid)
  9019  	}
  9020  
  9021  	// See if we recognize that commitment, and if we do, just wait for the
  9022  	// order ID, and process the request.
  9023  	var commit order.Commitment
  9024  	copy(commit[:], req.Commitment)
  9025  
  9026  	c.sentCommitsMtx.Lock()
  9027  	defer c.sentCommitsMtx.Unlock()
  9028  	commitSig, found := c.sentCommits[commit]
  9029  	if !found { // this is the main benefit of a commitment index
  9030  		return fmt.Errorf("received preimage request for unknown commitment %v, order %v",
  9031  			req.Commitment, oid)
  9032  	}
  9033  	delete(c.sentCommits, commit)
  9034  
  9035  	dc.log.Debugf("Received preimage request for order %v with known commitment %v", oid, commit)
  9036  
  9037  	// Go async while waiting.
  9038  	go func() {
  9039  		// Order request success OR fail closes the channel.
  9040  		<-commitSig
  9041  		if err := processPreimageRequest(c, dc, msg.ID, oid, req.CommitChecksum); err != nil {
  9042  			c.log.Errorf("async processPreimageRequest for %v failed: %v", oid, err)
  9043  		} else {
  9044  			c.log.Debugf("async processPreimageRequest for %v succeeded", oid)
  9045  		}
  9046  	}()
  9047  
  9048  	return nil
  9049  }
  9050  
  9051  func processPreimageRequest(c *Core, dc *dexConnection, reqID uint64, oid order.OrderID, commitChecksum dex.Bytes) error {
  9052  	tracker, isCancel := dc.findOrder(oid)
  9053  	var preImg order.Preimage
  9054  	if tracker == nil {
  9055  		var found bool
  9056  		dc.blindCancelsMtx.Lock()
  9057  		preImg, found = dc.blindCancels[oid]
  9058  		dc.blindCancelsMtx.Unlock()
  9059  		if !found {
  9060  			return fmt.Errorf("no active order found for preimage request for %s", oid)
  9061  		} // delete the entry in match/nomatch
  9062  	} else {
  9063  		// Record the csum if this preimage request is novel, and deny it if
  9064  		// this is a duplicate request with an altered csum.
  9065  		var accept bool
  9066  		if accept, preImg = acceptCsum(tracker, isCancel, commitChecksum); !accept {
  9067  			csumErr := errors.New("invalid csum in duplicate preimage request")
  9068  			resp, err := msgjson.NewResponse(reqID, nil,
  9069  				msgjson.NewError(msgjson.InvalidRequestError, "%v", csumErr))
  9070  			if err != nil {
  9071  				c.log.Errorf("Failed to encode response to denied preimage request: %v", err)
  9072  				return csumErr
  9073  			}
  9074  			err = dc.Send(resp)
  9075  			if err != nil {
  9076  				c.log.Errorf("Failed to send response to denied preimage request: %v", err)
  9077  			}
  9078  			return csumErr
  9079  		}
  9080  	}
  9081  
  9082  	resp, err := msgjson.NewResponse(reqID, &msgjson.PreimageResponse{
  9083  		Preimage: preImg[:],
  9084  	}, nil)
  9085  	if err != nil {
  9086  		return fmt.Errorf("preimage response encoding error: %w", err)
  9087  	}
  9088  	err = dc.Send(resp)
  9089  	if err != nil {
  9090  		return fmt.Errorf("preimage send error: %w", err)
  9091  	}
  9092  
  9093  	if tracker != nil {
  9094  		topic := TopicPreimageSent
  9095  		if isCancel {
  9096  			topic = TopicCancelPreimageSent
  9097  		}
  9098  		c.notify(newOrderNote(topic, "", "", db.Data, tracker.coreOrder()))
  9099  	}
  9100  
  9101  	return nil
  9102  }
  9103  
  9104  // acceptCsum will record the commitment checksum so we can verify that the
  9105  // subsequent match_proof with this order has the same checksum. If it does not,
  9106  // the server may have used the knowledge of this preimage we are sending them
  9107  // now to alter the epoch shuffle. The return value is false if a previous
  9108  // checksum has been recorded that differs from the provided one.
  9109  func acceptCsum(tracker *trackedTrade, isCancel bool, commitChecksum dex.Bytes) (bool, order.Preimage) {
  9110  	// Do not allow csum to be changed once it has been committed to
  9111  	// (initialized to something other than `nil`) because it is probably a
  9112  	// malicious behavior by the server.
  9113  	tracker.csumMtx.Lock()
  9114  	defer tracker.csumMtx.Unlock()
  9115  	if isCancel {
  9116  		if tracker.cancelCsum == nil {
  9117  			tracker.cancelCsum = commitChecksum
  9118  			return true, tracker.cancelPreimg
  9119  		}
  9120  		return bytes.Equal(commitChecksum, tracker.cancelCsum), tracker.cancelPreimg
  9121  	}
  9122  	if tracker.csum == nil {
  9123  		tracker.csum = commitChecksum
  9124  		return true, tracker.preImg
  9125  	}
  9126  
  9127  	return bytes.Equal(commitChecksum, tracker.csum), tracker.preImg
  9128  }
  9129  
  9130  // handleMatchRoute processes the DEX-originating match route request,
  9131  // indicating that a match has been made and needs to be negotiated.
  9132  func handleMatchRoute(c *Core, dc *dexConnection, msg *msgjson.Message) error {
  9133  	msgMatches := make([]*msgjson.Match, 0)
  9134  	err := msg.Unmarshal(&msgMatches)
  9135  	if err != nil {
  9136  		return fmt.Errorf("match request parsing error: %w", err)
  9137  	}
  9138  
  9139  	// TODO: If the dexConnection.acct is locked, prompt the user to login.
  9140  	// Maybe even spin here before failing with no hope of retrying the match
  9141  	// request handling.
  9142  
  9143  	// Acknowledgements MUST be in the same orders as the msgjson.Matches.
  9144  	matches, acks, err := dc.parseMatches(msgMatches, true)
  9145  	if err != nil {
  9146  		// Even one failed match fails them all since the server requires acks
  9147  		// for them all, and in the same order. TODO: consider lifting this
  9148  		// requirement, which requires changes to the server's handling.
  9149  		return err
  9150  	}
  9151  
  9152  	mktIDs := make(map[string]struct{})
  9153  
  9154  	// Warn about new matches for unfunded orders. We still must ack all the
  9155  	// matches in the 'match' request for the server to accept it, although the
  9156  	// server doesn't require match acks. See (*Swapper).processMatchAcks.
  9157  	for oid, srvMatch := range matches {
  9158  		mktIDs[srvMatch.tracker.mktID] = struct{}{}
  9159  		if !srvMatch.tracker.hasFundingCoins() {
  9160  			c.log.Warnf("Received new match for unfunded order %v!", oid)
  9161  			// In runMatches>tracker.negotiate we generate the matchTracker and
  9162  			// set swapErr after updating order status and filled amount, and
  9163  			// storing the match to the DB. It may still be possible for the
  9164  			// user to recover if the issue is just that the wrong wallet is
  9165  			// connected by fixing wallet config and restarting. p.s. Hopefully
  9166  			// we are maker.
  9167  		}
  9168  	}
  9169  
  9170  	resp, err := msgjson.NewResponse(msg.ID, acks, nil)
  9171  	if err != nil {
  9172  		return err
  9173  	}
  9174  
  9175  	// Send the match acknowledgments.
  9176  	err = dc.Send(resp)
  9177  	if err != nil {
  9178  		// Do not bail on the matches on error, just log it.
  9179  		c.log.Errorf("Send match response: %v", err)
  9180  	}
  9181  
  9182  	// Begin match negotiation.
  9183  	updatedAssets, err := c.runMatches(matches)
  9184  	if len(updatedAssets) > 0 {
  9185  		c.updateBalances(updatedAssets)
  9186  	}
  9187  
  9188  	for mktID := range mktIDs {
  9189  		c.checkEpochResolution(dc.acct.host, mktID)
  9190  	}
  9191  
  9192  	return err
  9193  }
  9194  
  9195  // handleNoMatchRoute handles the DEX-originating nomatch request, which is sent
  9196  // when an order does not match during the epoch match cycle.
  9197  func handleNoMatchRoute(c *Core, dc *dexConnection, msg *msgjson.Message) error {
  9198  	nomatchMsg := new(msgjson.NoMatch)
  9199  	err := msg.Unmarshal(nomatchMsg)
  9200  	if err != nil {
  9201  		return fmt.Errorf("nomatch request parsing error: %w", err)
  9202  	}
  9203  	var oid order.OrderID
  9204  	copy(oid[:], nomatchMsg.OrderID)
  9205  
  9206  	tracker, _ := dc.findOrder(oid)
  9207  	if tracker == nil {
  9208  		dc.blindCancelsMtx.Lock()
  9209  		_, found := dc.blindCancels[oid]
  9210  		delete(dc.blindCancels, oid)
  9211  		dc.blindCancelsMtx.Unlock()
  9212  		if found { // if it didn't match, the targeted order isn't booked and we're done
  9213  			c.log.Infof("Blind cancel order %v did not match. Its targeted order is assumed to be unbooked.", oid)
  9214  			return nil
  9215  		}
  9216  		return newError(unknownOrderErr, "nomatch request received for unknown order %v from %s", oid, dc.acct.host)
  9217  	}
  9218  
  9219  	updatedAssets, err := tracker.nomatch(oid)
  9220  	if len(updatedAssets) > 0 {
  9221  		c.updateBalances(updatedAssets)
  9222  	}
  9223  	c.checkEpochResolution(dc.acct.host, tracker.mktID)
  9224  	return err
  9225  }
  9226  
  9227  func (c *Core) schedTradeTick(tracker *trackedTrade) {
  9228  	oid := tracker.ID()
  9229  	c.tickSchedMtx.Lock()
  9230  	defer c.tickSchedMtx.Unlock()
  9231  	if _, found := c.tickSched[oid]; found {
  9232  		return // already going to tick this trade
  9233  	}
  9234  
  9235  	tick := func() {
  9236  		assets, err := c.tick(tracker)
  9237  		if len(assets) > 0 {
  9238  			c.updateBalances(assets)
  9239  		}
  9240  		if err != nil {
  9241  			c.log.Errorf("tick error for order %v: %v", oid, err)
  9242  		}
  9243  	}
  9244  
  9245  	numMatches := len(tracker.activeMatches())
  9246  	switch numMatches {
  9247  	case 0:
  9248  		return
  9249  	case 1:
  9250  		go tick()
  9251  		return
  9252  	default:
  9253  	}
  9254  
  9255  	// Schedule a tick for this trade.
  9256  	delay := 2*time.Second + time.Duration(numMatches)*time.Second/10 // 1 sec extra delay for every 10 active matches
  9257  	if delay > 5*time.Second {
  9258  		delay = 5 * time.Second
  9259  	}
  9260  	c.log.Debugf("Waiting %v to tick trade %v with %d active matches", delay, oid, numMatches)
  9261  	c.tickSched[oid] = time.AfterFunc(delay, func() {
  9262  		c.tickSchedMtx.Lock()
  9263  		defer c.tickSchedMtx.Unlock()
  9264  		defer delete(c.tickSched, oid)
  9265  		tick()
  9266  	})
  9267  }
  9268  
  9269  // handleAuditRoute handles the DEX-originating audit request, which is sent
  9270  // when a match counter-party reports their initiation transaction.
  9271  func handleAuditRoute(c *Core, dc *dexConnection, msg *msgjson.Message) error {
  9272  	audit := new(msgjson.Audit)
  9273  	err := msg.Unmarshal(audit)
  9274  	if err != nil {
  9275  		return fmt.Errorf("audit request parsing error: %w", err)
  9276  	}
  9277  	var oid order.OrderID
  9278  	copy(oid[:], audit.OrderID)
  9279  
  9280  	tracker, _ := dc.findOrder(oid)
  9281  	if tracker == nil {
  9282  		return fmt.Errorf("audit request received for unknown order: %s", string(msg.Payload))
  9283  	}
  9284  	return tracker.processAuditMsg(msg.ID, audit)
  9285  }
  9286  
  9287  // handleRedemptionRoute handles the DEX-originating redemption request, which
  9288  // is sent when a match counter-party reports their redemption transaction.
  9289  func handleRedemptionRoute(c *Core, dc *dexConnection, msg *msgjson.Message) error {
  9290  	redemption := new(msgjson.Redemption)
  9291  	err := msg.Unmarshal(redemption)
  9292  	if err != nil {
  9293  		return fmt.Errorf("redemption request parsing error: %w", err)
  9294  	}
  9295  
  9296  	sigMsg := redemption.Serialize()
  9297  	err = dc.acct.checkSig(sigMsg, redemption.Sig)
  9298  	if err != nil {
  9299  		c.log.Warnf("Server redemption signature error: %v", err) // just warn
  9300  	}
  9301  
  9302  	var oid order.OrderID
  9303  	copy(oid[:], redemption.OrderID)
  9304  
  9305  	tracker, isCancel := dc.findOrder(oid)
  9306  	if tracker != nil {
  9307  		if isCancel {
  9308  			return fmt.Errorf("redemption request received for cancel order %v, match %v (you ok server?)",
  9309  				oid, redemption.MatchID)
  9310  		}
  9311  		err = tracker.processRedemption(msg.ID, redemption)
  9312  		if err != nil {
  9313  			return err
  9314  		}
  9315  		c.schedTradeTick(tracker)
  9316  		return nil
  9317  	}
  9318  
  9319  	// This might be an order we completed on our own as taker without waiting
  9320  	// for redeem information to be provided to us, or as maker we retired the
  9321  	// order after redeeming but before receiving the taker's redeem info that
  9322  	// we don't need except for establishing a complete record of all
  9323  	// transactions in the atomic swap. Check the DB for the order, and if we
  9324  	// were taker with our redeem recorded, send it to the server.
  9325  	matches, err := c.db.MatchesForOrder(oid, true)
  9326  	if err != nil {
  9327  		return err
  9328  	}
  9329  
  9330  	for _, match := range matches {
  9331  		if !bytes.Equal(match.MatchID[:], redemption.MatchID) {
  9332  			continue
  9333  		}
  9334  
  9335  		// Respond to the DEX's redemption request with an ack.
  9336  		err = dc.ack(msg.ID, match.MatchID, redemption)
  9337  		if err != nil {
  9338  			c.log.Warnf("Failed to send redeem ack: %v", err) // just warn
  9339  		}
  9340  
  9341  		// Store the counterparty's redeem coin if we don't already have it
  9342  		// recorded, and if we are the taker, also send our redeem request.
  9343  
  9344  		proof := &match.MetaData.Proof
  9345  
  9346  		ourRedeem := proof.TakerRedeem
  9347  		if match.Side == order.Maker {
  9348  			ourRedeem = proof.MakerRedeem
  9349  		}
  9350  
  9351  		c.log.Debugf("Handling redemption request for inactive order %v, match %v in status %v, side %v "+
  9352  			"(revoked = %v, refunded = %v, redeemed = %v)",
  9353  			oid, match, match.Status, match.Side, proof.IsRevoked(),
  9354  			len(proof.RefundCoin) > 0, len(ourRedeem) > 0)
  9355  
  9356  		// If we are maker, we are being informed of the taker's redeem, so we
  9357  		// just record it TakerRedeem and be done. Presently server does not do
  9358  		// this anymore, but if it does again, we would record this.
  9359  		if match.Side == order.Maker {
  9360  			proof.TakerRedeem = order.CoinID(redemption.CoinID)
  9361  			return c.db.UpdateMatch(match)
  9362  		}
  9363  		// If we are taker, we are being informed of the maker's redeem, but
  9364  		// since we did not have this order actively tracked, that should mean
  9365  		// we found it on our own first and already redeemed. Load up the
  9366  		// details of our redeem and send our redeem request as required even
  9367  		// though it's mostly pointless as the last step.
  9368  
  9369  		// Do some sanity checks considering that this order is NOT active. We
  9370  		// won't actually try to resolve any discrepancy since we have retired
  9371  		// this order by own usual match negotiation process, and server could
  9372  		// just be spamming nonsense, but make some noise in the logs.
  9373  		if len(proof.RefundCoin) > 0 {
  9374  			c.log.Warnf("We have supposedly refunded inactive match %v as taker, "+
  9375  				"but server is telling us the counterparty just redeemed it!", match)
  9376  			// That should imply we have no redeem coin to send, but check.
  9377  		}
  9378  		if len(ourRedeem) == 0 {
  9379  			c.log.Warnf("We have not redeemed inactive match %v as taker (refunded = %v), "+
  9380  				"but server is telling us the counterparty just redeemed ours!",
  9381  				match, len(proof.RefundCoin) > 0) // nothing to send, return
  9382  			return fmt.Errorf("we have no record of our own redeem as taker on match %v", match.MatchID)
  9383  		}
  9384  
  9385  		makerRedeem := order.CoinID(redemption.CoinID)
  9386  		if len(proof.MakerRedeem) == 0 { // findMakersRedemption or processMakersRedemption would have recorded this!
  9387  			c.log.Warnf("We (taker) have no previous record of the maker's redeem for inactive match %v.", match)
  9388  			// proof.MakerRedeem = makerRedeem; _ = c.db.UpdateMatch(match) // maybe, but this is unexpected
  9389  		} else if !bytes.Equal(proof.MakerRedeem, makerRedeem) {
  9390  			c.log.Warnf("We (taker) have a different maker redeem coin already recorded: "+
  9391  				"recorded (%v) != notified (%v)", proof.MakerRedeem, makerRedeem)
  9392  		}
  9393  
  9394  		msgRedeem := &msgjson.Redeem{
  9395  			OrderID: redemption.OrderID,
  9396  			MatchID: redemption.MatchID,
  9397  			CoinID:  dex.Bytes(ourRedeem),
  9398  			Secret:  proof.Secret, // silly for taker, but send it back as required
  9399  		}
  9400  
  9401  		c.wg.Add(1)
  9402  		go func() {
  9403  			defer c.wg.Done()
  9404  			ack := new(msgjson.Acknowledgement)
  9405  			err := dc.signAndRequest(msgRedeem, msgjson.RedeemRoute, ack, 30*time.Second)
  9406  			if err != nil {
  9407  				c.log.Errorf("error sending 'redeem' message: %v", err)
  9408  				return
  9409  			}
  9410  
  9411  			err = dc.acct.checkSig(msgRedeem.Serialize(), ack.Sig)
  9412  			if err != nil {
  9413  				c.log.Errorf("'redeem' ack signature error: %v", err)
  9414  				return
  9415  			}
  9416  
  9417  			c.log.Debugf("Received valid ack for 'redeem' request for match %s", match)
  9418  			auth := &proof.Auth
  9419  			auth.RedeemSig = ack.Sig
  9420  			auth.RedeemStamp = uint64(time.Now().UnixMilli())
  9421  			err = c.db.UpdateMatch(match)
  9422  			if err != nil {
  9423  				c.log.Errorf("error storing redeem ack sig in database: %v", err)
  9424  			}
  9425  		}()
  9426  
  9427  		return nil
  9428  	}
  9429  
  9430  	return fmt.Errorf("redemption request received for unknown order: %s", string(msg.Payload))
  9431  }
  9432  
  9433  // peerChange is called by a wallet backend when the peer count changes or
  9434  // cannot be determined. A wallet state note is always emitted. In addition to
  9435  // recording the number of peers, if the number of peers is 0, the wallet is
  9436  // flagged as not synced. If the number of peers has just dropped to zero, a
  9437  // notification that includes wallet state is emitted with the topic
  9438  // TopicWalletPeersWarning. If the number of peers is >0 and was previously
  9439  // zero, a resync monitor goroutine is launched to poll SyncStatus until the
  9440  // wallet has caught up with its network. The monitor goroutine will regularly
  9441  // emit wallet state notes, and once sync has been restored, a wallet balance
  9442  // note will be emitted. If peerChangeErr is non-nil, numPeers should be zero.
  9443  func (c *Core) peerChange(w *xcWallet, numPeers uint32, peerChangeErr error) {
  9444  	if peerChangeErr != nil {
  9445  		c.log.Warnf("%s wallet communication issue: %q", unbip(w.AssetID), peerChangeErr.Error())
  9446  	} else if numPeers == 0 {
  9447  		c.log.Warnf("Wallet for asset %s has zero network peers!", unbip(w.AssetID))
  9448  	} else {
  9449  		c.log.Tracef("New peer count for asset %s: %v", unbip(w.AssetID), numPeers)
  9450  	}
  9451  
  9452  	ss, err := w.SyncStatus()
  9453  	if err != nil {
  9454  		c.log.Errorf("error getting sync status after peer change: %v", err)
  9455  		return
  9456  	}
  9457  
  9458  	w.mtx.Lock()
  9459  	wasDisconnected := w.peerCount == 0 // excludes no count (-1)
  9460  	w.peerCount = int32(numPeers)
  9461  	w.syncStatus = ss
  9462  	w.mtx.Unlock()
  9463  
  9464  	c.notify(newWalletConfigNote(TopicWalletPeersUpdate, "", "", db.Data, w.state()))
  9465  
  9466  	// When we get peers after having none, start waiting for re-sync, otherwise
  9467  	// leave synced alone. This excludes the unknown state (-1) prior to the
  9468  	// initial peer count report.
  9469  	if wasDisconnected && numPeers > 0 {
  9470  		subject, details := c.formatDetails(TopicWalletPeersRestored, w.Info().Name)
  9471  		c.notify(newWalletConfigNote(TopicWalletPeersRestored, subject, details,
  9472  			db.Success, w.state()))
  9473  		c.startWalletSyncMonitor(w)
  9474  	} else if !ss.Synced {
  9475  		c.startWalletSyncMonitor(w)
  9476  	}
  9477  
  9478  	// Send a WalletStateNote in case Synced or anything else has changed.
  9479  	if atomic.LoadUint32(w.broadcasting) == 1 {
  9480  		if (numPeers == 0 || peerChangeErr != nil) && !wasDisconnected { // was connected or initial report
  9481  			if peerChangeErr != nil {
  9482  				subject, details := c.formatDetails(TopicWalletCommsWarning,
  9483  					w.Info().Name, peerChangeErr.Error())
  9484  				c.notify(newWalletConfigNote(TopicWalletCommsWarning, subject, details,
  9485  					db.ErrorLevel, w.state()))
  9486  			} else {
  9487  				subject, details := c.formatDetails(TopicWalletPeersWarning, w.Info().Name)
  9488  				c.notify(newWalletConfigNote(TopicWalletPeersWarning, subject, details,
  9489  					db.WarningLevel, w.state()))
  9490  			}
  9491  		}
  9492  		c.notify(newWalletStateNote(w.state()))
  9493  	}
  9494  }
  9495  
  9496  // handleWalletNotification processes an asynchronous wallet notification.
  9497  func (c *Core) handleWalletNotification(ni asset.WalletNotification) {
  9498  	switch n := ni.(type) {
  9499  	case *asset.TipChangeNote:
  9500  		c.tipChange(n.AssetID)
  9501  	case *asset.BalanceChangeNote:
  9502  		w, ok := c.wallet(n.AssetID)
  9503  		if !ok {
  9504  			return
  9505  		}
  9506  		contractLockedAmt, orderLockedAmt, bondLockedAmt := c.lockedAmounts(n.AssetID)
  9507  		bal := &WalletBalance{
  9508  			Balance: &db.Balance{
  9509  				Balance: *n.Balance,
  9510  				Stamp:   time.Now(),
  9511  			},
  9512  			OrderLocked:    orderLockedAmt,
  9513  			ContractLocked: contractLockedAmt,
  9514  			BondLocked:     bondLockedAmt,
  9515  		}
  9516  		if err := c.storeAndSendWalletBalance(w, bal); err != nil {
  9517  			c.log.Errorf("Error storing and sending emitted balance: %v", err)
  9518  		}
  9519  		return // Notification sent already.
  9520  	case *asset.ActionRequiredNote:
  9521  		c.requestedActionMtx.Lock()
  9522  		c.requestedActions[n.UniqueID] = n
  9523  		c.requestedActionMtx.Unlock()
  9524  	case *asset.ActionResolvedNote:
  9525  		c.deleteRequestedAction(n.UniqueID)
  9526  	}
  9527  	c.notify(newWalletNote(ni))
  9528  }
  9529  
  9530  // tipChange is called by a wallet backend when the tip block changes, or when
  9531  // a connection error is encountered such that tip change reporting may be
  9532  // adversely affected.
  9533  func (c *Core) tipChange(assetID uint32) {
  9534  	c.log.Tracef("Processing tip change for %s", unbip(assetID))
  9535  	c.waiterMtx.RLock()
  9536  	for id, waiter := range c.blockWaiters {
  9537  		if waiter.assetID != assetID {
  9538  			continue
  9539  		}
  9540  		go func(id string, waiter *blockWaiter) {
  9541  			ok, err := waiter.trigger()
  9542  			if err != nil {
  9543  				waiter.action(err)
  9544  				c.removeWaiter(id)
  9545  				return
  9546  			}
  9547  			if ok {
  9548  				waiter.action(nil)
  9549  				c.removeWaiter(id)
  9550  			}
  9551  		}(id, waiter)
  9552  	}
  9553  	c.waiterMtx.RUnlock()
  9554  
  9555  	assets := make(assetMap)
  9556  	for _, dc := range c.dexConnections() {
  9557  		newUpdates := c.tickAsset(dc, assetID)
  9558  		if len(newUpdates) > 0 {
  9559  			assets.merge(newUpdates)
  9560  		}
  9561  	}
  9562  
  9563  	if _, exists := c.wallet(assetID); exists {
  9564  		// Ensure we always at least update this asset's balance regardless of
  9565  		// trade status changes.
  9566  		assets.count(assetID)
  9567  	}
  9568  	c.updateBalances(assets)
  9569  }
  9570  
  9571  // convertAssetInfo converts from a *msgjson.Asset to the nearly identical
  9572  // *dex.Asset.
  9573  func convertAssetInfo(ai *msgjson.Asset) *dex.Asset {
  9574  	return &dex.Asset{
  9575  		ID:         ai.ID,
  9576  		Symbol:     ai.Symbol,
  9577  		Version:    ai.Version,
  9578  		MaxFeeRate: ai.MaxFeeRate,
  9579  		SwapConf:   uint32(ai.SwapConf),
  9580  		UnitInfo:   ai.UnitInfo,
  9581  	}
  9582  }
  9583  
  9584  // checkSigS256 checks that the message's signature was created with the private
  9585  // key for the provided secp256k1 public key on the sha256 hash of the message.
  9586  func checkSigS256(msg, pkBytes, sigBytes []byte) error {
  9587  	pubKey, err := secp256k1.ParsePubKey(pkBytes)
  9588  	if err != nil {
  9589  		return fmt.Errorf("error decoding secp256k1 PublicKey from bytes: %w", err)
  9590  	}
  9591  	signature, err := ecdsa.ParseDERSignature(sigBytes)
  9592  	if err != nil {
  9593  		return fmt.Errorf("error decoding secp256k1 Signature from bytes: %w", err)
  9594  	}
  9595  	hash := sha256.Sum256(msg)
  9596  	if !signature.Verify(hash[:], pubKey) {
  9597  		return fmt.Errorf("secp256k1 signature verification failed")
  9598  	}
  9599  	return nil
  9600  }
  9601  
  9602  // signMsg hashes and signs the message with the sha256 hash function and the
  9603  // provided private key.
  9604  func signMsg(privKey *secp256k1.PrivateKey, msg []byte) []byte {
  9605  	// NOTE: legacy servers will not accept this signature.
  9606  	hash := sha256.Sum256(msg)
  9607  	return ecdsa.Sign(privKey, hash[:]).Serialize()
  9608  }
  9609  
  9610  // sign signs the msgjson.Signable with the provided private key.
  9611  func sign(privKey *secp256k1.PrivateKey, payload msgjson.Signable) {
  9612  	sigMsg := payload.Serialize()
  9613  	payload.SetSig(signMsg(privKey, sigMsg))
  9614  }
  9615  
  9616  // stampAndSign time stamps the msgjson.Stampable, and signs it with the given
  9617  // private key.
  9618  func stampAndSign(privKey *secp256k1.PrivateKey, payload msgjson.Stampable) {
  9619  	payload.Stamp(uint64(time.Now().UnixMilli()))
  9620  	sign(privKey, payload)
  9621  }
  9622  
  9623  // sendRequest sends a request via the specified ws connection and unmarshals
  9624  // the response into the provided interface.
  9625  // TODO: Modify to accept a context.Context argument so callers can pass core's
  9626  // context to break out of the reply wait when Core starts shutting down.
  9627  func sendRequest(conn comms.WsConn, route string, request, response any, timeout time.Duration) error {
  9628  	reqMsg, err := msgjson.NewRequest(conn.NextID(), route, request)
  9629  	if err != nil {
  9630  		return fmt.Errorf("error encoding %q request: %w", route, err)
  9631  	}
  9632  
  9633  	errChan := make(chan error, 1)
  9634  	err = conn.RequestWithTimeout(reqMsg, func(msg *msgjson.Message) {
  9635  		errChan <- msg.UnmarshalResult(response)
  9636  	}, timeout, func() {
  9637  		errChan <- fmt.Errorf("timed out waiting for %q response (%w)", route, errTimeout) // code this as a timeout! like today!!!
  9638  	})
  9639  	// Check the request error.
  9640  	if err != nil {
  9641  		return err // code this as a send error!
  9642  	}
  9643  
  9644  	// Check the response error.
  9645  	return <-errChan
  9646  }
  9647  
  9648  // newPreimage creates a random order commitment. If you require a matching
  9649  // commitment, generate a Preimage, then Preimage.Commit().
  9650  func newPreimage() (p order.Preimage) {
  9651  	copy(p[:], encode.RandomBytes(order.PreimageSize))
  9652  	return
  9653  }
  9654  
  9655  // messagePrefix converts the order.Prefix to a msgjson.Prefix.
  9656  func messagePrefix(prefix *order.Prefix) *msgjson.Prefix {
  9657  	oType := uint8(msgjson.LimitOrderNum)
  9658  	switch prefix.OrderType {
  9659  	case order.MarketOrderType:
  9660  		oType = msgjson.MarketOrderNum
  9661  	case order.CancelOrderType:
  9662  		oType = msgjson.CancelOrderNum
  9663  	}
  9664  	return &msgjson.Prefix{
  9665  		AccountID:  prefix.AccountID[:],
  9666  		Base:       prefix.BaseAsset,
  9667  		Quote:      prefix.QuoteAsset,
  9668  		OrderType:  oType,
  9669  		ClientTime: uint64(prefix.ClientTime.UnixMilli()),
  9670  		Commit:     prefix.Commit[:],
  9671  	}
  9672  }
  9673  
  9674  // messageTrade converts the order.Trade to a msgjson.Trade, adding the coins.
  9675  func messageTrade(trade *order.Trade, coins []*msgjson.Coin) *msgjson.Trade {
  9676  	side := uint8(msgjson.BuyOrderNum)
  9677  	if trade.Sell {
  9678  		side = msgjson.SellOrderNum
  9679  	}
  9680  	return &msgjson.Trade{
  9681  		Side:     side,
  9682  		Quantity: trade.Quantity,
  9683  		Coins:    coins,
  9684  		Address:  trade.Address,
  9685  	}
  9686  }
  9687  
  9688  // messageCoin converts the []asset.Coin to a []*msgjson.Coin, signing the coin
  9689  // IDs and retrieving the pubkeys too.
  9690  func messageCoins(wallet *xcWallet, coins asset.Coins, redeemScripts []dex.Bytes) ([]*msgjson.Coin, error) {
  9691  	msgCoins := make([]*msgjson.Coin, 0, len(coins))
  9692  	for i, coin := range coins {
  9693  		coinID := coin.ID()
  9694  		pubKeys, sigs, err := wallet.SignMessage(coin, coinID)
  9695  		if err != nil {
  9696  			return nil, fmt.Errorf("%s SignMessage error: %w", unbip(wallet.AssetID), err)
  9697  		}
  9698  		msgCoins = append(msgCoins, &msgjson.Coin{
  9699  			ID:      coinID,
  9700  			PubKeys: pubKeys,
  9701  			Sigs:    sigs,
  9702  			Redeem:  redeemScripts[i],
  9703  		})
  9704  	}
  9705  	return msgCoins, nil
  9706  }
  9707  
  9708  // messageOrder converts an order.Order of any underlying type to an appropriate
  9709  // msgjson type used for submitting the order.
  9710  func messageOrder(ord order.Order, coins []*msgjson.Coin) (string, msgjson.Stampable, *msgjson.Trade) {
  9711  	prefix, trade := ord.Prefix(), ord.Trade()
  9712  	switch o := ord.(type) {
  9713  	case *order.LimitOrder:
  9714  		tifFlag := uint8(msgjson.StandingOrderNum)
  9715  		if o.Force == order.ImmediateTiF {
  9716  			tifFlag = msgjson.ImmediateOrderNum
  9717  		}
  9718  		msgOrd := &msgjson.LimitOrder{
  9719  			Prefix: *messagePrefix(prefix),
  9720  			Trade:  *messageTrade(trade, coins),
  9721  			Rate:   o.Rate,
  9722  			TiF:    tifFlag,
  9723  		}
  9724  		return msgjson.LimitRoute, msgOrd, &msgOrd.Trade
  9725  	case *order.MarketOrder:
  9726  		msgOrd := &msgjson.MarketOrder{
  9727  			Prefix: *messagePrefix(prefix),
  9728  			Trade:  *messageTrade(trade, coins),
  9729  		}
  9730  		return msgjson.MarketRoute, msgOrd, &msgOrd.Trade
  9731  	case *order.CancelOrder:
  9732  		return msgjson.CancelRoute, &msgjson.CancelOrder{
  9733  			Prefix:   *messagePrefix(prefix),
  9734  			TargetID: o.TargetOrderID[:],
  9735  		}, nil
  9736  	default:
  9737  		panic("unknown order type")
  9738  	}
  9739  }
  9740  
  9741  // validateOrderResponse validates the response against the order and the order
  9742  // message, and stamps the order with the ServerTime, giving it a valid OrderID.
  9743  func validateOrderResponse(dc *dexConnection, result *msgjson.OrderResult, ord order.Order, msgOrder msgjson.Stampable) error {
  9744  	if result.ServerTime == 0 {
  9745  		return fmt.Errorf("OrderResult cannot have servertime = 0")
  9746  	}
  9747  	msgOrder.Stamp(result.ServerTime)
  9748  	msg := msgOrder.Serialize()
  9749  	err := dc.acct.checkSig(msg, result.Sig)
  9750  	if err != nil {
  9751  		return fmt.Errorf("signature error. order abandoned")
  9752  	}
  9753  	ord.SetTime(time.UnixMilli(int64(result.ServerTime)))
  9754  	checkID, err := order.IDFromBytes(result.OrderID)
  9755  	if err != nil {
  9756  		return err
  9757  	}
  9758  	oid := ord.ID()
  9759  	if oid != checkID {
  9760  		return fmt.Errorf("failed ID match. order abandoned")
  9761  	}
  9762  	return nil
  9763  }
  9764  
  9765  // parseCert returns the (presumed to be) TLS certificate. If the certI is a
  9766  // string, it will be treated as a filepath and the raw file contents returned.
  9767  // if certI is already a []byte, it is presumed to be the raw file contents, and
  9768  // is returned unmodified.
  9769  func parseCert(host string, certI any, net dex.Network) ([]byte, error) {
  9770  	switch c := certI.(type) {
  9771  	case string:
  9772  		if len(c) == 0 {
  9773  			return CertStore[net][host], nil // not found is ok (try without TLS)
  9774  		}
  9775  		cert, err := os.ReadFile(c)
  9776  		if err != nil {
  9777  			return nil, newError(fileReadErr, "failed to read certificate file from %s: %w", c, err)
  9778  		}
  9779  		return cert, nil
  9780  	case []byte:
  9781  		if len(c) == 0 {
  9782  			return CertStore[net][host], nil // not found is ok (try without TLS)
  9783  		}
  9784  		return c, nil
  9785  	case nil:
  9786  		return CertStore[net][host], nil // not found is ok (try without TLS)
  9787  	}
  9788  	return nil, fmt.Errorf("not a valid certificate type %T", certI)
  9789  }
  9790  
  9791  // WalletLogFilePath returns the path to the wallet's log file.
  9792  func (c *Core) WalletLogFilePath(assetID uint32) (string, error) {
  9793  	wallet, exists := c.wallet(assetID)
  9794  	if !exists {
  9795  		return "", newError(missingWalletErr, "no configured wallet found for %s (%d)",
  9796  			strings.ToUpper(unbip(assetID)), assetID)
  9797  	}
  9798  
  9799  	return wallet.logFilePath()
  9800  }
  9801  
  9802  // WalletRestorationInfo returns information about how to restore the currently
  9803  // loaded wallet for assetID in various external wallet software. This function
  9804  // will return an error if the currently loaded wallet for assetID does not
  9805  // implement the WalletRestorer interface.
  9806  func (c *Core) WalletRestorationInfo(pw []byte, assetID uint32) ([]*asset.WalletRestoration, error) {
  9807  	crypter, err := c.encryptionKey(pw)
  9808  	if err != nil {
  9809  		return nil, fmt.Errorf("WalletRestorationInfo password error: %w", err)
  9810  	}
  9811  	defer crypter.Close()
  9812  
  9813  	seed, _, err := c.assetSeedAndPass(assetID, crypter)
  9814  	if err != nil {
  9815  		return nil, fmt.Errorf("assetSeedAndPass error: %w", err)
  9816  	}
  9817  	defer encode.ClearBytes(seed)
  9818  
  9819  	wallet, found := c.wallet(assetID)
  9820  	if !found {
  9821  		return nil, fmt.Errorf("no wallet configured for asset %d", assetID)
  9822  	}
  9823  
  9824  	restorer, ok := wallet.Wallet.(asset.WalletRestorer)
  9825  	if !ok {
  9826  		return nil, fmt.Errorf("wallet for asset %d doesn't support exporting functionality", assetID)
  9827  	}
  9828  
  9829  	restorationInfo, err := restorer.RestorationInfo(seed)
  9830  	if err != nil {
  9831  		return nil, fmt.Errorf("failed to get restoration info for wallet %w", err)
  9832  	}
  9833  
  9834  	return restorationInfo, nil
  9835  }
  9836  
  9837  // createFile creates a new file and will create the file directory if it does
  9838  // not exist.
  9839  func createFile(fileName string) (*os.File, error) {
  9840  	if fileName == "" {
  9841  		return nil, errors.New("no file path specified for creating")
  9842  	}
  9843  	fileDir := filepath.Dir(fileName)
  9844  	if !dex.FileExists(fileDir) {
  9845  		err := os.MkdirAll(fileDir, 0755)
  9846  		if err != nil {
  9847  			return nil, fmt.Errorf("os.MkdirAll error: %w", err)
  9848  		}
  9849  	}
  9850  	fileName = dex.CleanAndExpandPath(fileName)
  9851  	// Errors if file exists.
  9852  	f, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
  9853  	if err != nil {
  9854  		return nil, err
  9855  	}
  9856  	return f, nil
  9857  }
  9858  
  9859  func (c *Core) deleteOrderFn(ordersFileStr string) (perOrderFn func(*db.MetaOrder) error, cleanUpFn func() error, err error) {
  9860  	ordersFile, err := createFile(ordersFileStr)
  9861  	if err != nil {
  9862  		return nil, nil, fmt.Errorf("problem opening orders file: %v", err)
  9863  	}
  9864  	csvWriter := csv.NewWriter(ordersFile)
  9865  	csvWriter.UseCRLF = runtime.GOOS == "windows"
  9866  	err = csvWriter.Write([]string{
  9867  		"Host",
  9868  		"Order ID",
  9869  		"Base",
  9870  		"Quote",
  9871  		"Base Quantity",
  9872  		"Order Rate",
  9873  		"Actual Rate",
  9874  		"Base Fees",
  9875  		"Base Fees Asset",
  9876  		"Quote Fees",
  9877  		"Quote Fees Asset",
  9878  		"Type",
  9879  		"Side",
  9880  		"Time in Force",
  9881  		"Status",
  9882  		"TargetOrderID",
  9883  		"Filled (%)",
  9884  		"Settled (%)",
  9885  		"Time",
  9886  	})
  9887  	if err != nil {
  9888  		ordersFile.Close()
  9889  		return nil, nil, fmt.Errorf("error writing CSV: %v", err)
  9890  	}
  9891  	csvWriter.Flush()
  9892  	err = csvWriter.Error()
  9893  	if err != nil {
  9894  		ordersFile.Close()
  9895  		return nil, nil, fmt.Errorf("error writing CSV: %v", err)
  9896  	}
  9897  	return func(ord *db.MetaOrder) error {
  9898  		cord := coreOrderFromTrade(ord.Order, ord.MetaData)
  9899  
  9900  		baseUnitInfo, err := asset.UnitInfo(cord.BaseID)
  9901  		if err != nil {
  9902  			return fmt.Errorf("unable to get base unit info for %v: %v", cord.BaseSymbol, err)
  9903  		}
  9904  
  9905  		baseFeeAssetSymbol := unbip(cord.BaseID)
  9906  		baseFeeUnitInfo := baseUnitInfo
  9907  		if baseToken := asset.TokenInfo(cord.BaseID); baseToken != nil {
  9908  			baseFeeAssetSymbol = unbip(baseToken.ParentID)
  9909  			baseFeeUnitInfo, err = asset.UnitInfo(baseToken.ParentID)
  9910  			if err != nil {
  9911  				return fmt.Errorf("unable to get base fee unit info for %v: %v", baseToken.ParentID, err)
  9912  			}
  9913  		}
  9914  
  9915  		quoteUnitInfo, err := asset.UnitInfo(cord.QuoteID)
  9916  		if err != nil {
  9917  			return fmt.Errorf("unable to get quote unit info for %v: %v", cord.QuoteSymbol, err)
  9918  		}
  9919  
  9920  		quoteFeeAssetSymbol := unbip(cord.QuoteID)
  9921  		quoteFeeUnitInfo := quoteUnitInfo
  9922  		if quoteToken := asset.TokenInfo(cord.QuoteID); quoteToken != nil {
  9923  			quoteFeeAssetSymbol = unbip(quoteToken.ParentID)
  9924  			quoteFeeUnitInfo, err = asset.UnitInfo(quoteToken.ParentID)
  9925  			if err != nil {
  9926  				return fmt.Errorf("unable to get quote fee unit info for %v: %v", quoteToken.ParentID, err)
  9927  			}
  9928  		}
  9929  
  9930  		ordReader := &OrderReader{
  9931  			Order:               cord,
  9932  			BaseUnitInfo:        baseUnitInfo,
  9933  			BaseFeeUnitInfo:     baseFeeUnitInfo,
  9934  			BaseFeeAssetSymbol:  baseFeeAssetSymbol,
  9935  			QuoteUnitInfo:       quoteUnitInfo,
  9936  			QuoteFeeUnitInfo:    quoteFeeUnitInfo,
  9937  			QuoteFeeAssetSymbol: quoteFeeAssetSymbol,
  9938  		}
  9939  
  9940  		timestamp := time.UnixMilli(int64(cord.Stamp)).Local().Format(time.RFC3339Nano)
  9941  		err = csvWriter.Write([]string{
  9942  			cord.Host,                     // Host
  9943  			ord.Order.ID().String(),       // Order ID
  9944  			cord.BaseSymbol,               // Base
  9945  			cord.QuoteSymbol,              // Quote
  9946  			ordReader.BaseQtyString(),     // Base Quantity
  9947  			ordReader.SimpleRateString(),  // Order Rate
  9948  			ordReader.AverageRateString(), // Actual Rate
  9949  			ordReader.BaseAssetFees(),     // Base Fees
  9950  			ordReader.BaseFeeSymbol(),     // Base Fees Asset
  9951  			ordReader.QuoteAssetFees(),    // Quote Fees
  9952  			ordReader.QuoteFeeSymbol(),    // Quote Fees Asset
  9953  			ordReader.Type.String(),       // Type
  9954  			ordReader.SideString(),        // Side
  9955  			cord.TimeInForce.String(),     // Time in Force
  9956  			ordReader.StatusString(),      // Status
  9957  			cord.TargetOrderID.String(),   // Target Order ID
  9958  			ordReader.FilledPercent(),     // Filled
  9959  			ordReader.SettledPercent(),    // Settled
  9960  			timestamp,                     // Time
  9961  		})
  9962  		if err != nil {
  9963  			return fmt.Errorf("error writing orders CSV: %v", err)
  9964  		}
  9965  		csvWriter.Flush()
  9966  		err = csvWriter.Error()
  9967  		if err != nil {
  9968  			return fmt.Errorf("error writing orders CSV: %v", err)
  9969  		}
  9970  		return nil
  9971  	}, ordersFile.Close, nil
  9972  }
  9973  
  9974  func deleteMatchFn(matchesFileStr string) (perMatchFn func(*db.MetaMatch, bool) error, cleanUpFn func() error, err error) {
  9975  	matchesFile, err := createFile(matchesFileStr)
  9976  	if err != nil {
  9977  		return nil, nil, fmt.Errorf("problem opening orders file: %v", err)
  9978  	}
  9979  	csvWriter := csv.NewWriter(matchesFile)
  9980  	csvWriter.UseCRLF = runtime.GOOS == "windows"
  9981  
  9982  	err = csvWriter.Write([]string{
  9983  		"Host",
  9984  		"Base",
  9985  		"Quote",
  9986  		"Match ID",
  9987  		"Order ID",
  9988  		"Quantity",
  9989  		"Rate",
  9990  		"Swap Fee Rate",
  9991  		"Swap Address",
  9992  		"Status",
  9993  		"Side",
  9994  		"Secret Hash",
  9995  		"Secret",
  9996  		"Maker Swap Coin ID",
  9997  		"Maker Redeem Coin ID",
  9998  		"Taker Swap Coin ID",
  9999  		"Taker Redeem Coin ID",
 10000  		"Refund Coin ID",
 10001  		"Time",
 10002  	})
 10003  	if err != nil {
 10004  		matchesFile.Close()
 10005  		return nil, nil, fmt.Errorf("error writing matches CSV: %v", err)
 10006  	}
 10007  	csvWriter.Flush()
 10008  	err = csvWriter.Error()
 10009  	if err != nil {
 10010  		matchesFile.Close()
 10011  		return nil, nil, fmt.Errorf("error writing matches CSV: %v", err)
 10012  	}
 10013  	return func(mtch *db.MetaMatch, isSell bool) error {
 10014  		numToStr := func(n any) string {
 10015  			return fmt.Sprintf("%d", n)
 10016  		}
 10017  		base, quote := mtch.MetaData.Base, mtch.MetaData.Quote
 10018  
 10019  		makerAsset, takerAsset := base, quote
 10020  		// If we are either not maker or not buying, invert it. Double
 10021  		// inverse would be no change.
 10022  		if (mtch.Side == order.Taker) != isSell {
 10023  			makerAsset, takerAsset = quote, base
 10024  		}
 10025  
 10026  		var (
 10027  			makerSwapID, makerRedeemID, takerSwapID, redeemSwapID, refundCoinID string
 10028  			err                                                                 error
 10029  		)
 10030  
 10031  		decode := func(assetID uint32, coin []byte) (string, error) {
 10032  			if coin == nil {
 10033  				return "", nil
 10034  			}
 10035  			return asset.DecodeCoinID(assetID, coin)
 10036  		}
 10037  
 10038  		makerSwapID, err = decode(takerAsset, mtch.MetaData.Proof.MakerSwap)
 10039  		if err != nil {
 10040  			return fmt.Errorf("unable to format maker's swap: %v", err)
 10041  		}
 10042  		makerRedeemID, err = decode(makerAsset, mtch.MetaData.Proof.MakerRedeem)
 10043  		if err != nil {
 10044  			return fmt.Errorf("unable to format maker's redeem: %v", err)
 10045  		}
 10046  		takerSwapID, err = decode(makerAsset, mtch.MetaData.Proof.TakerSwap)
 10047  		if err != nil {
 10048  			return fmt.Errorf("unable to format taker's swap: %v", err)
 10049  		}
 10050  		redeemSwapID, err = decode(takerAsset, mtch.MetaData.Proof.TakerRedeem)
 10051  		if err != nil {
 10052  			return fmt.Errorf("unable to format taker's redeem: %v", err)
 10053  		}
 10054  		refundCoinID, err = decode(makerAsset, mtch.MetaData.Proof.RefundCoin)
 10055  		if err != nil {
 10056  			return fmt.Errorf("unable to format maker's refund: %v", err)
 10057  		}
 10058  
 10059  		timestamp := time.UnixMilli(int64(mtch.MetaData.Stamp)).Local().Format(time.RFC3339Nano)
 10060  		err = csvWriter.Write([]string{
 10061  			mtch.MetaData.DEX,                                 // Host
 10062  			dex.BipIDSymbol(base),                             // Base
 10063  			dex.BipIDSymbol(quote),                            // Quote
 10064  			mtch.MatchID.String(),                             // Match ID
 10065  			mtch.OrderID.String(),                             // Order ID
 10066  			numToStr(mtch.Quantity),                           // Quantity
 10067  			numToStr(mtch.Rate),                               // Rate
 10068  			numToStr(mtch.FeeRateSwap),                        // Swap Fee Rate
 10069  			mtch.Address,                                      // Swap Address
 10070  			mtch.Status.String(),                              // Status
 10071  			mtch.Side.String(),                                // Side
 10072  			fmt.Sprintf("%x", mtch.MetaData.Proof.SecretHash), // Secret Hash
 10073  			fmt.Sprintf("%x", mtch.MetaData.Proof.Secret),     // Secret
 10074  			makerSwapID,                                       // Maker Swap Coin ID
 10075  			makerRedeemID,                                     // Maker Redeem Coin ID
 10076  			takerSwapID,                                       // Taker Swap Coin ID
 10077  			redeemSwapID,                                      // Taker Redeem Coin ID
 10078  			refundCoinID,                                      // Refund Coin ID
 10079  			timestamp,                                         // Time
 10080  		})
 10081  		if err != nil {
 10082  			return fmt.Errorf("error writing matches CSV: %v", err)
 10083  		}
 10084  		csvWriter.Flush()
 10085  		err = csvWriter.Error()
 10086  		if err != nil {
 10087  			return fmt.Errorf("error writing matches CSV: %v", err)
 10088  		}
 10089  		return nil
 10090  	}, matchesFile.Close, nil
 10091  }
 10092  
 10093  // archivedRecordsDataDirectory returns a data directory to save deleted archive
 10094  // records.
 10095  func (c *Core) archivedRecordsDataDirectory() string {
 10096  	return filepath.Join(filepath.Dir(c.cfg.DBPath), "archived-records")
 10097  }
 10098  
 10099  // DeleteArchivedRecordsWithBackup is like DeleteArchivedRecords but the
 10100  // required filepaths are provided by Core and the path where archived records
 10101  // are stored is returned.
 10102  func (c *Core) DeleteArchivedRecordsWithBackup(olderThan *time.Time, saveMatchesToFile, saveOrdersToFile bool) (string, int, error) {
 10103  	var matchesFile, ordersFile string
 10104  	if saveMatchesToFile {
 10105  		matchesFile = filepath.Join(c.archivedRecordsDataDirectory(), fmt.Sprintf("archived-matches-%d", time.Now().Unix()))
 10106  	}
 10107  	if saveOrdersToFile {
 10108  		ordersFile = filepath.Join(c.archivedRecordsDataDirectory(), fmt.Sprintf("archived-orders-%d", time.Now().Unix()))
 10109  	}
 10110  	nRecordsDeleted, err := c.DeleteArchivedRecords(olderThan, matchesFile, ordersFile)
 10111  	if nRecordsDeleted > 0 && (saveMatchesToFile || saveOrdersToFile) {
 10112  		return c.archivedRecordsDataDirectory(), nRecordsDeleted, err
 10113  	}
 10114  	return "", nRecordsDeleted, err
 10115  }
 10116  
 10117  // DeleteArchivedRecords deletes archived matches from the database and returns
 10118  // the total number of records deleted. Optionally set a time to delete older
 10119  // records and file paths to save deleted records as comma separated values. If
 10120  // a nil *time.Time is provided, current time is used.
 10121  func (c *Core) DeleteArchivedRecords(olderThan *time.Time, matchesFile, ordersFile string) (int, error) {
 10122  	var (
 10123  		err             error
 10124  		perMtchFn       func(*db.MetaMatch, bool) error
 10125  		nMatchesDeleted int
 10126  	)
 10127  	// If provided a file to write the orders csv to, write the header and
 10128  	// defer closing the file.
 10129  	if matchesFile != "" {
 10130  		var cleanup func() error
 10131  		perMtchFn, cleanup, err = deleteMatchFn(matchesFile)
 10132  		if err != nil {
 10133  			return 0, fmt.Errorf("unable to set up orders csv: %v", err)
 10134  		}
 10135  		defer func() {
 10136  			cleanup()
 10137  			// If no match was deleted, remove the matches file.
 10138  			if nMatchesDeleted == 0 {
 10139  				os.Remove(matchesFile)
 10140  			}
 10141  		}()
 10142  	}
 10143  
 10144  	// Delete matches while saving to csv if available until the database
 10145  	// says that's all or context is canceled.
 10146  	nMatchesDeleted, err = c.db.DeleteInactiveMatches(c.ctx, olderThan, perMtchFn)
 10147  	if err != nil {
 10148  		return 0, fmt.Errorf("unable to delete matches: %v", err)
 10149  	}
 10150  
 10151  	var (
 10152  		perOrdFn       func(*db.MetaOrder) error
 10153  		nOrdersDeleted int
 10154  	)
 10155  
 10156  	// If provided a file to write the orders csv to, write the header and
 10157  	// defer closing the file.
 10158  	if ordersFile != "" {
 10159  		var cleanup func() error
 10160  		perOrdFn, cleanup, err = c.deleteOrderFn(ordersFile)
 10161  		if err != nil {
 10162  			return 0, fmt.Errorf("unable to set up orders csv: %v", err)
 10163  		}
 10164  		defer func() {
 10165  			cleanup()
 10166  			// If no order was deleted, remove the orders file.
 10167  			if nOrdersDeleted == 0 {
 10168  				os.Remove(ordersFile)
 10169  			}
 10170  		}()
 10171  	}
 10172  
 10173  	// Delete orders while saving to csv if available until the database
 10174  	// says that's all or context is canceled.
 10175  	nOrdersDeleted, err = c.db.DeleteInactiveOrders(c.ctx, olderThan, perOrdFn)
 10176  	if err != nil {
 10177  		return 0, fmt.Errorf("unable to delete orders: %v", err)
 10178  	}
 10179  	return nOrdersDeleted + nMatchesDeleted, nil
 10180  }
 10181  
 10182  // AccelerateOrder will use the Child-Pays-For-Parent technique to accelerate
 10183  // the swap transactions in an order.
 10184  func (c *Core) AccelerateOrder(pw []byte, oidB dex.Bytes, newFeeRate uint64) (string, error) {
 10185  	_, err := c.encryptionKey(pw)
 10186  	if err != nil {
 10187  		return "", fmt.Errorf("AccelerateOrder password error: %w", err)
 10188  	}
 10189  
 10190  	oid, err := order.IDFromBytes(oidB)
 10191  	if err != nil {
 10192  		return "", err
 10193  	}
 10194  	tracker, err := c.findActiveOrder(oid)
 10195  	if err != nil {
 10196  		return "", err
 10197  	}
 10198  
 10199  	if !tracker.wallets.fromWallet.traits.IsAccelerator() {
 10200  		return "", fmt.Errorf("the %s wallet is not an accelerator", tracker.wallets.fromWallet.Symbol)
 10201  	}
 10202  
 10203  	tracker.mtx.Lock()
 10204  	defer tracker.mtx.Unlock()
 10205  
 10206  	swapCoinIDs, accelerationCoins, changeCoinID, requiredForRemainingSwaps, err := tracker.orderAccelerationParameters()
 10207  	if err != nil {
 10208  		return "", err
 10209  	}
 10210  
 10211  	newChangeCoin, txID, err :=
 10212  		tracker.wallets.fromWallet.accelerateOrder(swapCoinIDs, accelerationCoins, changeCoinID, requiredForRemainingSwaps, newFeeRate)
 10213  	if err != nil {
 10214  		return "", err
 10215  	}
 10216  	if newChangeCoin != nil {
 10217  		tracker.metaData.ChangeCoin = order.CoinID(newChangeCoin.ID())
 10218  		tracker.coins[newChangeCoin.ID().String()] = newChangeCoin
 10219  	} else {
 10220  		tracker.metaData.ChangeCoin = nil
 10221  	}
 10222  	tracker.metaData.AccelerationCoins = append(tracker.metaData.AccelerationCoins, tracker.metaData.ChangeCoin)
 10223  	return txID, tracker.db.UpdateOrderMetaData(oid, tracker.metaData)
 10224  }
 10225  
 10226  // AccelerationEstimate returns the amount of funds that would be needed to
 10227  // accelerate the swap transactions in an order to a desired fee rate.
 10228  func (c *Core) AccelerationEstimate(oidB dex.Bytes, newFeeRate uint64) (uint64, error) {
 10229  	oid, err := order.IDFromBytes(oidB)
 10230  	if err != nil {
 10231  		return 0, err
 10232  	}
 10233  
 10234  	tracker, err := c.findActiveOrder(oid)
 10235  	if err != nil {
 10236  		return 0, err
 10237  	}
 10238  
 10239  	if !tracker.wallets.fromWallet.traits.IsAccelerator() {
 10240  		return 0, fmt.Errorf("the %s wallet is not an accelerator", tracker.wallets.fromWallet.Symbol)
 10241  	}
 10242  
 10243  	tracker.mtx.RLock()
 10244  	defer tracker.mtx.RUnlock()
 10245  
 10246  	swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, err := tracker.orderAccelerationParameters()
 10247  	if err != nil {
 10248  		return 0, err
 10249  	}
 10250  
 10251  	accelerationFee, err := tracker.wallets.fromWallet.accelerationEstimate(swapCoins, accelerationCoins, changeCoin, requiredForRemainingSwaps, newFeeRate)
 10252  	if err != nil {
 10253  		return 0, err
 10254  	}
 10255  
 10256  	return accelerationFee, nil
 10257  }
 10258  
 10259  // PreAccelerateOrder returns information the user can use to decide how much
 10260  // to accelerate stuck swap transactions in an order.
 10261  func (c *Core) PreAccelerateOrder(oidB dex.Bytes) (*PreAccelerate, error) {
 10262  	oid, err := order.IDFromBytes(oidB)
 10263  	if err != nil {
 10264  		return nil, err
 10265  	}
 10266  
 10267  	tracker, err := c.findActiveOrder(oid)
 10268  	if err != nil {
 10269  		return nil, err
 10270  	}
 10271  
 10272  	if !tracker.wallets.fromWallet.traits.IsAccelerator() {
 10273  		return nil, fmt.Errorf("the %s wallet is not an accelerator", tracker.wallets.fromWallet.Symbol)
 10274  	}
 10275  
 10276  	feeSuggestion := c.feeSuggestionAny(tracker.fromAssetID)
 10277  
 10278  	tracker.mtx.RLock()
 10279  	defer tracker.mtx.RUnlock()
 10280  	swapCoinIDs, accelerationCoins, changeCoinID, requiredForRemainingSwaps, err := tracker.orderAccelerationParameters()
 10281  	if err != nil {
 10282  		return nil, err
 10283  	}
 10284  
 10285  	currentRate, suggestedRange, earlyAcceleration, err :=
 10286  		tracker.wallets.fromWallet.preAccelerate(swapCoinIDs, accelerationCoins, changeCoinID, requiredForRemainingSwaps, feeSuggestion)
 10287  	if err != nil {
 10288  		return nil, err
 10289  	}
 10290  
 10291  	if suggestedRange == nil {
 10292  		// this should never happen
 10293  		return nil, fmt.Errorf("suggested range is nil")
 10294  	}
 10295  
 10296  	return &PreAccelerate{
 10297  		SwapRate:          currentRate,
 10298  		SuggestedRate:     feeSuggestion,
 10299  		SuggestedRange:    *suggestedRange,
 10300  		EarlyAcceleration: earlyAcceleration,
 10301  	}, nil
 10302  }
 10303  
 10304  // WalletPeers returns a list of peers that a wallet is connected to. It also
 10305  // returns the user added peers that the wallet is not connected to.
 10306  func (c *Core) WalletPeers(assetID uint32) ([]*asset.WalletPeer, error) {
 10307  	w, err := c.connectedWallet(assetID)
 10308  	if err != nil {
 10309  		return nil, err
 10310  	}
 10311  
 10312  	peerManager, is := w.Wallet.(asset.PeerManager)
 10313  	if !is {
 10314  		return nil, fmt.Errorf("%s wallet is not a peer manager", unbip(assetID))
 10315  	}
 10316  
 10317  	return peerManager.Peers()
 10318  }
 10319  
 10320  // AddWalletPeer connects the wallet to a new peer, and also persists this peer
 10321  // to be connected to on future startups.
 10322  func (c *Core) AddWalletPeer(assetID uint32, address string) error {
 10323  	w, err := c.connectedWallet(assetID)
 10324  	if err != nil {
 10325  		return err
 10326  	}
 10327  
 10328  	peerManager, is := w.Wallet.(asset.PeerManager)
 10329  	if !is {
 10330  		return fmt.Errorf("%s wallet is not a peer manager", unbip(assetID))
 10331  	}
 10332  
 10333  	return peerManager.AddPeer(address)
 10334  }
 10335  
 10336  // RemoveWalletPeer disconnects from a peer that the user previously added. It
 10337  // will no longer be guaranteed to connect to this peer in the future.
 10338  func (c *Core) RemoveWalletPeer(assetID uint32, address string) error {
 10339  	w, err := c.connectedWallet(assetID)
 10340  	if err != nil {
 10341  		return err
 10342  	}
 10343  
 10344  	peerManager, is := w.Wallet.(asset.PeerManager)
 10345  	if !is {
 10346  		return fmt.Errorf("%s wallet is not a peer manager", unbip(assetID))
 10347  	}
 10348  
 10349  	return peerManager.RemovePeer(address)
 10350  }
 10351  
 10352  // findActiveOrder will search the dex connections for an active order by order
 10353  // id. An error is returned if it cannot be found.
 10354  func (c *Core) findActiveOrder(oid order.OrderID) (*trackedTrade, error) {
 10355  	for _, dc := range c.dexConnections() {
 10356  		tracker, _ := dc.findOrder(oid)
 10357  		if tracker != nil {
 10358  			return tracker, nil
 10359  		}
 10360  	}
 10361  	return nil, fmt.Errorf("could not find active order with order id: %s", oid)
 10362  }
 10363  
 10364  // fetchFiatExchangeRates starts the fiat rate fetcher goroutine and schedules
 10365  // refresh cycles. Use under ratesMtx lock.
 10366  func (c *Core) fetchFiatExchangeRates(ctx context.Context) {
 10367  	c.log.Debug("starting fiat rate fetching")
 10368  
 10369  	c.wg.Add(1)
 10370  	go func() {
 10371  		defer c.wg.Done()
 10372  		tick := time.NewTicker(fiatRateRequestInterval)
 10373  		defer tick.Stop()
 10374  		for {
 10375  			c.refreshFiatRates(ctx)
 10376  
 10377  			select {
 10378  			case <-tick.C:
 10379  			case <-c.reFiat:
 10380  			case <-ctx.Done():
 10381  				return
 10382  
 10383  			}
 10384  		}
 10385  	}()
 10386  }
 10387  
 10388  func (c *Core) fiatSources() []*commonRateSource {
 10389  	c.ratesMtx.RLock()
 10390  	defer c.ratesMtx.RUnlock()
 10391  	sources := make([]*commonRateSource, 0, len(c.fiatRateSources))
 10392  	for _, s := range c.fiatRateSources {
 10393  		sources = append(sources, s)
 10394  	}
 10395  	return sources
 10396  }
 10397  
 10398  // refreshFiatRates refreshes the fiat rates for rate sources whose values have
 10399  // not been updated since fiatRateRequestInterval. It also checks if fiat rates
 10400  // are expired and does some clean-up.
 10401  func (c *Core) refreshFiatRates(ctx context.Context) {
 10402  	var wg sync.WaitGroup
 10403  	supportedAssets := c.SupportedAssets()
 10404  	for _, source := range c.fiatSources() {
 10405  		wg.Add(1)
 10406  		go func(source *commonRateSource) {
 10407  			defer wg.Done()
 10408  			source.refreshRates(ctx, c.log, supportedAssets)
 10409  		}(source)
 10410  	}
 10411  	wg.Wait()
 10412  
 10413  	// Remove expired rate source if any.
 10414  	c.removeExpiredRateSources()
 10415  
 10416  	fiatRatesMap := c.fiatConversions()
 10417  	if len(fiatRatesMap) != 0 {
 10418  		c.notify(newFiatRatesUpdate(fiatRatesMap))
 10419  	}
 10420  }
 10421  
 10422  // FiatRateSources returns a list of fiat rate sources and their individual
 10423  // status.
 10424  func (c *Core) FiatRateSources() map[string]bool {
 10425  	c.ratesMtx.RLock()
 10426  	defer c.ratesMtx.RUnlock()
 10427  	rateSources := make(map[string]bool, len(fiatRateFetchers))
 10428  	for token := range fiatRateFetchers {
 10429  		rateSources[token] = c.fiatRateSources[token] != nil
 10430  	}
 10431  	return rateSources
 10432  }
 10433  
 10434  // FiatConversionRates are the currently cached fiat conversion rates. Must have
 10435  // 1 or more fiat rate sources enabled.
 10436  func (c *Core) FiatConversionRates() map[uint32]float64 {
 10437  	return c.fiatConversions()
 10438  }
 10439  
 10440  // fiatConversions returns fiat rate for all supported assets that have a
 10441  // wallet.
 10442  func (c *Core) fiatConversions() map[uint32]float64 {
 10443  	assetIDs := make(map[uint32]struct{})
 10444  	supportedAssets := asset.Assets()
 10445  	for assetID, asset := range supportedAssets {
 10446  		assetIDs[assetID] = struct{}{}
 10447  		for tokenID := range asset.Tokens {
 10448  			assetIDs[tokenID] = struct{}{}
 10449  		}
 10450  	}
 10451  
 10452  	fiatRatesMap := make(map[uint32]float64, len(supportedAssets))
 10453  	for assetID := range assetIDs {
 10454  		var rateSum float64
 10455  		var sources int
 10456  		for _, source := range c.fiatSources() {
 10457  			rateInfo := source.assetRate(assetID)
 10458  			if rateInfo != nil && time.Since(rateInfo.lastUpdate) < fiatRateDataExpiry && rateInfo.rate > 0 {
 10459  				sources++
 10460  				rateSum += rateInfo.rate
 10461  			}
 10462  		}
 10463  		if rateSum != 0 {
 10464  			fiatRatesMap[assetID] = rateSum / float64(sources) // get average rate.
 10465  		}
 10466  	}
 10467  	return fiatRatesMap
 10468  }
 10469  
 10470  // ToggleRateSourceStatus toggles a fiat rate source status. If disable is true,
 10471  // the fiat rate source is disabled, otherwise the rate source is enabled.
 10472  func (c *Core) ToggleRateSourceStatus(source string, disable bool) error {
 10473  	if disable {
 10474  		return c.disableRateSource(source)
 10475  	}
 10476  	return c.enableRateSource(source)
 10477  }
 10478  
 10479  // enableRateSource enables a fiat rate source.
 10480  func (c *Core) enableRateSource(source string) error {
 10481  	// Check if it's an invalid rate source or it is already enabled.
 10482  	rateFetcher, found := fiatRateFetchers[source]
 10483  	if !found {
 10484  		return errors.New("cannot enable unknown fiat rate source")
 10485  	}
 10486  
 10487  	c.ratesMtx.Lock()
 10488  	defer c.ratesMtx.Unlock()
 10489  	if c.fiatRateSources[source] != nil {
 10490  		return nil // already enabled.
 10491  	}
 10492  
 10493  	// Build fiat rate source.
 10494  	rateSource := newCommonRateSource(rateFetcher)
 10495  	c.fiatRateSources[source] = rateSource
 10496  
 10497  	select {
 10498  	case c.reFiat <- struct{}{}:
 10499  	default:
 10500  	}
 10501  
 10502  	// Update disabled fiat rate source.
 10503  	c.saveDisabledRateSources()
 10504  
 10505  	c.log.Infof("Enabled %s to fetch fiat rates.", source)
 10506  	return nil
 10507  }
 10508  
 10509  // disableRateSource disables a fiat rate source.
 10510  func (c *Core) disableRateSource(source string) error {
 10511  	// Check if it's an invalid fiat rate source or it is already
 10512  	// disabled.
 10513  	_, found := fiatRateFetchers[source]
 10514  	if !found {
 10515  		return errors.New("cannot disable unknown fiat rate source")
 10516  	}
 10517  
 10518  	c.ratesMtx.Lock()
 10519  	defer c.ratesMtx.Unlock()
 10520  
 10521  	if c.fiatRateSources[source] == nil {
 10522  		return nil // already disabled.
 10523  	}
 10524  
 10525  	// Remove fiat rate source.
 10526  	delete(c.fiatRateSources, source)
 10527  
 10528  	// Save disabled fiat rate sources to database.
 10529  	c.saveDisabledRateSources()
 10530  
 10531  	c.log.Infof("Disabled %s from fetching fiat rates.", source)
 10532  	return nil
 10533  }
 10534  
 10535  // removeExpiredRateSources disables expired fiat rate source.
 10536  func (c *Core) removeExpiredRateSources() {
 10537  	c.ratesMtx.Lock()
 10538  	defer c.ratesMtx.Unlock()
 10539  
 10540  	// Remove fiat rate source with expired exchange rate data.
 10541  	var disabledSources []string
 10542  	for token, source := range c.fiatRateSources {
 10543  		if source.isExpired(fiatRateDataExpiry) {
 10544  			delete(c.fiatRateSources, token)
 10545  			disabledSources = append(disabledSources, token)
 10546  		}
 10547  	}
 10548  
 10549  	// Ensure disabled fiat rate fetchers are saved to database.
 10550  	if len(disabledSources) > 0 {
 10551  		c.saveDisabledRateSources()
 10552  		c.log.Warnf("Expired rate source(s) has been disabled: %v", strings.Join(disabledSources, ", "))
 10553  	}
 10554  }
 10555  
 10556  // saveDisabledRateSources saves disabled fiat rate sources to database and
 10557  // shuts down rate fetching if there are no exchange rate source. Use under
 10558  // ratesMtx lock.
 10559  func (c *Core) saveDisabledRateSources() {
 10560  	var disabled []string
 10561  	for token := range fiatRateFetchers {
 10562  		if c.fiatRateSources[token] == nil {
 10563  			disabled = append(disabled, token)
 10564  		}
 10565  	}
 10566  
 10567  	err := c.db.SaveDisabledRateSources(disabled)
 10568  	if err != nil {
 10569  		c.log.Errorf("Unable to save disabled fiat rate source to database: %v", err)
 10570  	}
 10571  }
 10572  
 10573  // stakingWallet fetches the staking wallet and returns its asset.TicketBuyer
 10574  // interface. Errors if no wallet is currently loaded. Used for ticket
 10575  // purchasing.
 10576  func (c *Core) stakingWallet(assetID uint32) (*xcWallet, asset.TicketBuyer, error) {
 10577  	wallet, exists := c.wallet(assetID)
 10578  	if !exists {
 10579  		return nil, nil, newError(missingWalletErr, "no configured wallet found for %s", unbip(assetID))
 10580  	}
 10581  	ticketBuyer, is := wallet.Wallet.(asset.TicketBuyer)
 10582  	if !is {
 10583  		return nil, nil, fmt.Errorf("%s wallet is not a TicketBuyer", unbip(assetID))
 10584  	}
 10585  	return wallet, ticketBuyer, nil
 10586  }
 10587  
 10588  // StakeStatus returns current staking statuses such as currently owned
 10589  // tickets, ticket price, and current voting preferences. Used for
 10590  // ticket purchasing.
 10591  func (c *Core) StakeStatus(assetID uint32) (*asset.TicketStakingStatus, error) {
 10592  	_, tb, err := c.stakingWallet(assetID)
 10593  	if err != nil {
 10594  		return nil, err
 10595  	}
 10596  	return tb.StakeStatus()
 10597  }
 10598  
 10599  // SetVSP sets the VSP provider. Used for ticket purchasing.
 10600  func (c *Core) SetVSP(assetID uint32, addr string) error {
 10601  	_, tb, err := c.stakingWallet(assetID)
 10602  	if err != nil {
 10603  		return err
 10604  	}
 10605  	return tb.SetVSP(addr)
 10606  }
 10607  
 10608  // PurchaseTickets purchases n tickets. Returns the purchased ticket hashes if
 10609  // successful. Used for ticket purchasing.
 10610  func (c *Core) PurchaseTickets(assetID uint32, pw []byte, n int) error {
 10611  	wallet, tb, err := c.stakingWallet(assetID)
 10612  	if err != nil {
 10613  		return err
 10614  	}
 10615  	crypter, err := c.encryptionKey(pw)
 10616  	if err != nil {
 10617  		return fmt.Errorf("password error: %w", err)
 10618  	}
 10619  	defer crypter.Close()
 10620  
 10621  	if err = c.connectAndUnlock(crypter, wallet); err != nil {
 10622  		return err
 10623  	}
 10624  
 10625  	if err = tb.PurchaseTickets(n, c.feeSuggestionAny(assetID)); err != nil {
 10626  		return err
 10627  	}
 10628  	c.updateAssetBalance(assetID)
 10629  	// TODO: Send tickets bought notification.
 10630  	//subject, details := c.formatDetails(TopicSendSuccess, sentValue, unbip(assetID), address, coin)
 10631  	//c.notify(newSendNote(TopicSendSuccess, subject, details, db.Success))
 10632  	return nil
 10633  }
 10634  
 10635  // SetVotingPreferences sets default voting settings for all active tickets and
 10636  // future tickets. Nil maps can be provided for no change. Used for ticket
 10637  // purchasing.
 10638  func (c *Core) SetVotingPreferences(assetID uint32, choices, tSpendPolicy,
 10639  	treasuryPolicy map[string]string) error {
 10640  	_, tb, err := c.stakingWallet(assetID)
 10641  	if err != nil {
 10642  		return err
 10643  	}
 10644  	return tb.SetVotingPreferences(choices, tSpendPolicy, treasuryPolicy)
 10645  }
 10646  
 10647  // ListVSPs lists known available voting service providers.
 10648  func (c *Core) ListVSPs(assetID uint32) ([]*asset.VotingServiceProvider, error) {
 10649  	_, tb, err := c.stakingWallet(assetID)
 10650  	if err != nil {
 10651  		return nil, err
 10652  	}
 10653  	return tb.ListVSPs()
 10654  }
 10655  
 10656  // TicketPage fetches a page of TicketBuyer tickets within a range of block
 10657  // numbers with a target page size and optional offset. scanStart it the block
 10658  // in which to start the scan. The scan progresses in reverse block number
 10659  // order, starting at scanStart and going to progressively lower blocks.
 10660  // scanStart can be set to -1 to indicate the current chain tip.
 10661  func (c *Core) TicketPage(assetID uint32, scanStart int32, n, skipN int) ([]*asset.Ticket, error) {
 10662  	_, tb, err := c.stakingWallet(assetID)
 10663  	if err != nil {
 10664  		return nil, err
 10665  	}
 10666  	return tb.TicketPage(scanStart, n, skipN)
 10667  }
 10668  
 10669  func (c *Core) mixingWallet(assetID uint32) (*xcWallet, asset.FundsMixer, error) {
 10670  	w, known := c.wallet(assetID)
 10671  	if !known {
 10672  		return nil, nil, fmt.Errorf("unknown wallet %d", assetID)
 10673  	}
 10674  	mw, is := w.Wallet.(asset.FundsMixer)
 10675  	if !is {
 10676  		return nil, nil, fmt.Errorf("%s wallet is not a FundsMixer", w.Info().Name)
 10677  	}
 10678  	return w, mw, nil
 10679  }
 10680  
 10681  // FundsMixingStats returns the current state of the wallet's funds mixer.
 10682  func (c *Core) FundsMixingStats(assetID uint32) (*asset.FundsMixingStats, error) {
 10683  	_, mw, err := c.mixingWallet(assetID)
 10684  	if err != nil {
 10685  		return nil, err
 10686  	}
 10687  	return mw.FundsMixingStats()
 10688  }
 10689  
 10690  // ConfigureFundsMixer configures the wallet for funds mixing.
 10691  func (c *Core) ConfigureFundsMixer(pw []byte, assetID uint32, isMixerEnabled bool) error {
 10692  	wallet, mw, err := c.mixingWallet(assetID)
 10693  	if err != nil {
 10694  		return err
 10695  	}
 10696  	crypter, err := c.encryptionKey(pw)
 10697  	if err != nil {
 10698  		return fmt.Errorf("mixing password error: %w", err)
 10699  	}
 10700  	defer crypter.Close()
 10701  	if err := c.connectAndUnlock(crypter, wallet); err != nil {
 10702  		return err
 10703  	}
 10704  	return mw.ConfigureFundsMixer(isMixerEnabled)
 10705  }
 10706  
 10707  // NetworkFeeRate generates a network tx fee rate for the specified asset.
 10708  // If the wallet implements FeeRater, the wallet will be queried for the
 10709  // fee rate. If the wallet is not a FeeRater, local book feed caches are
 10710  // checked. If no relevant books are synced, connected DCRDEX servers will be
 10711  // queried.
 10712  func (c *Core) NetworkFeeRate(assetID uint32) uint64 {
 10713  	return c.feeSuggestionAny(assetID)
 10714  }
 10715  
 10716  func (c *Core) deleteRequestedAction(uniqueID string) {
 10717  	c.requestedActionMtx.Lock()
 10718  	delete(c.requestedActions, uniqueID)
 10719  	c.requestedActionMtx.Unlock()
 10720  }
 10721  
 10722  // handleRetryRedemptionAction handles a response to a user response to an
 10723  // ActionRequiredNote for a rejected redemption transaction.
 10724  func (c *Core) handleRetryRedemptionAction(actionB []byte) error {
 10725  	var req struct {
 10726  		OrderID dex.Bytes `json:"orderID"`
 10727  		CoinID  dex.Bytes `json:"coinID"`
 10728  		Retry   bool      `json:"retry"`
 10729  	}
 10730  	if err := json.Unmarshal(actionB, &req); err != nil {
 10731  		return fmt.Errorf("error decoding request: %w", err)
 10732  	}
 10733  	c.deleteRequestedAction(req.CoinID.String())
 10734  
 10735  	if !req.Retry {
 10736  		// Do nothing
 10737  		return nil
 10738  	}
 10739  	var oid order.OrderID
 10740  	copy(oid[:], req.OrderID)
 10741  	var tracker *trackedTrade
 10742  	for _, dc := range c.dexConnections() {
 10743  		tracker, _ = dc.findOrder(oid)
 10744  		if tracker != nil {
 10745  			break
 10746  		}
 10747  	}
 10748  	if tracker == nil {
 10749  		return fmt.Errorf("order %s not known", oid)
 10750  	}
 10751  	tracker.mtx.Lock()
 10752  	defer tracker.mtx.Unlock()
 10753  
 10754  	for _, match := range tracker.matches {
 10755  		coinID := match.MetaData.Proof.TakerRedeem
 10756  		if match.Side == order.Maker {
 10757  			coinID = match.MetaData.Proof.MakerRedeem
 10758  		}
 10759  		if bytes.Equal(coinID, req.CoinID) {
 10760  			if match.Side == order.Taker && match.Status == order.MatchComplete {
 10761  				// Try to redeem again.
 10762  				match.redemptionRejected = false
 10763  				match.MetaData.Proof.TakerRedeem = nil
 10764  				match.Status = order.MakerRedeemed
 10765  				if err := c.db.UpdateMatch(&match.MetaMatch); err != nil {
 10766  					c.log.Errorf("Failed to update match in DB: %v", err)
 10767  				}
 10768  			} else if match.Side == order.Maker && match.Status == order.MakerRedeemed {
 10769  				match.redemptionRejected = false
 10770  				match.MetaData.Proof.MakerRedeem = nil
 10771  				match.Status = order.TakerSwapCast
 10772  				if err := c.db.UpdateMatch(&match.MetaMatch); err != nil {
 10773  					c.log.Errorf("Failed to update match in DB: %v", err)
 10774  				}
 10775  			} else {
 10776  				c.log.Errorf("Redemption retry attempted for order side %s status %s", match.Side, match.Status)
 10777  			}
 10778  		}
 10779  	}
 10780  	return nil
 10781  }
 10782  
 10783  // handleCoreAction checks if the actionID is a known core action, and if so
 10784  // attempts to take the action requested.
 10785  func (c *Core) handleCoreAction(actionID string, actionB json.RawMessage) ( /* handled */ bool, error) {
 10786  	switch actionID {
 10787  	case ActionIDRedeemRejected:
 10788  		return true, c.handleRetryRedemptionAction(actionB)
 10789  	}
 10790  	return false, nil
 10791  }
 10792  
 10793  // TakeAction is called in response to a ActionRequiredNote. The note may have
 10794  // come from core or from a wallet.
 10795  func (c *Core) TakeAction(assetID uint32, actionID string, actionB json.RawMessage) (err error) {
 10796  	defer func() {
 10797  		if err != nil {
 10798  			c.log.Errorf("Error while attempting user action %q with parameters %q, asset ID %d: %v",
 10799  				actionID, string(actionB), assetID, err)
 10800  		} else {
 10801  			c.log.Infof("User completed action %q with parameters %q, asset ID %d",
 10802  				actionID, string(actionB), assetID)
 10803  		}
 10804  	}()
 10805  	if handled, err := c.handleCoreAction(actionID, actionB); handled {
 10806  		return err
 10807  	}
 10808  	w, err := c.connectedWallet(assetID)
 10809  	if err != nil {
 10810  		return err
 10811  	}
 10812  	goGetter, is := w.Wallet.(asset.ActionTaker)
 10813  	if !is {
 10814  		return fmt.Errorf("wallet for %s cannot handle user actions", w.Symbol)
 10815  	}
 10816  	return goGetter.TakeAction(actionID, actionB)
 10817  }
 10818  
 10819  // GenerateBCHRecoveryTransaction generates a tx that spends all inputs from the
 10820  // deprecated BCH wallet to the given recipient.
 10821  func (c *Core) GenerateBCHRecoveryTransaction(appPW []byte, recipient string) ([]byte, error) {
 10822  	const bipID = 145
 10823  	crypter, err := c.encryptionKey(appPW)
 10824  	if err != nil {
 10825  		return nil, err
 10826  	}
 10827  	_, walletPW, err := c.assetSeedAndPass(bipID, crypter)
 10828  	if err != nil {
 10829  		return nil, err
 10830  	}
 10831  	return asset.SPVWithdrawTx(c.ctx, bipID, walletPW, recipient, c.assetDataDirectory(bipID), c.net, c.log.SubLogger("BCH"))
 10832  }
 10833  
 10834  func (c *Core) checkEpochResolution(host string, mktID string) {
 10835  	dc, _, _ := c.dex(host)
 10836  	if dc == nil {
 10837  		return
 10838  	}
 10839  	currentEpoch := dc.marketEpoch(mktID, time.Now())
 10840  	lastEpoch := currentEpoch - 1
 10841  
 10842  	// Short path if we're already resolved.
 10843  	dc.epochMtx.RLock()
 10844  	resolvedEpoch := dc.resolvedEpoch[mktID]
 10845  	dc.epochMtx.RUnlock()
 10846  	if lastEpoch == resolvedEpoch {
 10847  		return
 10848  	}
 10849  
 10850  	ts, inFlights := dc.marketTrades(mktID)
 10851  	for _, ord := range inFlights {
 10852  		if ord.Epoch == lastEpoch {
 10853  			return
 10854  		}
 10855  	}
 10856  	for _, t := range ts {
 10857  		// Is this order from the last epoch and still not booked or executed?
 10858  		if t.epochIdx() == lastEpoch && t.status() == order.OrderStatusEpoch {
 10859  			return
 10860  		}
 10861  		// Does this order have an in-flight cancel order that is not yet
 10862  		// resolved?
 10863  		t.mtx.RLock()
 10864  		unresolvedCancel := t.cancel != nil && t.cancelEpochIdx() == lastEpoch && t.cancel.matches.taker == nil
 10865  		t.mtx.RUnlock()
 10866  		if unresolvedCancel {
 10867  			return
 10868  		}
 10869  	}
 10870  
 10871  	// We don't have any unresolved orders or cancel orders from the last epoch.
 10872  	// Just make sure that not other thread has resolved the epoch and then send
 10873  	// the notification.
 10874  	dc.epochMtx.Lock()
 10875  	sendUpdate := lastEpoch > dc.resolvedEpoch[mktID]
 10876  	dc.resolvedEpoch[mktID] = lastEpoch
 10877  	dc.epochMtx.Unlock()
 10878  	if sendUpdate {
 10879  		if bookie := dc.bookie(mktID); bookie != nil {
 10880  			bookie.send(&BookUpdate{
 10881  				Action:   EpochResolved,
 10882  				Host:     dc.acct.host,
 10883  				MarketID: mktID,
 10884  				Payload: &ResolvedEpoch{
 10885  					Current:  currentEpoch,
 10886  					Resolved: lastEpoch,
 10887  				},
 10888  			})
 10889  		}
 10890  
 10891  	}
 10892  }
 10893  
 10894  // RedeemGeocode redeems the provided game code with the wallet and redeems the
 10895  // prepaid bond (code is a prepaid bond). If the user is not registered with
 10896  // dex.decred.org yet, the dex will be added first.
 10897  func (c *Core) RedeemGeocode(appPW, code []byte, msg string) (dex.Bytes, uint64, error) {
 10898  	const dcrBipID = 42
 10899  	dcrWallet, found := c.wallet(dcrBipID)
 10900  	if !found {
 10901  		return nil, 0, errors.New("no decred wallet")
 10902  	}
 10903  	if !dcrWallet.connected() {
 10904  		return nil, 0, errors.New("decred wallet is not connected")
 10905  	}
 10906  
 10907  	host := "dex.decred.org:7232"
 10908  	switch c.net {
 10909  	case dex.Testnet:
 10910  		host = "bison.exchange:17232"
 10911  	case dex.Simnet:
 10912  		host = "127.0.0.1:17273"
 10913  	}
 10914  	cert := CertStore[c.net][host]
 10915  
 10916  	c.connMtx.RLock()
 10917  	dc, found := c.conns[host]
 10918  	c.connMtx.RUnlock()
 10919  	if !found {
 10920  		if err := c.AddDEX(appPW, host, cert); err != nil {
 10921  			return nil, 0, fmt.Errorf("error adding %s: %w", host, err)
 10922  		}
 10923  		c.connMtx.RLock()
 10924  		_, found = c.conns[host]
 10925  		c.connMtx.RUnlock()
 10926  		if !found {
 10927  			return nil, 0, fmt.Errorf("dex not found after adding")
 10928  		}
 10929  	} else if dc.status() != comms.Connected {
 10930  		return nil, 0, fmt.Errorf("not currently connected to %s", host)
 10931  	}
 10932  
 10933  	w, is := dcrWallet.Wallet.(asset.GeocodeRedeemer)
 10934  	if !is {
 10935  		return nil, 0, errors.New("decred wallet is not a GeocodeRedeemer?")
 10936  	}
 10937  
 10938  	coinID, win, err := w.RedeemGeocode(code, msg)
 10939  	if err != nil {
 10940  		return nil, 0, fmt.Errorf("error redeeming geocode: %w", err)
 10941  	}
 10942  
 10943  	if _, err := c.RedeemPrepaidBond(appPW, code, host, cert); err != nil {
 10944  		return nil, 0, fmt.Errorf("geocode redeemed, but failed to redeem prepaid bond: %w", err)
 10945  	}
 10946  
 10947  	return coinID, win, nil
 10948  }
 10949  
 10950  // ExtensionModeConfig is the configuration parsed from the extension-mode file.
 10951  func (c *Core) ExtensionModeConfig() *ExtensionModeConfig {
 10952  	return c.extensionModeConfig
 10953  }
 10954  
 10955  // calcParcelLimit computes the users score-scaled user parcel limit.
 10956  func calcParcelLimit(tier int64, score, maxScore int32) uint32 {
 10957  	// Users limit starts at 2 parcels per tier.
 10958  	lowerLimit := tier * dex.PerTierBaseParcelLimit
 10959  	// Limit can scale up to 3x with score.
 10960  	upperLimit := lowerLimit * dex.ParcelLimitScoreMultiplier
 10961  	limitRange := upperLimit - lowerLimit
 10962  	var scaleFactor float64
 10963  	if score > 0 {
 10964  		scaleFactor = float64(score) / float64(maxScore)
 10965  	}
 10966  	return uint32(lowerLimit) + uint32(math.Round(scaleFactor*float64(limitRange)))
 10967  }
 10968  
 10969  // TradingLimits returns the number of parcels the user can trade on an
 10970  // exchange and the amount that are currently being traded.
 10971  func (c *Core) TradingLimits(host string) (userParcels, parcelLimit uint32, err error) {
 10972  	dc, _, err := c.dex(host)
 10973  	if err != nil {
 10974  		return 0, 0, err
 10975  	}
 10976  
 10977  	cfg := dc.config()
 10978  	dc.acct.authMtx.RLock()
 10979  	rep := dc.acct.rep
 10980  	dc.acct.authMtx.RUnlock()
 10981  
 10982  	mkts := make(map[string]*msgjson.Market, len(cfg.Markets))
 10983  	for _, mkt := range cfg.Markets {
 10984  		mkts[mkt.Name] = mkt
 10985  	}
 10986  	mktTrades := make(map[string][]*trackedTrade)
 10987  	for _, t := range dc.trackedTrades() {
 10988  		mktTrades[t.mktID] = append(mktTrades[t.mktID], t)
 10989  	}
 10990  
 10991  	parcelLimit = calcParcelLimit(rep.EffectiveTier(), rep.Score, int32(cfg.MaxScore))
 10992  	for mktID, trades := range mktTrades {
 10993  		mkt := mkts[mktID]
 10994  		if mkt == nil {
 10995  			c.log.Warnf("trade for unknown market %q", mktID)
 10996  			continue
 10997  		}
 10998  
 10999  		var midGap, mktWeight uint64
 11000  		for _, t := range trades {
 11001  			if t.isEpochOrder() && midGap == 0 {
 11002  				midGap, err = dc.midGap(mkt.Base, mkt.Quote)
 11003  				if err != nil && !errors.Is(err, orderbook.ErrEmptyOrderbook) {
 11004  					return 0, 0, err
 11005  				}
 11006  			}
 11007  			mktWeight += t.marketWeight(midGap, mkt.LotSize)
 11008  		}
 11009  		userParcels += uint32(mktWeight / (uint64(mkt.ParcelSize) * mkt.LotSize))
 11010  	}
 11011  
 11012  	return userParcels, parcelLimit, nil
 11013  }