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