decred.org/dcrdex@v1.0.5/client/core/trade.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/hex"
    11  	"errors"
    12  	"fmt"
    13  	"math"
    14  	"strings"
    15  	"sync"
    16  	"sync/atomic"
    17  	"time"
    18  
    19  	"decred.org/dcrdex/client/asset"
    20  	"decred.org/dcrdex/client/comms"
    21  	"decred.org/dcrdex/client/db"
    22  	"decred.org/dcrdex/dex"
    23  	"decred.org/dcrdex/dex/calc"
    24  	"decred.org/dcrdex/dex/encode"
    25  	"decred.org/dcrdex/dex/msgjson"
    26  	"decred.org/dcrdex/dex/order"
    27  	"decred.org/dcrdex/dex/wait"
    28  )
    29  
    30  // ExpirationErr indicates that the wait.TickerQueue has expired a waiter, e.g.
    31  // a reported coin was not found before the set expiration time.
    32  type ExpirationErr string
    33  
    34  // Error satisfies the error interface for ExpirationErr.
    35  func (err ExpirationErr) Error() string { return string(err) }
    36  
    37  // Ensure matchTracker satisfies the Stringer interface.
    38  var _ (fmt.Stringer) = (*matchTracker)(nil)
    39  
    40  // A matchTracker is used to negotiate a match.
    41  type matchTracker struct {
    42  	// counterConfirms records the last known confirms of the counterparty swap.
    43  	// This is set in isSwappable for taker, isRedeemable for maker. -1 means
    44  	// the confirms have not yet been checked (or logged). swapConfirms is for
    45  	// your own swap. For safe access, use the matchTracker methods: confirms,
    46  	// setSwapConfirms, and setCounterConfirms.
    47  	counterConfirms int64 // atomic
    48  	swapConfirms    int64 // atomic
    49  
    50  	// sendingInitAsync indicates if this match's init request is being sent to
    51  	// the server and awaiting a response. No attempts will be made to send
    52  	// another init request for this match while one is already active.
    53  	sendingInitAsync uint32 // atomic
    54  	// sendingRedeemAsync indicates if this match's redeem request is being sent
    55  	// to the server and awaiting a response. No attempts will be made to send
    56  	// another redeem request for this match while one is already active.
    57  	sendingRedeemAsync uint32 // atomic
    58  
    59  	// The first group of fields below should be accessed with the parent
    60  	// trackedTrade's mutex locked, excluding the atomic fields.
    61  
    62  	db.MetaMatch
    63  	// swapErr is an error set when we have given up hope on broadcasting a swap
    64  	// tx for a match. This can happen if 1) the swap has been attempted
    65  	// (repeatedly), but could not be successfully broadcast before the
    66  	// broadcast timeout, or 2) a match data was found to be in a nonsensical
    67  	// state during startup.
    68  	swapErr error
    69  	// swapErrCount counts the swap attempts. It is used in recovery.
    70  	swapErrCount int
    71  	// redeemErrCount counts the redeem attempts. It is used in recovery.
    72  	redeemErrCount int
    73  	// suspectSwap is a flag to indicate that there was a problem encountered
    74  	// trying to send a swap contract for this match. If suspectSwap is true,
    75  	// the match will not be grouped when attempting future swaps.
    76  	suspectSwap bool
    77  	// suspectRedeem is a flag to indicate that there was a problem encountered
    78  	// trying to redeem this match. If suspectRedeem is true, the match will not
    79  	// be grouped when attempting future redemptions.
    80  	suspectRedeem bool
    81  	// refundErr will be set to true if we attempt a refund and get a
    82  	// CoinNotFoundError, indicating there is nothing to refund and the
    83  	// counterparty redemption search should be attempted. Prevents retries.
    84  	refundErr   error
    85  	prefix      *order.Prefix
    86  	trade       *order.Trade
    87  	counterSwap *asset.AuditInfo
    88  	// cancelRedemptionSearch should be set when taker starts searching for
    89  	// maker's redemption. Required to cancel a find redemption attempt if
    90  	// taker successfully executes a refund.
    91  	cancelRedemptionSearch context.CancelFunc
    92  
    93  	// confirmRedemptionNumTries is just used for logging.
    94  	confirmRedemptionNumTries int
    95  	// redemptionConfs and redemptionConfsReq are updated while the redemption
    96  	// confirmation process is running. Their values are not updated after the
    97  	// match reaches MatchConfirmed status.
    98  	redemptionConfs    uint64
    99  	redemptionConfsReq uint64
   100  	// redemptionRejected will be true if a redemption tx was rejected. A
   101  	// a rejected tx may indicate a serious internal issue, so we will seek
   102  	// user approval before replacing the tx.
   103  	redemptionRejected bool
   104  	// matchCompleteSent precludes sending another redeem to the server if we
   105  	// we are retrying after rejection and they already accepted our first
   106  	// request. Additional requests will just error and they don't really care
   107  	// if we redeem as taker anyway.
   108  	matchCompleteSent bool
   109  
   110  	// The fields below need to be modified without the parent trackedTrade's
   111  	// mutex being write locked, so they have dedicated mutexes.
   112  
   113  	swapSpentTimeMtx sync.Mutex
   114  	swapSpentTime    time.Time
   115  
   116  	// lastExpireDur is the most recently logged time until expiry of the
   117  	// party's own contract. This may be negative if expiry has passed, but it
   118  	// is not yet refundable due to other consensus rules. This is used only by
   119  	// isRefundable. Initialize this to a very large value to guarantee that it
   120  	// will be logged on the first check or when 0. This facilitates useful
   121  	// logging, while not being spammy.
   122  	lastExpireDurMtx sync.Mutex
   123  	lastExpireDur    time.Duration
   124  
   125  	// Certain exceptions that control swap actions are commonly accessed
   126  	// together, and these share a single mutex. See the exceptions and
   127  	// delayTicks methods.
   128  	exceptionMtx sync.RWMutex
   129  	// tickGovernor can be set non-nil to prevent swaps or redeems from being
   130  	// attempted for a match. Typically, the Timer comes from an AfterFunc that
   131  	// itself nils the tickGovernor. Guarded by exceptionMtx.
   132  	tickGovernor *time.Timer
   133  	// checkServerRevoke is set to make sure that a taker will not prematurely
   134  	// send an initialization until it is confirmed with the server (see
   135  	// authDEX) that the match is not revoked. This should be set on reconnect
   136  	// for all taker matches in MakerSwapCast. Guarded by exceptionMtx.
   137  	checkServerRevoke bool
   138  }
   139  
   140  // matchTime returns the match's match time as a time.Time.
   141  func (m *matchTracker) matchTime() time.Time {
   142  	return time.UnixMilli(int64(m.MetaData.Proof.Auth.MatchStamp)).UTC()
   143  }
   144  
   145  func (m *matchTracker) swapSpentAgo() time.Duration {
   146  	m.swapSpentTimeMtx.Lock()
   147  	defer m.swapSpentTimeMtx.Unlock()
   148  	if m.swapSpentTime.IsZero() {
   149  		return 0
   150  	}
   151  	return time.Since(m.swapSpentTime)
   152  }
   153  
   154  func (m *matchTracker) swapSpent() {
   155  	m.swapSpentTimeMtx.Lock()
   156  	defer m.swapSpentTimeMtx.Unlock()
   157  	if !m.swapSpentTime.IsZero() {
   158  		return // already noted
   159  	}
   160  	m.swapSpentTime = time.Now()
   161  }
   162  
   163  // setExpireDur records the last known duration until expiry if the difference
   164  // from the previous recorded duration is at least the provided log interval
   165  // threshold. The return indicates if it was updated (and should be logged by
   166  // the caller).
   167  func (m *matchTracker) setExpireDur(expireDur, logInterval time.Duration) (intervalPassed bool) {
   168  	m.lastExpireDurMtx.Lock()
   169  	defer m.lastExpireDurMtx.Unlock()
   170  	if m.lastExpireDur-expireDur < logInterval {
   171  		return false // too soon
   172  	}
   173  	m.lastExpireDur = expireDur
   174  	return true // ok to log
   175  }
   176  
   177  func (m *matchTracker) exceptions() (ticksGoverned, checkServerRevoke bool) {
   178  	m.exceptionMtx.RLock()
   179  	defer m.exceptionMtx.RUnlock()
   180  	return m.tickGovernor != nil, m.checkServerRevoke
   181  }
   182  
   183  // delayTicks sets the tickGovernor to prevent retrying too quickly after an
   184  // error.
   185  func (m *matchTracker) delayTicks(waitTime time.Duration) {
   186  	m.exceptionMtx.Lock()
   187  	m.tickGovernor = time.AfterFunc(waitTime, func() {
   188  		m.exceptionMtx.Lock()
   189  		m.tickGovernor = nil
   190  		m.exceptionMtx.Unlock()
   191  	})
   192  	m.exceptionMtx.Unlock()
   193  }
   194  
   195  func (m *matchTracker) confirms() (mine, theirs int64) {
   196  	return atomic.LoadInt64(&m.swapConfirms), atomic.LoadInt64(&m.counterConfirms)
   197  }
   198  
   199  func (m *matchTracker) setSwapConfirms(mine int64) {
   200  	atomic.StoreInt64(&m.swapConfirms, mine)
   201  }
   202  
   203  func (m *matchTracker) setCounterConfirms(theirs int64) (was int64) {
   204  	return atomic.SwapInt64(&m.counterConfirms, theirs)
   205  }
   206  
   207  // token returns a shortened representation of the match ID.
   208  func (m *matchTracker) token() string {
   209  	return hex.EncodeToString(m.MatchID[:4])
   210  }
   211  
   212  // trackedCancel is information necessary to track a cancel order. A
   213  // trackedCancel is always associated with a trackedTrade.
   214  type trackedCancel struct {
   215  	order.CancelOrder
   216  	epochLen uint64
   217  	matches  struct {
   218  		maker *msgjson.Match
   219  		taker *msgjson.Match
   220  	}
   221  }
   222  
   223  type feeStamped struct {
   224  	sync.RWMutex
   225  	rate  uint64
   226  	stamp time.Time
   227  }
   228  
   229  func (fs *feeStamped) get() uint64 {
   230  	fs.RLock()
   231  	defer fs.RUnlock()
   232  	return fs.rate
   233  }
   234  
   235  const (
   236  	// freshRedeemFeeAge is the expiry age for cached redeem fee rates, past
   237  	// which fetchFeeFromOracle should be used to refresh the rate. See
   238  	// cacheRedemptionFeeSuggestion.
   239  	freshRedeemFeeAge = time.Minute
   240  
   241  	// spentAgoThreshNormal is how long to wait after we as taker observer our
   242  	// swap spent by the maker without receiving a redemption request from the
   243  	// server before initiating a redemption search and auto-redeem.
   244  	spentAgoThreshNormal = 10 * time.Minute
   245  	// spentAgoThreshSelfGoverned is like spentAgoThreshNormal, but for a
   246  	// self-governed trade. We are less patient if the server is down or
   247  	// lacking the market or asset configs involved.
   248  	spentAgoThreshSelfGoverned = time.Minute
   249  )
   250  
   251  // trackedTrade is an order (issued by this client), its matches, and its cancel
   252  // order, if applicable. The trackedTrade has methods for handling requests
   253  // from the DEX to progress match negotiation.
   254  type trackedTrade struct {
   255  	// redeemFeeSuggestion is cached fee suggestion for redemption. We can't
   256  	// request a fee suggestion at redeem time because it would require making
   257  	// the full redemption routine async (TODO?). This fee suggestion is
   258  	// intentionally not stored as part of the db.OrderMetaData, and should be
   259  	// repopulated if the client is restarted.
   260  	redeemFeeSuggestion feeStamped
   261  
   262  	selfGoverned uint32 // (atomic) server either lacks this market or is down
   263  
   264  	tickLock sync.Mutex // prevent multiple concurrent ticks, but allow them to queue
   265  
   266  	order.Order
   267  
   268  	db                 db.DB
   269  	dc                 *dexConnection
   270  	latencyQ           *wait.TickerQueue
   271  	mktID              string // convenience for marketName(t.Base(), t.Quote())
   272  	lockTimeTaker      time.Duration
   273  	lockTimeMaker      time.Duration
   274  	notify             func(Notification)
   275  	formatDetails      func(Topic, ...any) (string, string)
   276  	fromAssetID        uint32            // wallets.fromWallet.AssetID
   277  	options            map[string]string // metaData.Options (immutable) for Redeem and Swap
   278  	redemptionReserves uint64            // metaData.RedemptionReserves (immutable)
   279  	refundReserves     uint64            // metaData.RefundReserves (immutable)
   280  	preImg             order.Preimage
   281  
   282  	csumMtx      sync.RWMutex
   283  	csum         dex.Bytes // the commitment checksum provided in the preimage request
   284  	cancelCsum   dex.Bytes
   285  	cancelPreimg order.Preimage
   286  
   287  	// mtx protects all read-write fields of the trackedTrade and the
   288  	// matchTrackers in the matches map.
   289  	mtx              sync.RWMutex
   290  	metaData         *db.OrderMetaData
   291  	wallets          *walletSet
   292  	coins            map[string]asset.Coin
   293  	coinsLocked      bool
   294  	change           asset.Coin
   295  	changeLocked     bool
   296  	cancel           *trackedCancel
   297  	matches          map[order.MatchID]*matchTracker
   298  	redemptionLocked uint64 // remaining locked of redemptionReserves
   299  	refundLocked     uint64 // remaining locked of refundReserves
   300  	readyToTick      bool   // this will be false if either of the wallets cannot be connected and unlocked
   301  }
   302  
   303  // newTrackedTrade is a constructor for a trackedTrade.
   304  func newTrackedTrade(dbOrder *db.MetaOrder, preImg order.Preimage, dc *dexConnection,
   305  	lockTimeTaker, lockTimeMaker time.Duration, db db.DB, latencyQ *wait.TickerQueue, wallets *walletSet,
   306  	coins asset.Coins, notify func(Notification), formatDetails func(Topic, ...any) (string, string)) *trackedTrade {
   307  
   308  	fromID := dbOrder.Order.Quote()
   309  	if dbOrder.Order.Trade().Sell {
   310  		fromID = dbOrder.Order.Base()
   311  	}
   312  
   313  	ord := dbOrder.Order
   314  	t := &trackedTrade{
   315  		Order:              ord,
   316  		metaData:           dbOrder.MetaData,
   317  		dc:                 dc,
   318  		db:                 db,
   319  		latencyQ:           latencyQ,
   320  		wallets:            wallets,
   321  		preImg:             preImg,
   322  		mktID:              marketName(ord.Base(), ord.Quote()),
   323  		coins:              mapifyCoins(coins), // must not be nil even if empty
   324  		coinsLocked:        len(coins) > 0,
   325  		lockTimeTaker:      lockTimeTaker,
   326  		lockTimeMaker:      lockTimeMaker,
   327  		matches:            make(map[order.MatchID]*matchTracker),
   328  		notify:             notify,
   329  		fromAssetID:        fromID,
   330  		formatDetails:      formatDetails,
   331  		options:            dbOrder.MetaData.Options,
   332  		redemptionReserves: dbOrder.MetaData.RedemptionReserves,
   333  		refundReserves:     dbOrder.MetaData.RefundReserves,
   334  		readyToTick:        true,
   335  	}
   336  	return t
   337  }
   338  
   339  func (t *trackedTrade) isSelfGoverned() bool {
   340  	return atomic.LoadUint32(&t.selfGoverned) == 1
   341  }
   342  
   343  func (t *trackedTrade) spentAgoThresh() time.Duration {
   344  	if t.isSelfGoverned() {
   345  		return spentAgoThreshSelfGoverned
   346  	}
   347  	return spentAgoThreshNormal // longer
   348  }
   349  
   350  func (t *trackedTrade) setSelfGoverned(is bool) (changed bool) {
   351  	if is {
   352  		return atomic.CompareAndSwapUint32(&t.selfGoverned, 0, 1)
   353  	}
   354  	return atomic.CompareAndSwapUint32(&t.selfGoverned, 1, 0)
   355  }
   356  
   357  func (t *trackedTrade) status() order.OrderStatus {
   358  	t.mtx.RLock()
   359  	defer t.mtx.RUnlock()
   360  	return t.metaData.Status
   361  }
   362  
   363  // cacheRedemptionFeeSuggestion sets the redeemFeeSuggestion for the
   364  // trackedTrade. If a request to the server for the fee suggestion must be made,
   365  // the request will be run in a goroutine, i.e. the field is not necessarily set
   366  // when this method returns. If there is a synced book, the estimate will always
   367  // be updated. If there is no synced book, but a non-zero fee suggestion is
   368  // already cached, no new requests will be made.
   369  //
   370  // The trackedTrade mutex should be held for reads for safe access to the
   371  // walletSet and the readyToTick flag.
   372  func (t *trackedTrade) cacheRedemptionFeeSuggestion() {
   373  	now := time.Now()
   374  
   375  	t.redeemFeeSuggestion.Lock()
   376  	defer t.redeemFeeSuggestion.Unlock()
   377  
   378  	if now.Sub(t.redeemFeeSuggestion.stamp) < freshRedeemFeeAge {
   379  		return
   380  	}
   381  
   382  	set := func(rate uint64) {
   383  		t.redeemFeeSuggestion.rate = rate
   384  		t.redeemFeeSuggestion.stamp = now
   385  	}
   386  
   387  	// Use the wallet's rate first. Note that this could make a costly request
   388  	// to an external fee oracle if an internal estimate is not available and
   389  	// the wallet settings permit external API requests.
   390  	toWallet := t.wallets.toWallet
   391  	if t.readyToTick && toWallet.connected() {
   392  		if feeRate := toWallet.feeRate(); feeRate != 0 {
   393  			set(feeRate)
   394  			return
   395  		}
   396  	}
   397  
   398  	// Check any book that might have the fee recorded from an epoch_report note
   399  	// (requires a book subscription).
   400  	redeemAsset := toWallet.AssetID
   401  	feeSuggestion := t.dc.bestBookFeeSuggestion(redeemAsset)
   402  	if feeSuggestion > 0 {
   403  		set(feeSuggestion)
   404  		return
   405  	}
   406  
   407  	// Fetch it from the server. Last resort!
   408  	go func() {
   409  		feeSuggestion = t.dc.fetchFeeRate(redeemAsset)
   410  		if feeSuggestion > 0 {
   411  			t.redeemFeeSuggestion.Lock()
   412  			set(feeSuggestion)
   413  			t.redeemFeeSuggestion.Unlock()
   414  		}
   415  	}()
   416  }
   417  
   418  // accountRedeemer is equivalent to calling
   419  // xcWallet.Wallet.(asset.AccountLocker) on the to-wallet.
   420  func (t *trackedTrade) accountRedeemer() (asset.AccountLocker, bool) {
   421  	ar, is := t.wallets.toWallet.Wallet.(asset.AccountLocker)
   422  	return ar, is
   423  }
   424  
   425  // accountRefunder is equivalent to calling
   426  // xcWallet.Wallet.(asset.AccountLocker) on the from-wallet.
   427  func (t *trackedTrade) accountRefunder() (asset.AccountLocker, bool) {
   428  	ar, is := t.wallets.fromWallet.Wallet.(asset.AccountLocker)
   429  	return ar, is
   430  }
   431  
   432  // lockRefundFraction locks the specified fraction of the available
   433  // refund reserves. Subsequent calls are additive. If a call to
   434  // lockRefundFraction would put the locked reserves > available reserves,
   435  // nothing will be reserved, and an error message is logged.
   436  func (t *trackedTrade) lockRefundFraction(num, denom uint64) {
   437  	refunder, is := t.accountRefunder()
   438  	if !is {
   439  		return
   440  	}
   441  	newReserved := t.reservesToLock(num, denom, t.refundReserves, t.refundLocked)
   442  	if newReserved == 0 {
   443  		return
   444  	}
   445  
   446  	if err := refunder.ReReserveRefund(newReserved); err != nil {
   447  		t.dc.log.Errorf("error re-reserving refund %d %s for order %s: %v",
   448  			newReserved, t.wallets.fromWallet.unitInfo().AtomicUnit, t.ID(), err)
   449  		return
   450  	}
   451  	t.refundLocked += newReserved
   452  }
   453  
   454  // lockRedemptionFraction locks the specified fraction of the available
   455  // redemption reserves. Subsequent calls are additive. If a call to
   456  // lockRedemptionFraction would put the locked reserves > available reserves,
   457  // nothing will be reserved, and an error message is logged.
   458  func (t *trackedTrade) lockRedemptionFraction(num, denom uint64) {
   459  	redeemer, is := t.accountRedeemer()
   460  	if !is {
   461  		return
   462  	}
   463  	newReserved := t.reservesToLock(num, denom, t.redemptionReserves, t.redemptionLocked)
   464  	if newReserved == 0 {
   465  		return
   466  	}
   467  
   468  	if err := redeemer.ReReserveRedemption(newReserved); err != nil {
   469  		t.dc.log.Errorf("error re-reserving redemption %d %s for order %s: %v",
   470  			newReserved, t.wallets.toWallet.unitInfo().AtomicUnit, t.ID(), err)
   471  		return
   472  	}
   473  	t.redemptionLocked += newReserved
   474  }
   475  
   476  // reservesToLock is a helper function used by lockRedemptionFraction and
   477  // lockRefundFraction to determine the amount of funds to lock.
   478  func (t *trackedTrade) reservesToLock(num, denom, reserves, reservesLocked uint64) uint64 {
   479  	newReserved := applyFraction(num, denom, reserves)
   480  	if reservesLocked+newReserved > reserves {
   481  		t.dc.log.Errorf("attempting to mark as active more reserves than available for order %s:"+
   482  			"%d available, %d already reserved, %d requested", t.ID(), t.redemptionReserves, t.redemptionLocked, newReserved)
   483  		return 0
   484  	}
   485  	return newReserved
   486  }
   487  
   488  // unlockRefundFraction unlocks the specified fraction of the refund
   489  // reserves. t.mtx should be locked if this trackedTrade is in the dc.trades
   490  // map. If the requested unlock would put the locked reserves < 0, an error
   491  // message is logged and the remaining locked reserves will be unlocked instead.
   492  // If the remaining locked reserves after this unlock is determined to be
   493  // "dust", it will be unlocked too.
   494  func (t *trackedTrade) unlockRefundFraction(num, denom uint64) {
   495  	refunder, is := t.accountRefunder()
   496  	if !is {
   497  		return
   498  	}
   499  	unlock := t.reservesToUnlock(num, denom, t.refundReserves, t.refundLocked)
   500  	t.refundLocked -= unlock
   501  	refunder.UnlockRefundReserves(unlock)
   502  }
   503  
   504  // unlockRedemptionFraction unlocks the specified fraction of the redemption
   505  // reserves. t.mtx should be locked if this trackedTrade is in the dc.trades
   506  // map. If the requested unlock would put the locked reserves < 0, an error
   507  // message is logged and the remaining locked reserves will be unlocked instead.
   508  // If the remaining locked reserves after this unlock is determined to be
   509  // "dust", it will be unlocked too.
   510  func (t *trackedTrade) unlockRedemptionFraction(num, denom uint64) {
   511  	redeemer, is := t.accountRedeemer()
   512  	if !is {
   513  		return
   514  	}
   515  	unlock := t.reservesToUnlock(num, denom, t.redemptionReserves, t.redemptionLocked)
   516  	t.redemptionLocked -= unlock
   517  	redeemer.UnlockRedemptionReserves(unlock)
   518  }
   519  
   520  // reservesToUnlock is a helper function used by unlockRedemptionFraction and
   521  // unlockRefundFraction to determine the amount of funds to unlock.
   522  func (t *trackedTrade) reservesToUnlock(num, denom, reserves, reservesLocked uint64) uint64 {
   523  	unlock := applyFraction(num, denom, reserves)
   524  	if unlock > reservesLocked {
   525  		t.dc.log.Errorf("attempting to unlock more than is reserved for order %s. unlocking reserved amount instead: "+
   526  			"%d reserved, unlocking %d", t.ID(), reservesLocked, unlock)
   527  		unlock = reservesLocked
   528  	}
   529  
   530  	reservesLocked -= unlock
   531  
   532  	// Can be dust. Clean it up.
   533  	var isDust bool
   534  	if t.isMarketBuy() {
   535  		isDust = reservesLocked < applyFraction(1, uint64(2*len(t.matches)), reserves)
   536  	} else if t.metaData.Status > order.OrderStatusBooked && len(t.matches) > 0 {
   537  		// Order is executed, so no changes should be expected. If there were
   538  		// zero matches, the return is expected to be fraction 1 / 1, so no
   539  		// reason to add handling for that case.
   540  		mkt := t.dc.marketConfig(t.mktID)
   541  		if mkt == nil {
   542  			t.dc.log.Errorf("reservesToUnlock: could not find market: %v", t.mktID)
   543  			return 0
   544  		}
   545  		lotSize := mkt.LotSize
   546  		qty := t.Trade().Quantity
   547  		// Dust if remaining reserved is less than the amount needed to
   548  		// reserve one lot, which would be the smallest trade. Flooring
   549  		// to avoid rounding issues.
   550  		isDust = reservesLocked < uint64(math.Floor(float64(lotSize)/float64(qty)*float64(reserves)))
   551  	}
   552  	if isDust {
   553  		unlock += reservesLocked
   554  	}
   555  	return unlock
   556  }
   557  
   558  func (t *trackedTrade) isMarketBuy() bool {
   559  	trade := t.Trade()
   560  	if trade == nil {
   561  		return false
   562  	}
   563  	return t.Type() == order.MarketOrderType && !trade.Sell
   564  }
   565  
   566  func (t *trackedTrade) epochLen() uint64 {
   567  	return t.metaData.EpochDur
   568  }
   569  
   570  func (t *trackedTrade) epochIdx() uint64 {
   571  	// Guard against bizarre circumstances with both an old order without epoch
   572  	// duration stored, AND a server that is either down or missing the market.
   573  	if t.epochLen() == 0 {
   574  		return 0
   575  	}
   576  	return uint64(t.Prefix().ServerTime.UnixMilli()) / t.epochLen()
   577  }
   578  
   579  // cancelEpochIdx gives the epoch index of any cancel associated cancel order.
   580  // The mutex must be at least read locked.
   581  func (t *trackedTrade) cancelEpochIdx() uint64 {
   582  	if t.cancel == nil {
   583  		return 0
   584  	}
   585  	epochLen := t.cancel.epochLen
   586  	if epochLen == 0 {
   587  		epochLen = t.epochLen()
   588  	}
   589  	if epochLen == 0 {
   590  		// In these strange circumstances, the cancel should be declared stale
   591  		// anyway (see hasStaleCancelOrder).
   592  		return 0
   593  	}
   594  	return uint64(t.cancel.Prefix().ServerTime.UnixMilli()) / epochLen
   595  }
   596  
   597  func (t *trackedTrade) verifyCSum(vsum dex.Bytes, epochIdx uint64) error {
   598  	t.mtx.RLock()
   599  	defer t.mtx.RUnlock()
   600  
   601  	t.csumMtx.RLock()
   602  	csum, cancelCsum := t.csum, t.cancelCsum
   603  	t.csumMtx.RUnlock()
   604  
   605  	// First check the trade's recorded csum, if it is in this epoch.
   606  	if epochIdx == t.epochIdx() && !bytes.Equal(vsum, csum) {
   607  		return fmt.Errorf("checksum %s != trade order preimage request checksum %s for trade order %v",
   608  			csum, csum, t.ID())
   609  	}
   610  
   611  	if t.cancel == nil {
   612  		return nil // no linked cancel order
   613  	}
   614  
   615  	// Check the linked cancel order if it is for this epoch.
   616  	if epochIdx == t.cancelEpochIdx() && !bytes.Equal(vsum, cancelCsum) {
   617  		return fmt.Errorf("checksum %s != cancel order preimage request checksum %s for cancel order %v",
   618  			vsum, cancelCsum, t.cancel.ID())
   619  	}
   620  
   621  	return nil // includes not in epoch
   622  }
   623  
   624  // rate returns the order's rate, or zero if a market or cancel order.
   625  func (t *trackedTrade) rate() uint64 {
   626  	if ord, ok := t.Order.(*order.LimitOrder); ok {
   627  		return ord.Rate
   628  	}
   629  	return 0
   630  }
   631  
   632  // broadcastTimeout gets associated DEX's configured broadcast timeout. If the
   633  // trade's dexConnection was unable to be established, 0 is returned.
   634  func (t *trackedTrade) broadcastTimeout() time.Duration {
   635  	t.dc.cfgMtx.RLock()
   636  	defer t.dc.cfgMtx.RUnlock()
   637  	// If the dexConnection was never established, we have no config.
   638  	if t.dc.cfg == nil {
   639  		return 0
   640  	}
   641  	return time.Millisecond * time.Duration(t.dc.cfg.BroadcastTimeout)
   642  }
   643  
   644  // coreOrder constructs a *core.Order for the tracked order.Order. If the trade
   645  // has a cancel order associated with it, the cancel order will be returned,
   646  // otherwise the second returned *Order will be nil.
   647  func (t *trackedTrade) coreOrder() *Order {
   648  	t.mtx.RLock()
   649  	defer t.mtx.RUnlock()
   650  	return t.coreOrderInternal()
   651  }
   652  
   653  // coreOrderInternal constructs a *core.Order for the tracked order.Order. If
   654  // the trade has a cancel order associated with it, the cancel order will be
   655  // returned, otherwise the second returned *Order will be nil. coreOrderInternal
   656  // should be called with the mtx >= RLocked.
   657  func (t *trackedTrade) coreOrderInternal() *Order {
   658  	corder := coreOrderFromTrade(t.Order, t.metaData)
   659  
   660  	corder.Epoch = t.dc.marketEpoch(t.mktID, t.Prefix().ServerTime)
   661  	corder.LockedAmt = t.lockedAmount()
   662  	corder.ParentAssetLockedAmt = t.parentLockedAmt()
   663  	corder.ReadyToTick = t.readyToTick
   664  	corder.RedeemLockedAmt = t.redemptionLocked
   665  	corder.RefundLockedAmt = t.refundLocked
   666  
   667  	allFeesConfirmed := true
   668  	for _, mt := range t.matches {
   669  		if !mt.MetaData.Proof.SwapFeeConfirmed || !mt.MetaData.Proof.RedemptionFeeConfirmed {
   670  			allFeesConfirmed = false
   671  		}
   672  		swapConfs, counterConfs := mt.confirms()
   673  		corder.Matches = append(corder.Matches, matchFromMetaMatchWithConfs(t, &mt.MetaMatch,
   674  			swapConfs, int64(t.metaData.FromSwapConf),
   675  			counterConfs, int64(t.metaData.ToSwapConf),
   676  			int64(mt.redemptionConfs), int64(mt.redemptionConfsReq)))
   677  	}
   678  	corder.AllFeesConfirmed = allFeesConfirmed
   679  
   680  	return corder
   681  }
   682  
   683  // hasFundingCoins indicates if either funding or change coins are locked.
   684  // This should be called with the mtx at least read locked.
   685  func (t *trackedTrade) hasFundingCoins() bool {
   686  	return t.changeLocked || t.coinsLocked
   687  }
   688  
   689  // lockedAmount is the total value of all coins currently locked for this trade.
   690  // Returns the value sum of the initial funding coins if no swap has been sent,
   691  // otherwise, the value of the locked change coin is returned.
   692  // NOTE: This amount only applies to the wallet from which swaps are sent. This
   693  // is the BASE asset wallet for a SELL order and the QUOTE asset wallet for a
   694  // BUY order.
   695  // lockedAmount should be called with the mtx >= RLocked.
   696  func (t *trackedTrade) lockedAmount() (locked uint64) {
   697  	if t.coinsLocked {
   698  		// This implies either no swap has been sent, or the trade has been
   699  		// resumed on restart after a swap that produced locked change (partial
   700  		// fill and still booked) since restarting loads into coins/coinsLocked.
   701  		for _, coin := range t.coins {
   702  			locked += coin.Value()
   703  		}
   704  	} else if t.changeLocked && t.change != nil { // change may be returned but unlocked if the last swap has been sent
   705  		locked = t.change.Value()
   706  	}
   707  	return
   708  }
   709  
   710  // parentLockedAmt returns the total amount of the parent asset locked for
   711  // funding swaps in this order.
   712  //
   713  // NOTE: This amount only applies to the wallet from which swaps are sent. This
   714  // is the BASE asset wallet for a SELL order and the QUOTE asset wallet for a
   715  // BUY order.
   716  // parentLockedAmt should be called with the mtx >= RLocked.
   717  func (t *trackedTrade) parentLockedAmt() (locked uint64) {
   718  	if t.coinsLocked {
   719  		// This implies either no swap has been sent, or the trade has been
   720  		// resumed on restart after a swap that produced locked change (partial
   721  		// fill and still booked) since restarting loads into coins/coinsLocked.
   722  		for _, coin := range t.coins {
   723  			if tokenCoin, is := coin.(asset.TokenCoin); is {
   724  				locked += tokenCoin.Fees()
   725  			}
   726  		}
   727  	} else if t.changeLocked && t.change != nil { // change may be returned but unlocked if the last swap has been sent
   728  		if tokenCoin, is := t.change.(asset.TokenCoin); is {
   729  			locked += tokenCoin.Fees()
   730  		}
   731  	}
   732  	return
   733  }
   734  
   735  // token is a string representation of the order ID.
   736  func (t *trackedTrade) token() string {
   737  	return (t.ID().String())
   738  }
   739  
   740  // clearCancel clears the unmatched cancel and deletes the cancel checksum and
   741  // link to the trade in the dexConnection. clearCancel must be called with the
   742  // trackedTrade.mtx locked.
   743  func (t *trackedTrade) clearCancel(preImg order.Preimage) {
   744  	if t.cancel != nil {
   745  		t.dc.deleteCancelLink(t.cancel.ID())
   746  		t.cancel = nil
   747  	}
   748  	t.csumMtx.Lock()
   749  	t.cancelCsum = nil
   750  	t.cancelPreimg = preImg
   751  	t.csumMtx.Unlock()
   752  }
   753  
   754  // cancelTrade sets the cancellation data with the order and its preimage.
   755  // cancelTrade must be called with the mtx write-locked.
   756  func (t *trackedTrade) cancelTrade(co *order.CancelOrder, preImg order.Preimage, epochLen uint64) error {
   757  	t.clearCancel(preImg)
   758  	t.cancel = &trackedCancel{
   759  		CancelOrder: *co,
   760  		epochLen:    epochLen,
   761  	}
   762  	cid := co.ID()
   763  	oid := t.ID()
   764  	t.dc.registerCancelLink(cid, oid)
   765  	err := t.db.LinkOrder(oid, cid)
   766  	if err != nil {
   767  		return fmt.Errorf("error linking cancel order %s for trade %s: %w", cid, oid, err)
   768  	}
   769  	t.metaData.LinkedOrder = cid
   770  	return nil
   771  }
   772  
   773  // nomatch sets the appropriate order status and returns funding coins.
   774  func (t *trackedTrade) nomatch(oid order.OrderID) (assetMap, error) {
   775  	assets := make(assetMap)
   776  	// Check if this is the cancel order.
   777  	t.mtx.Lock()
   778  	defer t.mtx.Unlock()
   779  	if t.ID() != oid {
   780  		if t.cancel == nil || t.cancel.ID() != oid {
   781  			return assets, newError(unknownOrderErr, "nomatch order ID %s does not match trade or cancel order", oid)
   782  		}
   783  		// This is a cancel order. Cancel status goes to executed, but the trade
   784  		// status will not be canceled. Remove the trackedCancel and remove the
   785  		// DB linked order from the trade, but not the cancel.
   786  		t.dc.log.Warnf("Cancel order %s targeting trade %s did not match.", oid, t.ID())
   787  		err := t.db.LinkOrder(t.ID(), order.OrderID{})
   788  		if err != nil {
   789  			t.dc.log.Errorf("DB error unlinking cancel order %s for trade %s: %v", oid, t.ID(), err)
   790  		}
   791  		// Clearing the trackedCancel allows this order to be canceled again.
   792  		t.clearCancel(order.Preimage{})
   793  		t.metaData.LinkedOrder = order.OrderID{}
   794  
   795  		subject, details := t.formatDetails(TopicMissedCancel, makeOrderToken(t.token()))
   796  		t.notify(newOrderNote(TopicMissedCancel, subject, details, db.WarningLevel, t.coreOrderInternal()))
   797  		return assets, t.db.UpdateOrderStatus(oid, order.OrderStatusExecuted)
   798  	}
   799  
   800  	// This is the trade. Return coins and set status based on whether this is
   801  	// a standing limit order or not.
   802  	if t.metaData.Status != order.OrderStatusEpoch {
   803  		return assets, fmt.Errorf("nomatch sent for non-epoch order %s", oid)
   804  	}
   805  	if lo, ok := t.Order.(*order.LimitOrder); ok && lo.Force == order.StandingTiF {
   806  		t.dc.log.Infof("Standing order %s did not match and is now booked.", t.token())
   807  		t.metaData.Status = order.OrderStatusBooked
   808  		t.notify(newOrderNote(TopicOrderBooked, "", "", db.Data, t.coreOrderInternal()))
   809  	} else {
   810  		t.returnCoins()
   811  		t.unlockRedemptionFraction(1, 1)
   812  		t.unlockRefundFraction(1, 1)
   813  		assets.count(t.wallets.fromWallet.AssetID)
   814  		t.dc.log.Infof("Non-standing order %s did not match.", t.token())
   815  		t.metaData.Status = order.OrderStatusExecuted
   816  		t.notify(newOrderNote(TopicNoMatch, "", "", db.Data, t.coreOrderInternal()))
   817  	}
   818  	return assets, t.db.UpdateOrderStatus(t.ID(), t.metaData.Status)
   819  }
   820  
   821  // negotiate creates and stores matchTrackers for the []*msgjson.Match, and
   822  // updates (UserMatch).Filled. Match negotiation can then be progressed by
   823  // calling (*trackedTrade).tick when a relevant event occurs, such as a request
   824  // from the DEX or a tip change.
   825  func (t *trackedTrade) negotiate(msgMatches []*msgjson.Match) error {
   826  	trade := t.Trade()
   827  	// Validate matches and check if a cancel match is included.
   828  	// Non-cancel matches should be negotiated and are added to
   829  	// the newTrackers slice.
   830  	var cancelMatch *msgjson.Match
   831  	newTrackers := make([]*matchTracker, 0, len(msgMatches))
   832  	for _, msgMatch := range msgMatches {
   833  		if len(msgMatch.MatchID) != order.MatchIDSize {
   834  			return fmt.Errorf("match id of incorrect length. expected %d, got %d",
   835  				order.MatchIDSize, len(msgMatch.MatchID))
   836  		}
   837  		var oid order.OrderID
   838  		copy(oid[:], msgMatch.OrderID)
   839  		if oid != t.ID() {
   840  			return fmt.Errorf("negotiate called for wrong order. %s != %s", oid, t.ID())
   841  		}
   842  
   843  		var mid order.MatchID
   844  		copy(mid[:], msgMatch.MatchID)
   845  		// Do not process matches with existing matchTrackers. e.g. In case we
   846  		// start "extra" matches from the 'connect' response negotiating via
   847  		// authDEX>readConnectMatches, and a subsequent resent 'match' request
   848  		// leads us here again or vice versa. Or just duplicate match requests.
   849  		if t.matches[mid] != nil {
   850  			t.dc.log.Warnf("Skipping match %v that is already negotiating.", mid)
   851  			continue
   852  		}
   853  
   854  		// Check if this is a match with a cancel order, in which case the
   855  		// counterparty Address field would be empty. If the user placed a
   856  		// cancel order, that order will be recorded in t.cancel on cancel
   857  		// order creation via (*dexConnection).tryCancel or restored from DB
   858  		// via (*Core).dbTrackers.
   859  		if t.cancel != nil && msgMatch.Address == "" {
   860  			cancelMatch = msgMatch
   861  			continue
   862  		}
   863  
   864  		match := &matchTracker{
   865  			prefix:          t.Prefix(),
   866  			trade:           trade,
   867  			MetaMatch:       *t.makeMetaMatch(msgMatch),
   868  			counterConfirms: -1, // initially unknown, log first check
   869  			lastExpireDur:   365 * 24 * time.Hour,
   870  		}
   871  		match.Status = order.NewlyMatched // these must be new matches
   872  		newTrackers = append(newTrackers, match)
   873  	}
   874  
   875  	// Record any cancel order Match and update order status.
   876  	var metaCancelMatch *db.MetaMatch
   877  	if cancelMatch != nil {
   878  		t.dc.log.Infof("Order %s canceled. match id = %s",
   879  			t.ID(), cancelMatch.MatchID)
   880  
   881  		// Set this order status to Canceled and unlock any locked coins
   882  		// if there are no new matches and there's no need to send swap
   883  		// for any previous match.
   884  		t.metaData.Status = order.OrderStatusCanceled
   885  		if len(newTrackers) == 0 {
   886  			t.maybeReturnCoins()
   887  		}
   888  
   889  		// Note: In TopicNewMatch later, it must be status complete to agree
   890  		// with coreOrderFromMetaOrder, which pulls match data *from the DB*.
   891  		cancelMatch.Status = uint8(order.MatchComplete) // we're completing it now
   892  		cancelMatch.Address = ""                        // not a trade match
   893  
   894  		t.cancel.matches.maker = cancelMatch // taker is stored via processCancelMatch before negotiate
   895  		// Set the order status for the cancel order.
   896  		err := t.db.UpdateOrderStatus(t.cancel.ID(), order.OrderStatusExecuted)
   897  		if err != nil {
   898  			t.dc.log.Errorf("Failed to update status of cancel order %v to executed: %v",
   899  				t.cancel.ID(), err)
   900  			// Try to store the match anyway.
   901  		}
   902  		// Store a completed maker cancel match in the DB.
   903  		metaCancelMatch = t.makeMetaMatch(cancelMatch)
   904  		err = t.db.UpdateMatch(metaCancelMatch)
   905  		if err != nil {
   906  			return fmt.Errorf("failed to update match in db: %w", err)
   907  		}
   908  	}
   909  
   910  	// Now that each Match in msgMatches has been validated, store them in the
   911  	// trackedTrade and the DB, and update the newFill amount.
   912  	var newFill uint64
   913  	for _, match := range newTrackers {
   914  		var qty uint64
   915  		if t.isMarketBuy() {
   916  			qty = calc.BaseToQuote(match.Rate, match.Quantity)
   917  		} else {
   918  			qty = match.Quantity
   919  		}
   920  		newFill += qty
   921  
   922  		if trade.Filled()+newFill > trade.Quantity {
   923  			t.dc.log.Errorf("Match %s would put order %s fill over quantity. Revoking the match.",
   924  				match, t.ID())
   925  			match.MetaData.Proof.SelfRevoked = true
   926  		}
   927  
   928  		// If this order has no funding coins, block swaps attempts on the new
   929  		// match. Do not revoke however since the user may be able to resolve
   930  		// wallet configuration issues and restart to restore funding coins.
   931  		// Otherwise the server will end up revoking these matches.
   932  		if !t.hasFundingCoins() {
   933  			t.dc.log.Errorf("Unable to begin swap negotiation for unfunded order %v", t.ID())
   934  			match.swapErr = errors.New("no funding coins for swap")
   935  		}
   936  
   937  		err := t.db.UpdateMatch(&match.MetaMatch)
   938  		if err != nil {
   939  			// Don't abandon other matches because of this error, attempt
   940  			// to negotiate the other matches.
   941  			t.dc.log.Errorf("failed to update match %s in db: %v", match, err)
   942  			continue
   943  		}
   944  
   945  		// Only add this match to the map if the db update succeeds, so
   946  		// funds don't get stuck if user restarts Core after sending a
   947  		// swap because negotiations will not be resumed for this match
   948  		// and auto-refund cannot be performed.
   949  		// TODO: Maybe allow? This match can be restored from the DEX's
   950  		// connect response on restart IF it is not revoked.
   951  		t.matches[match.MatchID] = match
   952  		t.dc.log.Infof("Starting negotiation for match %s for order %v with swap fee rate = %v, quantity = %v",
   953  			match, t.ID(), match.FeeRateSwap, qty)
   954  	}
   955  
   956  	// If the order has been canceled, add that to filled and newFill.
   957  	preCancelFilled, canceled := t.recalcFilled()
   958  	filled := preCancelFilled + canceled
   959  	if cancelMatch != nil {
   960  		newFill += cancelMatch.Quantity
   961  	}
   962  	// The filled amount includes all of the trackedTrade's matches, so the
   963  	// filled amount must be set, not just increased.
   964  	trade.SetFill(filled)
   965  
   966  	// Before we update any order statuses, check if this is a market sell
   967  	// order or an immediate TiF limit order which has just been executed. We
   968  	// can return reserves for the remaining part of an order which will not
   969  	// filled in the future if the order is a market sell, an immediate TiF
   970  	// limit order, or if the order was cancelled.
   971  	var completedMarketSell, completedImmediateTiF bool
   972  	completedMarketSell = trade.Sell && t.Type() == order.MarketOrderType && t.metaData.Status < order.OrderStatusExecuted
   973  	lo, ok := t.Order.(*order.LimitOrder)
   974  	if ok {
   975  		completedImmediateTiF = lo.Force == order.ImmediateTiF && t.metaData.Status < order.OrderStatusExecuted
   976  	}
   977  	if remain := trade.Quantity - preCancelFilled; remain > 0 && (completedMarketSell || completedImmediateTiF || cancelMatch != nil) {
   978  		t.unlockRedemptionFraction(remain, trade.Quantity)
   979  		t.unlockRefundFraction(remain, trade.Quantity)
   980  	}
   981  
   982  	// Set the order as executed depending on type and fill.
   983  	if t.metaData.Status != order.OrderStatusCanceled && t.metaData.Status != order.OrderStatusRevoked {
   984  		if lo, ok := t.Order.(*order.LimitOrder); ok && lo.Force == order.StandingTiF && filled < trade.Quantity {
   985  			t.metaData.Status = order.OrderStatusBooked
   986  		} else {
   987  			t.metaData.Status = order.OrderStatusExecuted
   988  		}
   989  	}
   990  
   991  	// Send notifications.
   992  	corder := t.coreOrderInternal()
   993  	if metaCancelMatch != nil {
   994  		topic := TopicBuyOrderCanceled
   995  		if trade.Sell {
   996  			topic = TopicSellOrderCanceled
   997  		}
   998  		subject, details := t.formatDetails(topic, unbip(t.Base()), unbip(t.Quote()), t.dc.acct.host, makeOrderToken(t.token()))
   999  
  1000  		t.notify(newOrderNote(topic, subject, details, db.Poke, corder))
  1001  		// Also send out a data notification with the cancel order information.
  1002  		t.notify(newOrderNote(TopicCancel, "", "", db.Data, corder))
  1003  		t.notify(newMatchNote(TopicNewMatch, "", "", db.Data, t, &matchTracker{
  1004  			prefix:    t.Prefix(),
  1005  			trade:     trade,
  1006  			MetaMatch: *metaCancelMatch,
  1007  		}))
  1008  	}
  1009  	if len(newTrackers) > 0 {
  1010  		fillPct := 100 * float64(filled) / float64(trade.Quantity)
  1011  		t.dc.log.Debugf("Trade order %v matched with %d orders: +%d filled, total fill %d / %d (%.1f%%)",
  1012  			t.ID(), len(newTrackers), newFill, filled, trade.Quantity, fillPct)
  1013  
  1014  		// Match notifications.
  1015  		for _, match := range newTrackers {
  1016  			t.notify(newMatchNote(TopicNewMatch, "", "", db.Data, t, match))
  1017  		}
  1018  
  1019  		// A single order notification.
  1020  		topic := TopicBuyMatchesMade
  1021  		if trade.Sell {
  1022  			topic = TopicSellMatchesMade
  1023  		}
  1024  		subject, details := t.formatDetails(topic, unbip(t.Base()), unbip(t.Quote()), fillPct, makeOrderToken(t.token()))
  1025  		t.notify(newOrderNote(topic, subject, details, db.Poke, corder))
  1026  	}
  1027  
  1028  	err := t.db.UpdateOrder(t.metaOrder())
  1029  	if err != nil {
  1030  		return fmt.Errorf("failed to update order in db: %w", err)
  1031  	}
  1032  	return nil
  1033  }
  1034  
  1035  func (t *trackedTrade) recalcFilled() (matchFilled, canceled uint64) {
  1036  	for _, mt := range t.matches {
  1037  		if t.isMarketBuy() {
  1038  			matchFilled += calc.BaseToQuote(mt.Rate, mt.Quantity)
  1039  		} else {
  1040  			matchFilled += mt.Quantity
  1041  		}
  1042  	}
  1043  	if t.cancel != nil && t.cancel.matches.maker != nil {
  1044  		canceled = t.cancel.matches.maker.Quantity
  1045  	}
  1046  	t.Trade().SetFill(matchFilled + canceled)
  1047  	return
  1048  }
  1049  
  1050  func (t *trackedTrade) metaOrder() *db.MetaOrder {
  1051  	return &db.MetaOrder{
  1052  		MetaData: t.metaData,
  1053  		Order:    t.Order,
  1054  	}
  1055  }
  1056  
  1057  func (t *trackedTrade) makeMetaMatch(msgMatch *msgjson.Match) *db.MetaMatch {
  1058  	// Contract txn asset: buy means quote, sell means base. NOTE: msgjson.Match
  1059  	// could instead have just FeeRateSwap for the recipient, but the other fee
  1060  	// rate could be of value for auditing the counter party's contract txn.
  1061  	feeRateSwap := msgMatch.FeeRateQuote
  1062  	if t.Trade().Sell {
  1063  		feeRateSwap = msgMatch.FeeRateBase
  1064  	}
  1065  
  1066  	// Consider: bump fee rate here based on a user setting in dexConnection.
  1067  	// feeRateSwap = feeRateSwap * 11 / 10
  1068  	// maxFeeRate := t.dc.assets[swapAssetID].MaxFeeRate // swapAssetID according to t.Trade().Sell and t.Base()/Quote()
  1069  	// if feeRateSwap > maxFeeRate {
  1070  	// 	feeRateSwap = maxFeeRate
  1071  	// }
  1072  
  1073  	var oid order.OrderID
  1074  	copy(oid[:], msgMatch.OrderID)
  1075  	var mid order.MatchID
  1076  	copy(mid[:], msgMatch.MatchID)
  1077  	return &db.MetaMatch{
  1078  		MetaData: &db.MatchMetaData{
  1079  			Proof: db.MatchProof{
  1080  				Auth: db.MatchAuth{
  1081  					MatchSig:   msgMatch.Sig,
  1082  					MatchStamp: msgMatch.ServerTime,
  1083  				},
  1084  			},
  1085  			DEX:   t.dc.acct.host,
  1086  			Base:  t.Base(),
  1087  			Quote: t.Quote(),
  1088  			Stamp: msgMatch.ServerTime,
  1089  		},
  1090  		UserMatch: &order.UserMatch{
  1091  			OrderID:     oid,
  1092  			MatchID:     mid,
  1093  			Quantity:    msgMatch.Quantity,
  1094  			Rate:        msgMatch.Rate,
  1095  			Address:     msgMatch.Address,
  1096  			Status:      order.MatchStatus(msgMatch.Status),
  1097  			Side:        order.MatchSide(msgMatch.Side),
  1098  			FeeRateSwap: feeRateSwap,
  1099  		},
  1100  	}
  1101  }
  1102  
  1103  // processCancelMatch should be called with the message for the match on a
  1104  // cancel order.
  1105  func (t *trackedTrade) processCancelMatch(msgMatch *msgjson.Match) error {
  1106  	var oid order.OrderID
  1107  	copy(oid[:], msgMatch.OrderID)
  1108  	var mid order.MatchID
  1109  	copy(mid[:], msgMatch.MatchID)
  1110  	t.mtx.Lock()
  1111  	defer t.mtx.Unlock()
  1112  	if t.cancel == nil {
  1113  		return fmt.Errorf("no cancel order recorded for order %v", oid)
  1114  	}
  1115  	if oid != t.cancel.ID() {
  1116  		return fmt.Errorf("negotiate called for wrong order. %s != %s", oid, t.cancel.ID())
  1117  	}
  1118  	// Maker notification is logged at info.
  1119  	t.dc.log.Debugf("Taker notification for cancel order %v received. Match id = %s", oid, mid)
  1120  	t.cancel.matches.taker = msgMatch
  1121  	// Store the completed taker cancel match.
  1122  	takerCancelMeta := t.makeMetaMatch(t.cancel.matches.taker)
  1123  	takerCancelMeta.Status = order.MatchComplete
  1124  	takerCancelMeta.Address = "" // not a trade match
  1125  	err := t.db.UpdateMatch(takerCancelMeta)
  1126  	if err != nil {
  1127  		return fmt.Errorf("failed to update match in db: %w", err)
  1128  	}
  1129  	return nil
  1130  }
  1131  
  1132  // Get the required and current confirmation count on the counterparty's swap
  1133  // contract transaction for the provided match. If the count has not changed
  1134  // since the previous check, changed will be false.
  1135  //
  1136  // This method accesses match fields and MUST be called with the trackedTrade
  1137  // mutex lock held for reads.
  1138  func (t *trackedTrade) counterPartyConfirms(ctx context.Context, match *matchTracker) (have, needed uint32, changed, spent, expired bool, err error) {
  1139  	fail := func(err error) (uint32, uint32, bool, bool, bool, error) {
  1140  		return 0, 0, false, false, false, err
  1141  	}
  1142  
  1143  	// Counter-party's swap is the "to" asset.
  1144  	needed = t.metaData.ToSwapConf
  1145  
  1146  	// Check the confirmations on the counter-party's swap. If counterSwap is
  1147  	// not set, we shouldn't be here, but catch this just in case.
  1148  	if match.counterSwap == nil {
  1149  		return fail(errors.New("no AuditInfo available to check"))
  1150  	}
  1151  
  1152  	wallet := t.wallets.toWallet
  1153  	coin := match.counterSwap.Coin
  1154  
  1155  	if !wallet.connected() {
  1156  		return fail(errWalletNotConnected)
  1157  	}
  1158  
  1159  	_, lockTime, err := wallet.ContractLockTimeExpired(ctx, match.MetaData.Proof.CounterContract)
  1160  	if err != nil {
  1161  		return fail(fmt.Errorf("error checking if locktime has expired on taker's contract on order %s, "+
  1162  			"match %s: %w", t.ID(), match, err))
  1163  	}
  1164  	expired = time.Until(lockTime) < 0 // not necessarily refundable, but can be at any moment
  1165  
  1166  	have, spent, err = wallet.swapConfirmations(ctx, coin.ID(),
  1167  		match.MetaData.Proof.CounterContract, match.MetaData.Stamp)
  1168  	if err != nil {
  1169  		return fail(fmt.Errorf("failed to get confirmations of the counter-party's swap %s (%s) "+
  1170  			"for match %s, order %v: %w",
  1171  			coin, t.wallets.toWallet.Symbol, match, t.UID(), err))
  1172  	}
  1173  
  1174  	// Log the pending swap status at new heights only.
  1175  	was := match.setCounterConfirms(int64(have))
  1176  	if changed = was != int64(have); changed {
  1177  		t.notify(newMatchNote(TopicCounterConfirms, "", "", db.Data, t, match))
  1178  	}
  1179  
  1180  	return
  1181  }
  1182  
  1183  // deleteCancelOrder will clear any associated trackedCancel, and set the status
  1184  // of the cancel order as revoked in the DB so that it will not be loaded with
  1185  // other active orders on startup. The trackedTrade's OrderMetaData.LinkedOrder
  1186  // is also zeroed, but the caller is responsible for updating the trade's DB
  1187  // entry in a way that is appropriate for the caller (e.g. with LinkOrder,
  1188  // UpdateOrder, or UpdateOrderStatus).
  1189  //
  1190  // This is to be used in trade status resolution only, since normally the fate
  1191  // of cancel orders is determined by match/nomatch and status set to executed
  1192  // (see nomatch and negotiate). A missed preimage request for the cancel order
  1193  // that results in a revoke_order message for the cancel order should also use
  1194  // this method to unlink and retire the failed cancel order. Similarly, cancel
  1195  // orders detected as "stale" with the two-epochs-old heuristic use this.
  1196  //
  1197  // This method MUST be called with the trackedTrade mutex lock held for writes.
  1198  func (t *trackedTrade) deleteCancelOrder() {
  1199  	if t.cancel == nil {
  1200  		return
  1201  	}
  1202  	cid := t.cancel.ID()
  1203  	err := t.db.UpdateOrderStatus(cid, order.OrderStatusRevoked) // could actually be OrderStatusExecuted
  1204  	if err != nil {
  1205  		t.dc.log.Errorf("Error updating status in db for cancel order %v to revoked: %v", cid, err)
  1206  	}
  1207  	// Unlink the cancel order from the trade.
  1208  	t.clearCancel(order.Preimage{})
  1209  	t.metaData.LinkedOrder = order.OrderID{} // NOTE: caller may wish to update the trades's DB entry
  1210  }
  1211  
  1212  func (t *trackedTrade) hasStaleCancelOrder() bool {
  1213  	if t.cancel == nil || t.metaData.Status != order.OrderStatusBooked {
  1214  		return false
  1215  	}
  1216  
  1217  	epoch := order.EpochID{Idx: t.cancelEpochIdx(), Dur: t.epochLen()}
  1218  	epochEnd := epoch.End()
  1219  
  1220  	return time.Since(epochEnd) >= preimageReqTimeout
  1221  }
  1222  
  1223  // deleteStaleCancelOrder checks if this trade has an associated cancel order,
  1224  // and deletes the cancel order if the cancel order stays at Epoch status for
  1225  // more than 2 epochs. Deleting the stale cancel order from this trade makes
  1226  // it possible for the client to re- attempt cancelling the order.
  1227  //
  1228  // NOTE:
  1229  // Stale cancel orders would be Executed if their preimage was sent or Revoked
  1230  // if their preimages was not sent. We cannot currently tell whether the cancel
  1231  // order's preimage was revealed, so assume that the cancel order is Executed
  1232  // but unmatched. Consider adding a order.PreimageRevealed field to ensure that
  1233  // the correct final status is set for the cancel order; or allow the server to
  1234  // check and return status of cancel orders.
  1235  //
  1236  // This method MUST be called with the trackedTrade mutex lock held for writes.
  1237  func (t *trackedTrade) deleteStaleCancelOrder() {
  1238  	if !t.hasStaleCancelOrder() {
  1239  		return
  1240  	}
  1241  
  1242  	t.dc.log.Infof("Cancel order %v in epoch status with server time stamp %v (%v old) considered executed and unmatched.",
  1243  		t.cancel.ID(), t.cancel.ServerTime, time.Since(t.cancel.ServerTime))
  1244  
  1245  	// Clear the trackedCancel, allowing this order to be canceled again, and
  1246  	// set the cancel order's status as revoked.
  1247  	cancelOrd := t.cancel
  1248  	t.deleteCancelOrder()
  1249  	err := t.db.LinkOrder(t.ID(), order.OrderID{})
  1250  	if err != nil {
  1251  		t.dc.log.Errorf("DB error unlinking cancel order %s for trade %s: %v", cancelOrd.ID(), t.ID(), err)
  1252  	}
  1253  
  1254  	subject, details := t.formatDetails(TopicFailedCancel, makeOrderToken(t.token()))
  1255  	t.notify(newOrderNote(TopicFailedCancel, subject, details, db.WarningLevel, t.coreOrderInternal()))
  1256  }
  1257  
  1258  // isActive will be true if the trade is booked or epoch, or if any of the
  1259  // matches are still negotiating.
  1260  func (t *trackedTrade) isActive() bool {
  1261  	t.mtx.RLock()
  1262  	defer t.mtx.RUnlock()
  1263  
  1264  	// Status of the order itself.
  1265  	if t.metaData.Status == order.OrderStatusBooked ||
  1266  		t.metaData.Status == order.OrderStatusEpoch {
  1267  		return true
  1268  	}
  1269  
  1270  	// Status of all matches for the order.
  1271  	for _, match := range t.matches {
  1272  		// For debugging issues with match status and steps:
  1273  		// proof := &match.MetaData.Proof
  1274  		// t.dc.log.Tracef("Checking match %s (%v) in status %v. "+
  1275  		// 	"Order: %v, Refund coin: %v, ContractData: %x, Revoked: %v", match,
  1276  		// 	match.Side, match.Status, t.ID(),
  1277  		// 	proof.RefundCoin, proof.ContractData, proof.IsRevoked())
  1278  		if t.matchIsActive(match) {
  1279  			return true
  1280  		}
  1281  	}
  1282  	return false
  1283  }
  1284  
  1285  // matchIsRevoked checks if the match is revoked, RLocking the trackedTrade.
  1286  func (t *trackedTrade) matchIsRevoked(match *matchTracker) bool {
  1287  	t.mtx.RLock()
  1288  	defer t.mtx.RUnlock()
  1289  	return match.MetaData.Proof.IsRevoked()
  1290  }
  1291  
  1292  // Matches are inactive if: (1) status is confirmed, (2) it is refunded, or (3)
  1293  // it is revoked and this side of the match requires no further action.
  1294  func (t *trackedTrade) matchIsActive(match *matchTracker) bool {
  1295  	proof := &match.MetaData.Proof
  1296  	isActive := db.MatchIsActive(match.UserMatch, proof)
  1297  	if proof.IsRevoked() && !isActive {
  1298  		t.dc.log.Tracef("Revoked match %s (%v) in status %v considered inactive.",
  1299  			match, match.Side, match.Status)
  1300  	}
  1301  	return isActive
  1302  }
  1303  
  1304  // activeMatches returns active matches.
  1305  func (t *trackedTrade) activeMatches() []*matchTracker {
  1306  	var actives []*matchTracker
  1307  	t.mtx.RLock()
  1308  	defer t.mtx.RUnlock()
  1309  	for _, match := range t.matches {
  1310  		if t.matchIsActive(match) {
  1311  			actives = append(actives, match)
  1312  		}
  1313  	}
  1314  	return actives
  1315  }
  1316  
  1317  // unspentContractAmounts returns the total amount locked in unspent swaps.
  1318  // NOTE: This amount only applies to the wallet from which swaps are sent. This
  1319  // is the BASE asset wallet for a SELL order and the QUOTE asset wallet for a
  1320  // BUY order.
  1321  // unspentContractAmounts should be called with the mtx >= RLocked.
  1322  func (t *trackedTrade) unspentContractAmounts() (amount uint64) {
  1323  	swapSentFromQuoteAsset := t.fromAssetID == t.Quote()
  1324  	for _, match := range t.matches {
  1325  		side, status := match.Side, match.Status
  1326  		if status >= order.MakerRedeemed || len(match.MetaData.Proof.RefundCoin) != 0 {
  1327  			// Any redemption or own refund implies our swap is spent.
  1328  			// Even if we're Maker and our swap has not been redeemed
  1329  			// by Taker, we should consider it spent.
  1330  			continue
  1331  		}
  1332  		if (side == order.Maker && status >= order.MakerSwapCast) ||
  1333  			(side == order.Taker && status == order.TakerSwapCast) {
  1334  			swapAmount := match.Quantity
  1335  			if swapSentFromQuoteAsset {
  1336  				swapAmount = calc.BaseToQuote(match.Rate, match.Quantity)
  1337  			}
  1338  			amount += swapAmount
  1339  		}
  1340  	}
  1341  	return
  1342  }
  1343  
  1344  // isSwappable will be true if the match is ready for a swap transaction to be
  1345  // broadcast.
  1346  //
  1347  // In certain situations, the match should be revoked and the return will
  1348  // indicate this. In particular, the situations are when the match is in
  1349  // MakerSwapCast on the taker side and the maker's swap is found to be either
  1350  // spent or expired, or if our future contract would have an expiry in the past.
  1351  // Such matches are also not swappable.
  1352  //
  1353  // This method accesses match fields and MUST be called with the trackedTrade
  1354  // mutex lock held for reads.
  1355  func (t *trackedTrade) isSwappable(ctx context.Context, match *matchTracker) (ready, shouldRevoke bool) {
  1356  	// Quick status check before we bother with the wallet.
  1357  	switch match.Status {
  1358  	case order.TakerSwapCast, order.MakerRedeemed, order.MatchComplete:
  1359  		return false, false // all swaps already sent
  1360  	}
  1361  
  1362  	if match.swapErr != nil || match.MetaData.Proof.IsRevoked() {
  1363  		// t.dc.log.Tracef("Match %s not swappable: swapErr = %v, revoked = %v",
  1364  		// 	match, match.swapErr, match.MetaData.Proof.IsRevoked())
  1365  		return false, false
  1366  	}
  1367  	if ticksGoverned, checkServerRevoke := match.exceptions(); ticksGoverned || checkServerRevoke {
  1368  		// t.dc.log.Tracef("Match %s not swappable: metered = %t, checkServerRevoke = %v",
  1369  		// 	match, ticksGoverned, checkServerRevoke)
  1370  		return false, false
  1371  	}
  1372  
  1373  	wallet := t.wallets.fromWallet
  1374  	// Just a quick check here. We'll perform a more thorough check if there are
  1375  	// actually swappables.
  1376  	if !wallet.locallyUnlocked() {
  1377  		t.dc.log.Errorf("Order %s, match %s is not swappable because %s wallet is not unlocked",
  1378  			t.ID(), match, unbip(wallet.AssetID))
  1379  		return false, false
  1380  	}
  1381  
  1382  	defer func() {
  1383  		// We should never try to init when the server is known to be down or
  1384  		// lacks this market, but allow all the checks to run first for the sake
  1385  		// of confirmation notifications and conditional self-revocation.
  1386  		ready = ready && !t.isSelfGoverned() && t.dc.status() == comms.Connected // NOTE: swapMatchGroup rechecks dc conn anyway
  1387  	}()
  1388  
  1389  	switch match.Status {
  1390  	case order.NewlyMatched:
  1391  		return match.Side == order.Maker, false
  1392  	case order.MakerSwapCast:
  1393  		// Get the confirmation count on the maker's coin.
  1394  		if match.Side == order.Taker {
  1395  			// If the maker is the counterparty, we can determine swappability
  1396  			// based on the confirmations.
  1397  			confs, req, changed, spent, expired, err := t.counterPartyConfirms(ctx, match)
  1398  			if err != nil {
  1399  				if !errors.Is(err, asset.ErrSwapNotInitiated) {
  1400  					// We cannot get the swap data yet but there is no need
  1401  					// to log an error if swap not initiated as this is
  1402  					// expected for newly made swaps involving contracts.
  1403  					t.dc.log.Errorf("isSwappable: %v", err)
  1404  				}
  1405  				return false, false
  1406  			}
  1407  			if spent {
  1408  				t.dc.log.Errorf("Counter-party's swap is spent before we could broadcast our own. REVOKING!")
  1409  				return false, true // REVOKE!
  1410  			}
  1411  			if expired {
  1412  				t.dc.log.Errorf("Counter-party's swap expired before we could broadcast our own. REVOKING!")
  1413  				return false, true // REVOKE!
  1414  			}
  1415  			matchTime := match.matchTime()
  1416  			if lockTime := matchTime.Add(t.lockTimeTaker); time.Until(lockTime) < 0 {
  1417  				t.dc.log.Errorf("Our contract would expire in the past (%v). REVOKING!", lockTime)
  1418  				return false, true // REVOKE!
  1419  			}
  1420  			ready = confs >= req
  1421  			if changed && !ready {
  1422  				t.dc.log.Debugf("Match %s not yet swappable: current confs = %d, required confs = %d",
  1423  					match, confs, req)
  1424  			}
  1425  			return ready, false
  1426  		}
  1427  
  1428  		// If we're the maker, check the confirmations anyway so we can notify.
  1429  		confs, spent, err := wallet.swapConfirmations(ctx, match.MetaData.Proof.MakerSwap,
  1430  			match.MetaData.Proof.ContractData, match.MetaData.Stamp)
  1431  		if err != nil && !errors.Is(err, asset.ErrSwapNotInitiated) {
  1432  			// No need to log an error if swap not initiated as this
  1433  			// is expected for newly made swaps involving contracts.
  1434  			t.dc.log.Errorf("isSwappable: error getting confirmation for our own swap transaction: %v", err)
  1435  		}
  1436  		if spent { // This should NEVER happen for maker in MakerSwapCast unless revoked and refunded!
  1437  			t.dc.log.Errorf("Our (maker) swap for match %s is being reported as spent before taker's swap was broadcast!", match)
  1438  		}
  1439  		match.setSwapConfirms(int64(confs))
  1440  		t.notify(newMatchNote(TopicConfirms, "", "", db.Data, t, match))
  1441  		return false, false
  1442  	}
  1443  
  1444  	return false, false
  1445  }
  1446  
  1447  // checkSwapFeeConfirms returns whether the swap fee confirmations should be
  1448  // checked.
  1449  //
  1450  // This method accesses match fields and MUST be called with the trackedTrade
  1451  // mutex lock held for reads.
  1452  func (t *trackedTrade) checkSwapFeeConfirms(match *matchTracker) bool {
  1453  	if match.MetaData.Proof.SwapFeeConfirmed {
  1454  		return false
  1455  	}
  1456  	_, dynamic := t.wallets.fromWallet.Wallet.(asset.DynamicSwapper)
  1457  	if !dynamic {
  1458  		// Confirmed will be set in the db.
  1459  		return true
  1460  	}
  1461  	// Waiting until the swap is definitely confirmed in order to not
  1462  	// keep calling the fee checker before the swap is confirmed.
  1463  	mySwapConfs, _ := match.confirms()
  1464  	if match.Side == order.Maker {
  1465  		return match.Status > order.MakerSwapCast || mySwapConfs > 0
  1466  	}
  1467  	return match.Status > order.TakerSwapCast || mySwapConfs > 0
  1468  }
  1469  
  1470  // checkRedemptionFeeConfirms returns whether the swap fee confirmations should
  1471  // be checked.
  1472  //
  1473  // This method accesses match fields and MUST be called with the trackedTrade
  1474  // mutex lock held for reads.
  1475  func (t *trackedTrade) checkRedemptionFeeConfirms(match *matchTracker) bool {
  1476  	if match.MetaData.Proof.RedemptionFeeConfirmed || match.redemptionRejected {
  1477  		return false
  1478  	}
  1479  	_, dynamic := t.wallets.toWallet.Wallet.(asset.DynamicSwapper)
  1480  	if !dynamic {
  1481  		// Confirmed will be set in the db.
  1482  		return true
  1483  	}
  1484  	if match.Side == order.Maker {
  1485  		return match.Status >= order.MakerRedeemed
  1486  	}
  1487  	return match.Status >= order.MatchComplete
  1488  }
  1489  
  1490  // updateDynamicSwapOrRedemptionFeesPaid updates the fees used for dynamic fee
  1491  // checker transactions. We do not know the exact fees the tx will use until
  1492  // they are mined, so this waits until they are mined and updates the value for
  1493  // the entire trade.
  1494  //
  1495  // NOTE: As long as init and redemption confirms add up to more than two this
  1496  // method will fire as expected before the swap is determined in Confirmed
  1497  // status. Swaps naturally require a certain number of redemption confirms
  1498  // before they are confirmed so this is currently ensured.
  1499  func (t *trackedTrade) updateDynamicSwapOrRedemptionFeesPaid(ctx context.Context, match *matchTracker, isInit bool) {
  1500  	wallet := t.wallets.fromWallet
  1501  	if !isInit {
  1502  		wallet = t.wallets.toWallet
  1503  	}
  1504  	stopChecks := func() {
  1505  		if isInit {
  1506  			match.MetaData.Proof.SwapFeeConfirmed = true
  1507  		} else {
  1508  			match.MetaData.Proof.RedemptionFeeConfirmed = true
  1509  		}
  1510  		err := t.db.UpdateOrderMetaData(t.ID(), t.metaData)
  1511  		if err != nil {
  1512  			t.dc.log.Errorf("Error updating order metadata for order %s: %v", t.ID(), err)
  1513  		}
  1514  	}
  1515  	feeChecker, dynamic := wallet.Wallet.(asset.DynamicSwapper)
  1516  	if !dynamic {
  1517  		stopChecks()
  1518  		return
  1519  	}
  1520  	txType := "swap"
  1521  	if !isInit {
  1522  		txType = "redemption"
  1523  	}
  1524  	var coinID, contractData []byte
  1525  	// Check if a swap or redeem coin id has been populated in the
  1526  	// match tracker. If it has we ask the wallet for the fees paid
  1527  	// and add that to either the total swap or redeem fees for the
  1528  	// trade.
  1529  	if isInit {
  1530  		coinID = []byte(match.MetaData.Proof.MakerSwap)
  1531  		if match.Side != order.Maker {
  1532  			coinID = []byte(match.MetaData.Proof.TakerSwap)
  1533  		}
  1534  		contractData = match.MetaData.Proof.ContractData
  1535  	} else {
  1536  		coinID = []byte(match.MetaData.Proof.MakerRedeem)
  1537  		if match.Side != order.Maker {
  1538  			coinID = []byte(match.MetaData.Proof.TakerRedeem)
  1539  		}
  1540  		contractData = match.MetaData.Proof.CounterContract
  1541  	}
  1542  	secretHash := match.MetaData.Proof.SecretHash
  1543  	if len(coinID) == 0 {
  1544  		// If there is no coin ID yet and the match was revoked, assume
  1545  		// the transaction will never happen.
  1546  		if match.MetaData.Proof.IsRevoked() {
  1547  			stopChecks()
  1548  		}
  1549  		return
  1550  	}
  1551  	checkFees := feeChecker.DynamicSwapFeesPaid
  1552  	if !isInit {
  1553  		checkFees = feeChecker.DynamicRedemptionFeesPaid
  1554  	}
  1555  	actualFees, secrets, err := checkFees(ctx, coinID, contractData)
  1556  	if err != nil {
  1557  		if errors.Is(err, asset.CoinNotFoundError) || errors.Is(err, asset.ErrNotEnoughConfirms) {
  1558  			return
  1559  		}
  1560  		t.dc.log.Errorf("Failed to determine actual %s transaction fees paid for "+
  1561  			"match %s: %v", txType, match, err)
  1562  		return
  1563  	}
  1564  	// Only add the tx fee once.
  1565  	if !bytes.Equal(secrets[0], secretHash) {
  1566  		stopChecks()
  1567  		return
  1568  	}
  1569  	if isInit {
  1570  		t.metaData.SwapFeesPaid += actualFees
  1571  	} else {
  1572  		t.metaData.RedemptionFeesPaid += actualFees
  1573  	}
  1574  	stopChecks()
  1575  	t.notify(newOrderNote(TopicOrderStatusUpdate, "", "", db.Data, t.coreOrderInternal()))
  1576  }
  1577  
  1578  // isRedeemable will be true if the match is ready for our redemption to be
  1579  // broadcast.
  1580  //
  1581  // In certain situations, the match should be revoked and the return will
  1582  // indicate this. In particular, the situations are when the match is in
  1583  // TakerSwapCast on the Maker side and the taker's swap is found to be either
  1584  // spent or expired. Such matches are also not redeemable.
  1585  //
  1586  // This method accesses match fields and MUST be called with the trackedTrade
  1587  // mutex lock held for reads.
  1588  func (t *trackedTrade) isRedeemable(ctx context.Context, match *matchTracker) (ready, shouldRevoke bool) {
  1589  	// Quick status check before we bother with the wallet.
  1590  	switch match.Status {
  1591  	case order.NewlyMatched, order.MakerSwapCast:
  1592  		return false, false // all swaps not yet sent
  1593  	}
  1594  
  1595  	if match.swapErr != nil || len(match.MetaData.Proof.RefundCoin) != 0 {
  1596  		t.dc.log.Tracef("Match %s not redeemable: swapErr = %v, RefundCoin = %v",
  1597  			match, match.swapErr, match.MetaData.Proof.RefundCoin)
  1598  		return false, false
  1599  	}
  1600  	if ticksGoverned, _ := match.exceptions(); ticksGoverned {
  1601  		t.dc.log.Tracef("Match %s not redeemable: ticks metered", match)
  1602  		return false, false
  1603  	}
  1604  	// NOTE: Taker must be able to redeem when revoked!  As maker, only block
  1605  	// redeem if we have determined that the counterparty swap was either spent
  1606  	// or expired, as indicated by SelfRevoked. (maybe)
  1607  	//
  1608  	// if match.Side == order.Maker && match.MetaData.Proof.SelfRevoked {
  1609  	// 	t.dc.log.Debugf("Revoked match %s not redeemable as maker.", match)
  1610  	// 	return false, false
  1611  	// }
  1612  
  1613  	wallet := t.wallets.toWallet
  1614  	// Just a quick check here. We'll perform a more thorough check if there are
  1615  	// actually redeemables.
  1616  	if !wallet.locallyUnlocked() {
  1617  		t.dc.log.Errorf("not checking if order %s, match %s is redeemable because %s wallet is locked or disabled",
  1618  			t.ID(), match, unbip(wallet.AssetID))
  1619  		return false, false
  1620  	}
  1621  
  1622  	switch match.Status {
  1623  	case order.TakerSwapCast:
  1624  		if match.Side == order.Maker {
  1625  			// Check the confirmations on the taker's swap.
  1626  			confs, req, changed, spent, expired, err := t.counterPartyConfirms(ctx, match)
  1627  			if err != nil {
  1628  				if !errors.Is(err, asset.ErrSwapNotInitiated) {
  1629  					// We cannot get the swap data yet but there is no need
  1630  					// to log an error if swap not initiated as this is
  1631  					// expected for newly made swaps involving contracts.
  1632  					t.dc.log.Errorf("isRedeemable: %v", err)
  1633  				}
  1634  				return false, false
  1635  			}
  1636  			if spent {
  1637  				if match.MetaData.Proof.SelfRevoked {
  1638  					return false, false // already self-revoked
  1639  				}
  1640  				// Here we can check to see if this is a redeem we failed to record...
  1641  				t.dc.log.Warnf("Order %s, match %s counter-party's swap is spent before we could redeem", t.ID(), match)
  1642  				return false, true // REVOKE!
  1643  			}
  1644  			if expired {
  1645  				if match.MetaData.Proof.SelfRevoked {
  1646  					return false, false // already self-revoked
  1647  				}
  1648  				t.dc.log.Warnf("Order %s, match %s counter-party's swap expired before we could redeem", t.ID(), match)
  1649  				return false, true // REVOKE!
  1650  			}
  1651  			// NOTE: We'll redeem even if the market has vanished - taker will
  1652  			// find it. We'll keep trying to send the redeem request. If the
  1653  			// server/market never reappears, we should self-revoke and retire
  1654  			// after taker lock time has expired and server would have revoked.
  1655  			ready = confs >= req
  1656  			if changed && !ready {
  1657  				t.dc.log.Infof("Match %s not yet redeemable: current confs = %d, required confs = %d",
  1658  					match, confs, req)
  1659  			}
  1660  			return ready, false
  1661  		}
  1662  
  1663  		// If we're the taker, check the confirmations anyway so we can notify.
  1664  		confs, spent, err := t.wallets.fromWallet.swapConfirmations(ctx, match.MetaData.Proof.TakerSwap,
  1665  			match.MetaData.Proof.ContractData, match.MetaData.Stamp)
  1666  		if err != nil && !errors.Is(err, asset.ErrSwapNotInitiated) {
  1667  			// No need to log an error if swap not initiated as this
  1668  			// is expected for newly made swaps involving contracts.
  1669  			t.dc.log.Errorf("isRedeemable: error getting confirmation for our own swap transaction: %v", err)
  1670  		}
  1671  		if spent {
  1672  			t.dc.log.Debugf("Our (taker) swap for match %s is being reported as spent, "+
  1673  				"but we have not seen the counter-party's redemption yet. This could just"+
  1674  				" be network latency.", match)
  1675  			// Record this time if this is the first time we have observed that
  1676  			// it's spent.
  1677  			match.swapSpent()
  1678  		}
  1679  		match.setSwapConfirms(int64(confs))
  1680  		t.notify(newMatchNote(TopicConfirms, "", "", db.Data, t, match))
  1681  		return false, false
  1682  
  1683  	case order.MakerRedeemed:
  1684  		return match.Side == order.Taker, false
  1685  	}
  1686  
  1687  	return false, false
  1688  }
  1689  
  1690  // isRefundable will be true if all of the following are true:
  1691  //   - We have broadcasted a swap contract (matchProof.ContractData != nil).
  1692  //   - Neither party has redeemed (matchStatus < order.MakerRedeemed).
  1693  //     For Maker, this means we've not redeemed. For Taker, this means we've
  1694  //     not been notified of / we haven't yet found the Maker's redeem.
  1695  //   - Our swap's locktime has expired.
  1696  //
  1697  // Those checks are skipped and isRefundable is false if we've already
  1698  // executed a refund or our refund-to wallet is locked.
  1699  //
  1700  // This method modifies match fields and MUST be called with the trackedTrade
  1701  // mutex lock held for reads.
  1702  func (t *trackedTrade) isRefundable(ctx context.Context, match *matchTracker) bool {
  1703  	if match.refundErr != nil || len(match.MetaData.Proof.RefundCoin) != 0 {
  1704  		t.dc.log.Tracef("Match %s not refundable: refundErr = %v, RefundCoin = %v",
  1705  			match, match.refundErr, match.MetaData.Proof.RefundCoin)
  1706  		return false
  1707  	}
  1708  
  1709  	wallet := t.wallets.fromWallet
  1710  	// Just a quick check here. We'll perform a more thorough check if there are
  1711  	// actually refundables.
  1712  	if !wallet.locallyUnlocked() {
  1713  		t.dc.log.Errorf("not checking if order %s, match %s is refundable because %s wallet is locked or disabled",
  1714  			t.ID(), match, unbip(wallet.AssetID))
  1715  		return false
  1716  	}
  1717  
  1718  	// Return if we've NOT sent a swap OR a redeem has been
  1719  	// executed by either party.
  1720  	if len(match.MetaData.Proof.ContractData) == 0 || match.Status >= order.MakerRedeemed {
  1721  		return false
  1722  	}
  1723  
  1724  	// Issue a refund if our swap's locktime has expired.
  1725  	swapLocktimeExpired, contractExpiry, err := wallet.ContractLockTimeExpired(ctx, match.MetaData.Proof.ContractData)
  1726  	if err != nil {
  1727  		if !errors.Is(err, asset.ErrSwapNotInitiated) {
  1728  			// No need to log an error as this is expected for newly
  1729  			// made swaps involving contracts.
  1730  			t.dc.log.Errorf("error checking if locktime has expired for %s contract on order %s, match %s: %v",
  1731  				match.Side, t.ID(), match, err)
  1732  		}
  1733  		return false
  1734  	}
  1735  
  1736  	if swapLocktimeExpired {
  1737  		return true
  1738  	}
  1739  
  1740  	// Log contract expiry info on intervals: hourly when not expired, otherwise
  1741  	// every 5 minutes until the refund occurs.
  1742  	expiresIn := time.Until(contractExpiry) // may be negative
  1743  	logInterval := time.Hour
  1744  	if expiresIn <= 0 {
  1745  		logInterval = 5 * time.Minute
  1746  	}
  1747  	if !match.setExpireDur(expiresIn, logInterval) {
  1748  		return false // too recently logged
  1749  	}
  1750  
  1751  	swapCoinID := match.MetaData.Proof.TakerSwap
  1752  	if match.Side == order.Maker {
  1753  		swapCoinID = match.MetaData.Proof.MakerSwap
  1754  	}
  1755  	symbol, assetID := t.wallets.fromWallet.Symbol, t.wallets.fromWallet.AssetID
  1756  	remainingTime := expiresIn.Round(time.Second)
  1757  	assetSymb := strings.ToUpper(symbol)
  1758  	var expireDetails string
  1759  	if remainingTime > 0 {
  1760  		expireDetails = fmt.Sprintf("expires at %v (%v).", contractExpiry, remainingTime)
  1761  	} else {
  1762  		expireDetails = fmt.Sprintf("expired %v ago, but additional blocks are required by the %s network.",
  1763  			-remainingTime, assetSymb)
  1764  	}
  1765  	t.dc.log.Infof("Contract for match %s with swap coin %v (%s) %s",
  1766  		match, coinIDString(assetID, swapCoinID), assetSymb, expireDetails)
  1767  
  1768  	return false
  1769  }
  1770  
  1771  // shouldBeginFindRedemption will be true if we are the Taker on this match,
  1772  // we've broadcasted a swap, our swap has gotten the required confs, we've not
  1773  // refunded our swap, and either the match was revoked (without receiving a
  1774  // valid notification of Maker's redeem) or the match is self-governed and it
  1775  // has been a while since our swap was spent. The revoked status is provided as
  1776  // in input since it may not be flagged as revoked in the MatchProof yet.
  1777  //
  1778  // This method accesses match fields and MUST be called with the trackedTrade
  1779  // mutex lock held for reads.
  1780  func (t *trackedTrade) shouldBeginFindRedemption(ctx context.Context, match *matchTracker, revoked bool) bool {
  1781  	proof := &match.MetaData.Proof // revoked flags may not be updated yet, so we use an input arg
  1782  	swapCoinID := proof.TakerSwap
  1783  	if match.Side != order.Taker || len(swapCoinID) == 0 || len(proof.MakerRedeem) > 0 || len(proof.RefundCoin) > 0 {
  1784  		// t.dc.log.Tracef(
  1785  		// 	"Not finding redemption for match %s: side = %s, swapErr = %v, TakerSwap = %v RefundCoin = %v",
  1786  		// 	match, match.Side, match.swapErr, proof.TakerSwap, proof.RefundCoin)
  1787  		return false
  1788  	}
  1789  	// We are taker and have published our contract, there is no known maker
  1790  	// redeem, and we have not refunded. We may want to search for a maker
  1791  	// redeem if this match is revoked or our swap has been spent some time ago.
  1792  	if match.cancelRedemptionSearch != nil { // already finding redemption
  1793  		return false
  1794  	}
  1795  
  1796  	confs, spent, err := t.wallets.fromWallet.swapConfirmations(ctx, swapCoinID, proof.ContractData, match.MetaData.Stamp)
  1797  	if err != nil {
  1798  		if !errors.Is(err, asset.ErrSwapNotInitiated) {
  1799  			// No need to log an error if swap not initiated as this
  1800  			// is expected for newly made swaps involving contracts.
  1801  			t.dc.log.Errorf("Failed to get confirmations of the taker's swap %s (%s) for match %s, order %v: %v",
  1802  				coinIDString(t.wallets.fromWallet.AssetID, swapCoinID), t.wallets.fromWallet.Symbol, match, t.UID(), err)
  1803  		}
  1804  		return false
  1805  	}
  1806  	if spent {
  1807  		match.swapSpent() // noted.
  1808  		// NOTE: spent may not be accurate for SPV wallet (false negative), so
  1809  		// this should not be a requirement. (specifically... 0-conf?)
  1810  		t.dc.log.Infof("Swap contract for match %s, order %s is spent. "+
  1811  			"Search for counterparty redemption may begin soon.", match, t.ID())
  1812  	}
  1813  	if revoked { // no delays if it's revoked
  1814  		return spent || confs >= t.metaData.FromSwapConf
  1815  	}
  1816  	// Even if not revoked, go find that redeem if it was spent a while ago.
  1817  	return spent && match.swapSpentAgo() > t.spentAgoThresh()
  1818  }
  1819  
  1820  // shouldConfirmRedemption will return true if a redemption transaction
  1821  // has been broadcast, but it has not yet been confirmed.
  1822  //
  1823  // This method accesses match fields and MUST be called with the trackedTrade
  1824  // mutex lock held for reads.
  1825  func shouldConfirmRedemption(match *matchTracker) bool {
  1826  	if match.Status == order.MatchConfirmed {
  1827  		return false
  1828  	}
  1829  
  1830  	if (match.Side == order.Maker && match.Status < order.MakerRedeemed) ||
  1831  		(match.Side == order.Taker && match.Status < order.MatchComplete) {
  1832  		return false
  1833  	}
  1834  
  1835  	if match.redemptionRejected {
  1836  		return false
  1837  	}
  1838  
  1839  	proof := &match.MetaData.Proof
  1840  	if match.Side == order.Maker {
  1841  		return len(proof.MakerRedeem) > 0
  1842  	}
  1843  	return len(proof.TakerRedeem) > 0
  1844  }
  1845  
  1846  // tick will check for and perform any match actions necessary.
  1847  func (c *Core) tick(t *trackedTrade) (assetMap, error) {
  1848  	assets := make(assetMap) // callers expect non-nil map even on error :(
  1849  
  1850  	tStart := time.Now()
  1851  	var tLock time.Duration
  1852  	defer func() {
  1853  		if eTime := time.Since(tStart); eTime > 500*time.Millisecond {
  1854  			c.log.Debugf("Slow tick: trade %v processed in %v, blocked for %v",
  1855  				t.ID(), eTime, tLock)
  1856  		}
  1857  	}()
  1858  
  1859  	// Another tick may be running for this trade. We have a mutex just for this
  1860  	// so we don't have to write-lock t.mtx.Lock, which would block many other
  1861  	// actions such as isActive. We can't just run concurrent checks since the
  1862  	// results may become inaccurate when/if the other goroutine begins acting,
  1863  	// and we MUST NOT take the same action twice.
  1864  	t.tickLock.Lock()
  1865  	defer t.tickLock.Unlock()
  1866  	tLock = time.Since(tStart)
  1867  
  1868  	var swaps, redeems, refunds, revokes, searches, redemptionConfirms,
  1869  		dynamicSwapFeeConfirms, dynamicRedemptionFeeConfirms []*matchTracker
  1870  	var sent, quoteSent, received, quoteReceived uint64
  1871  
  1872  	checkMatch := func(match *matchTracker) error { // only errors on context.DeadlineExceeded or context.Canceled
  1873  		side := match.Side
  1874  		if match.Status == order.MatchConfirmed {
  1875  			return nil
  1876  		}
  1877  		if match.Address == "" {
  1878  			return nil // a cancel order match
  1879  		}
  1880  		if !t.matchIsActive(match) {
  1881  			return nil // either refunded or revoked requiring no action on this side of the match
  1882  		}
  1883  
  1884  		// Inform shouldBeginFindRedemption without modifying the MatchProof.
  1885  		revoked := match.MetaData.Proof.IsRevoked()
  1886  
  1887  		// The trackedTrade mutex is locked, so we must not hang forever. Give
  1888  		// this a generous timeout because it may be necessary to retrieve full
  1889  		// blocks, and catch timeout/shutdown after each check. Individual
  1890  		// requests can have shorter timeouts of their own. This is cumulative.
  1891  		ctx, cancel := context.WithTimeout(c.ctx, 40*time.Second)
  1892  		defer cancel()
  1893  
  1894  		ok, revoke := t.isSwappable(ctx, match) // rejects revoked matches
  1895  		if ok {
  1896  			c.log.Debugf("Swappable match %s for order %v (%v)", match, t.ID(), side)
  1897  			swaps = append(swaps, match)
  1898  			sent += match.Quantity
  1899  			quoteSent += calc.BaseToQuote(match.Rate, match.Quantity)
  1900  			return nil
  1901  		}
  1902  		if revoke {
  1903  			revokes = append(revokes, match) // may still need refund/redeem, continue
  1904  			revoked = true
  1905  		}
  1906  		if ctx.Err() != nil { // may be here because of timeout or shutdown
  1907  			return ctx.Err()
  1908  		}
  1909  
  1910  		if t.checkSwapFeeConfirms(match) {
  1911  			dynamicSwapFeeConfirms = append(dynamicSwapFeeConfirms, match)
  1912  		}
  1913  
  1914  		ok, revoke = t.isRedeemable(ctx, match) // does not reject revoked matches
  1915  		if ok {
  1916  			c.log.Debugf("Redeemable match %s for order %v (%v)", match, t.ID(), side)
  1917  			redeems = append(redeems, match)
  1918  			received += match.Quantity
  1919  			quoteReceived += calc.BaseToQuote(match.Rate, match.Quantity)
  1920  			return nil
  1921  		}
  1922  		if revoke {
  1923  			revokes = append(revokes, match) // may still need refund/redeem, continue
  1924  			revoked = true
  1925  		}
  1926  		if ctx.Err() != nil {
  1927  			return ctx.Err()
  1928  		}
  1929  
  1930  		if t.checkRedemptionFeeConfirms(match) {
  1931  			dynamicRedemptionFeeConfirms = append(dynamicRedemptionFeeConfirms, match)
  1932  		}
  1933  
  1934  		// Check refundability before checking if to start finding redemption.
  1935  		// Ensures that redemption search is not started if locktime has expired.
  1936  		// If we've already started redemption search for this match, the search
  1937  		// will be aborted if/when auto-refund succeeds.
  1938  		if t.isRefundable(ctx, match) { // does not matter if revoked
  1939  			c.log.Debugf("Refundable match %s for order %v (%v)", match, t.ID(), side)
  1940  			refunds = append(refunds, match)
  1941  			return nil
  1942  		}
  1943  		if ctx.Err() != nil {
  1944  			return ctx.Err()
  1945  		}
  1946  
  1947  		if t.shouldBeginFindRedemption(ctx, match, revoked /* consider new pending self-revoke */) {
  1948  			c.log.Debugf("Ready to find counter-party redemption for match %s, order %v (%v)", match, t.ID(), side)
  1949  			searches = append(searches, match)
  1950  			return nil
  1951  		}
  1952  
  1953  		if shouldConfirmRedemption(match) {
  1954  			redemptionConfirms = append(redemptionConfirms, match)
  1955  			return nil
  1956  		}
  1957  
  1958  		// For certain "self-governed" trades where the market or server has
  1959  		// vanished, we should revoke the match to allow it to retire without
  1960  		// having sent any pending redeem requests. Note that self-governed is
  1961  		// not necessarily a permanent state, so we delay this action.
  1962  		if !revoked && t.isSelfGoverned() && time.Since(match.matchTime()) > t.lockTimeTaker {
  1963  			c.log.Warnf("Revoking old self-governed match %v for market %v, host %v.",
  1964  				match, t.mktID, t.dc.acct.host)
  1965  			revokes = append(revokes, match)
  1966  			// NOTE: If the trade is in booked status, the order still won't
  1967  			// retire. We need a way to force-cancel such orders.
  1968  		}
  1969  
  1970  		return ctx.Err()
  1971  	}
  1972  
  1973  	c.loginMtx.Lock()
  1974  	loggedIn := c.loggedIn
  1975  	c.loginMtx.Unlock()
  1976  
  1977  	// Begin checks under read-only lock.
  1978  	t.mtx.RLock()
  1979  
  1980  	// Make sure we have a redemption fee suggestion cached.
  1981  	t.cacheRedemptionFeeSuggestion()
  1982  
  1983  	if !t.readyToTick {
  1984  		t.mtx.RUnlock()
  1985  		return assets, nil
  1986  	}
  1987  
  1988  	// Check all matches for and resend pending requests as necessary.
  1989  	// It's possible we're not logged in if we receive a tipChange
  1990  	// notification before we connect to dex servers.
  1991  	if loggedIn {
  1992  		c.resendPendingRequests(t)
  1993  	}
  1994  
  1995  	// Check all matches and then swap, redeem, or refund as necessary.
  1996  	var err error
  1997  	for _, match := range t.matches {
  1998  		if err = checkMatch(match); err != nil {
  1999  			break
  2000  		}
  2001  	}
  2002  
  2003  	rmCancel := t.hasStaleCancelOrder()
  2004  
  2005  	// End checks under read-only lock.
  2006  	t.mtx.RUnlock()
  2007  
  2008  	if err != nil {
  2009  		if len(revokes) != 0 {
  2010  			// Still flag any "should revoke"s for IsRevoked() and to fast track
  2011  			// the next tick. NOTE: See the TODO below regarding revokeMatch.
  2012  			t.mtx.Lock()
  2013  			defer t.mtx.Unlock()
  2014  			for _, rm := range revokes {
  2015  				rm.MetaData.Proof.SelfRevoked = true
  2016  			}
  2017  		}
  2018  		return assets, err
  2019  	}
  2020  
  2021  	if len(swaps) > 0 || len(refunds) > 0 {
  2022  		assets.count(t.wallets.fromWallet.AssetID)
  2023  	}
  2024  	if len(redeems) > 0 {
  2025  		assets.count(t.wallets.toWallet.AssetID)
  2026  		assets.count(t.wallets.fromWallet.AssetID) // update ContractLocked balance
  2027  	}
  2028  
  2029  	if !rmCancel && len(swaps) == 0 && len(refunds) == 0 && len(redeems) == 0 &&
  2030  		len(revokes) == 0 && len(searches) == 0 && len(redemptionConfirms) == 0 &&
  2031  		len(dynamicSwapFeeConfirms) == 0 && len(dynamicRedemptionFeeConfirms) == 0 {
  2032  		return assets, nil // nothing to do, don't acquire the write-lock
  2033  	}
  2034  
  2035  	// Wallet requests below may still hang if there are no internal timeouts.
  2036  	// We should consider giving each asset.Wallet method a context arg.
  2037  	// However, if the requests in the checks above just succeeded, the wallets
  2038  	// are likely to be responsive below.
  2039  
  2040  	// Take the actions that will modify the match.
  2041  	errs := newErrorSet("%s tick: ", t.dc.acct.host)
  2042  	t.mtx.Lock()
  2043  	defer t.mtx.Unlock()
  2044  
  2045  	if rmCancel {
  2046  		t.deleteStaleCancelOrder()
  2047  	}
  2048  
  2049  	for _, match := range revokes {
  2050  		match.MetaData.Proof.SelfRevoked = true
  2051  		// TODO: maybe revokeMatch() instead of just setting the flag? If this
  2052  		// match is in refunds or redeems (or a redemption search is running),
  2053  		// the match will still be updated after those actions are taken.
  2054  		// Otherwise, we'll be waiting for a revokeMatch call from either
  2055  		// handleRevokeMatchMsg or resolveMatchConflicts (on reconnect).
  2056  	}
  2057  
  2058  	if len(swaps) > 0 {
  2059  		didUnlock, err := t.wallets.fromWallet.refreshUnlock()
  2060  		if err != nil { // Just log it and try anyway.
  2061  			c.log.Errorf("refreshUnlock error swapping %s: %v", t.wallets.fromWallet.Symbol, err)
  2062  		}
  2063  		if didUnlock {
  2064  			c.log.Infof("Unexpected unlock needed for the %s wallet to send a swap", t.wallets.fromWallet.Symbol)
  2065  		}
  2066  		qty := sent
  2067  		if !t.Trade().Sell {
  2068  			qty = quoteSent
  2069  		}
  2070  		err = c.swapMatches(t, swaps)
  2071  		corder := t.coreOrderInternal() // after swapMatches modifies matches
  2072  		ui := t.wallets.fromWallet.Info().UnitInfo
  2073  		if err != nil {
  2074  			errs.addErr(err)
  2075  			subject, details := c.formatDetails(TopicSwapSendError, ui.ConventionalString(qty), ui.Conventional.Unit, makeOrderToken(t.token()))
  2076  			t.notify(newOrderNote(TopicSwapSendError, subject, details, db.ErrorLevel, corder))
  2077  		} else {
  2078  			subject, details := c.formatDetails(TopicSwapsInitiated, ui.ConventionalString(qty), ui.Conventional.Unit, makeOrderToken(t.token()))
  2079  			t.notify(newOrderNote(TopicSwapsInitiated, subject, details, db.Poke, corder))
  2080  		}
  2081  	}
  2082  
  2083  	if len(redeems) > 0 {
  2084  		didUnlock, err := t.wallets.toWallet.refreshUnlock()
  2085  		if err != nil { // Just log it and try anyway.
  2086  			c.log.Errorf("refreshUnlock error redeeming %s: %v", t.wallets.toWallet.Symbol, err)
  2087  		}
  2088  		if didUnlock {
  2089  			c.log.Infof("Unexpected unlock needed for the %s wallet to send a redemption", t.wallets.toWallet.Symbol)
  2090  		}
  2091  		qty := received
  2092  		if t.Trade().Sell {
  2093  			qty = quoteReceived
  2094  		}
  2095  		err = c.redeemMatches(t, redeems)
  2096  		corder := t.coreOrderInternal()
  2097  		ui := t.wallets.toWallet.Info().UnitInfo
  2098  		if err != nil {
  2099  			errs.addErr(err)
  2100  			subject, details := c.formatDetails(TopicRedemptionError,
  2101  				ui.ConventionalString(qty), ui.Conventional.Unit, makeOrderToken(t.token()))
  2102  			t.notify(newOrderNote(TopicRedemptionError, subject, details, db.ErrorLevel, corder))
  2103  		} else {
  2104  			subject, details := c.formatDetails(TopicMatchComplete,
  2105  				ui.ConventionalString(qty), ui.Conventional.Unit, makeOrderToken(t.token()))
  2106  			t.notify(newOrderNote(TopicMatchComplete, subject, details, db.Poke, corder))
  2107  		}
  2108  	}
  2109  
  2110  	if len(refunds) > 0 {
  2111  		didUnlock, err := t.wallets.fromWallet.refreshUnlock()
  2112  		if err != nil { // Just log it and try anyway.
  2113  			c.log.Errorf("refreshUnlock error refunding %s: %v", t.wallets.fromWallet.Symbol, err)
  2114  		}
  2115  		if didUnlock {
  2116  			c.log.Infof("Unexpected unlock needed for the %s wallet while sending a refund", t.wallets.fromWallet.Symbol)
  2117  		}
  2118  		refunded, err := c.refundMatches(t, refunds)
  2119  		corder := t.coreOrderInternal()
  2120  		ui := t.wallets.fromWallet.Info().UnitInfo
  2121  		if err != nil {
  2122  			errs.addErr(err)
  2123  			subject, details := c.formatDetails(TopicRefundFailure,
  2124  				ui.ConventionalString(refunded), ui.Conventional.Unit, makeOrderToken(t.token()))
  2125  			t.notify(newOrderNote(TopicRefundFailure, subject, details, db.ErrorLevel, corder))
  2126  		} else {
  2127  			subject, details := c.formatDetails(TopicMatchesRefunded,
  2128  				ui.ConventionalString(refunded), ui.Conventional.Unit, makeOrderToken(t.token()))
  2129  			t.notify(newOrderNote(TopicMatchesRefunded, subject, details, db.WarningLevel, corder))
  2130  		}
  2131  	}
  2132  
  2133  	if len(searches) > 0 {
  2134  		for _, match := range searches {
  2135  			t.findMakersRedemption(c.ctx, match) // async search, just set cancelRedemptionSearch
  2136  		}
  2137  	}
  2138  
  2139  	if len(redemptionConfirms) > 0 {
  2140  		c.confirmRedemptions(t, redemptionConfirms)
  2141  	}
  2142  
  2143  	for _, match := range dynamicSwapFeeConfirms {
  2144  		t.updateDynamicSwapOrRedemptionFeesPaid(c.ctx, match, true)
  2145  	}
  2146  
  2147  	for _, match := range dynamicRedemptionFeeConfirms {
  2148  		t.updateDynamicSwapOrRedemptionFeesPaid(c.ctx, match, false)
  2149  	}
  2150  
  2151  	return assets, errs.ifAny()
  2152  }
  2153  
  2154  // resendPendingRequests checks all matches for this order to re-attempt
  2155  // sending the `init` or `redeem` request where necessary.
  2156  //
  2157  // This method modifies match fields and MUST be called with the trackedTrade
  2158  // mutex lock held for reads.
  2159  func (c *Core) resendPendingRequests(t *trackedTrade) {
  2160  	if t.isSelfGoverned() {
  2161  		return
  2162  	}
  2163  
  2164  	for _, match := range t.matches {
  2165  		proof, auth := &match.MetaData.Proof, &match.MetaData.Proof.Auth
  2166  		// Do not resend pending requests for revoked matches.
  2167  		// Matches where we've refunded our swap or we auto-redeemed maker's
  2168  		// swap will be set to revoked and will be skipped as well.
  2169  		if match.swapErr != nil || proof.IsRevoked() {
  2170  			continue
  2171  		}
  2172  		side, status := match.Side, match.Status
  2173  		var swapCoinID, redeemCoinID []byte
  2174  		switch {
  2175  		case side == order.Maker && status == order.MakerSwapCast:
  2176  			swapCoinID = proof.MakerSwap
  2177  		case side == order.Taker && status == order.TakerSwapCast:
  2178  			swapCoinID = proof.TakerSwap
  2179  		case side == order.Maker && status >= order.MakerRedeemed:
  2180  			redeemCoinID = proof.MakerRedeem
  2181  		case side == order.Taker && status >= order.MatchComplete:
  2182  			redeemCoinID = proof.TakerRedeem
  2183  		}
  2184  		if len(swapCoinID) != 0 && len(auth.InitSig) == 0 { // resend pending `init` request
  2185  			c.sendInitAsync(t, match, swapCoinID, proof.ContractData)
  2186  		} else if len(redeemCoinID) != 0 && len(auth.RedeemSig) == 0 { // resend pending `redeem` request
  2187  			c.sendRedeemAsync(t, match, redeemCoinID, proof.Secret)
  2188  		}
  2189  	}
  2190  }
  2191  
  2192  // revoke sets the trade status to Revoked, either because the market is
  2193  // suspended with persist=false or because the order is revoked and unbooked
  2194  // by the server.
  2195  // Funding coins or change coin will be returned IF there are no matches that
  2196  // MAY later require sending swaps.
  2197  func (t *trackedTrade) revoke() {
  2198  	t.mtx.Lock()
  2199  	defer t.mtx.Unlock()
  2200  
  2201  	if t.metaData.Status >= order.OrderStatusExecuted {
  2202  		// Executed, canceled or already revoked orders cannot be (re)revoked.
  2203  		t.dc.log.Errorf("revoke() wrongly called for order %v, status %s", t.ID(), t.metaData.Status)
  2204  		return
  2205  	}
  2206  
  2207  	t.dc.log.Warnf("Revoking order %v", t.ID())
  2208  
  2209  	metaOrder := t.metaOrder()
  2210  	metaOrder.MetaData.Status = order.OrderStatusRevoked
  2211  	err := t.db.UpdateOrder(metaOrder)
  2212  	if err != nil {
  2213  		t.dc.log.Errorf("unable to update order: %v", err)
  2214  	}
  2215  
  2216  	// Return coins if there are no matches that MAY later require sending swaps.
  2217  	t.maybeReturnCoins()
  2218  
  2219  	if t.isMarketBuy() { // Is this even possible?
  2220  		t.unlockRedemptionFraction(1, 1)
  2221  		t.unlockRefundFraction(1, 1)
  2222  	} else {
  2223  		t.unlockRedemptionFraction(t.Trade().Remaining(), t.Trade().Quantity)
  2224  		t.unlockRefundFraction(t.Trade().Remaining(), t.Trade().Quantity)
  2225  	}
  2226  }
  2227  
  2228  // revokeMatch sets the status as revoked for the specified match, emits an
  2229  // Order note with TopicMatchRevoked, returns any unneeded funding coins, and
  2230  // unlocks and reserves for refunds and redeems (for AccountLocker wallet
  2231  // types like eth). revokeMatch must be called with the mtx write-locked.
  2232  func (t *trackedTrade) revokeMatch(matchID order.MatchID, fromServer bool) error {
  2233  	var revokedMatch *matchTracker
  2234  	for _, match := range t.matches {
  2235  		if match.MatchID == matchID {
  2236  			revokedMatch = match
  2237  			break
  2238  		}
  2239  	}
  2240  	if revokedMatch == nil {
  2241  		return fmt.Errorf("no match found with id %s for order %v", matchID, t.ID())
  2242  	}
  2243  
  2244  	// Set the match as revoked.
  2245  	if fromServer {
  2246  		revokedMatch.MetaData.Proof.ServerRevoked = true
  2247  	} else {
  2248  		revokedMatch.MetaData.Proof.SelfRevoked = true
  2249  	}
  2250  	err := t.db.UpdateMatch(&revokedMatch.MetaMatch)
  2251  	if err != nil {
  2252  		t.dc.log.Errorf("db update error for revoked match %v, order %v: %v", matchID, t.ID(), err)
  2253  	}
  2254  
  2255  	// Notify the user of the failed match.
  2256  	corder := t.coreOrderInternal() // no cancel order
  2257  	subject, details := t.formatDetails(TopicMatchRevoked, token(matchID[:]))
  2258  	t.notify(newOrderNote(TopicMatchRevoked, subject, details, db.WarningLevel, corder))
  2259  
  2260  	// Unlock coins if we're not expecting future matches for this trade and
  2261  	// there are no matches that MAY later require sending swaps.
  2262  	t.maybeReturnCoins()
  2263  
  2264  	// Return unused and unneeded redemption reserves.
  2265  	if (revokedMatch.Side == order.Taker && (revokedMatch.Status < order.TakerSwapCast)) ||
  2266  		(revokedMatch.Side == order.Maker && revokedMatch.Status < order.MakerSwapCast) {
  2267  
  2268  		if t.isMarketBuy() {
  2269  			t.unlockRedemptionFraction(1, uint64(len(t.matches)))
  2270  			t.unlockRefundFraction(1, uint64(len(t.matches)))
  2271  		} else {
  2272  			t.unlockRedemptionFraction(revokedMatch.Quantity, t.Trade().Quantity)
  2273  			t.unlockRefundFraction(revokedMatch.Quantity, t.Trade().Quantity)
  2274  		}
  2275  	}
  2276  
  2277  	t.dc.log.Warnf("Match %v revoked in status %v for order %v", matchID, revokedMatch.Status, t.ID())
  2278  	return nil
  2279  }
  2280  
  2281  // swapMatches will send a transaction with swaps for the specified matches.
  2282  // The matches will be de-grouped so that matches marked as suspect are swapped
  2283  // individually and separate from the non-suspect group.
  2284  //
  2285  // This method modifies match fields and MUST be called with the trackedTrade
  2286  // mutex lock held for writes.
  2287  func (c *Core) swapMatches(t *trackedTrade, matches []*matchTracker) error {
  2288  	errs := newErrorSet("swapMatches order %s - ", t.ID())
  2289  	groupables := make([]*matchTracker, 0, len(matches)) // Over-allocating if there are suspect matches
  2290  	var suspects []*matchTracker
  2291  	for _, m := range matches {
  2292  		if m.suspectSwap {
  2293  			suspects = append(suspects, m)
  2294  		} else {
  2295  			groupables = append(groupables, m)
  2296  		}
  2297  	}
  2298  	if len(groupables) > 0 {
  2299  		maxSwapsInTx := int(t.wallets.fromWallet.Info().MaxSwapsInTx)
  2300  		if maxSwapsInTx <= 0 || len(groupables) < maxSwapsInTx {
  2301  			c.swapMatchGroup(t, groupables, errs)
  2302  		} else {
  2303  			for i := 0; i < len(groupables); i += maxSwapsInTx {
  2304  				if i+maxSwapsInTx < len(groupables) {
  2305  					c.swapMatchGroup(t, groupables[i:i+maxSwapsInTx], errs)
  2306  				} else {
  2307  					c.swapMatchGroup(t, groupables[i:], errs)
  2308  				}
  2309  			}
  2310  		}
  2311  	}
  2312  	for _, m := range suspects {
  2313  		c.swapMatchGroup(t, []*matchTracker{m}, errs)
  2314  	}
  2315  	return errs.ifAny()
  2316  }
  2317  
  2318  // swapMatchGroup will send a transaction with swap outputs for the specified
  2319  // matches.
  2320  //
  2321  // This method modifies match fields and MUST be called with the trackedTrade
  2322  // mutex lock held for writes.
  2323  func (c *Core) swapMatchGroup(t *trackedTrade, matches []*matchTracker, errs *errorSet) {
  2324  	// Prepare the asset.Contracts.
  2325  	contracts := make([]*asset.Contract, len(matches))
  2326  	// These matches may have different fee rates, matched in different epochs.
  2327  	var highestFeeRate uint64
  2328  	for i, match := range matches {
  2329  		value := match.Quantity
  2330  		if !match.trade.Sell {
  2331  			value = calc.BaseToQuote(match.Rate, match.Quantity)
  2332  		}
  2333  		matchTime := match.matchTime()
  2334  		lockTime := matchTime.Add(t.lockTimeTaker).UTC().Unix()
  2335  		if match.Side == order.Maker {
  2336  			match.MetaData.Proof.Secret = encode.RandomBytes(32)
  2337  			secretHash := sha256.Sum256(match.MetaData.Proof.Secret)
  2338  			match.MetaData.Proof.SecretHash = secretHash[:]
  2339  			lockTime = matchTime.Add(t.lockTimeMaker).UTC().Unix()
  2340  		}
  2341  
  2342  		contracts[i] = &asset.Contract{
  2343  			Address:    match.Address,
  2344  			Value:      value,
  2345  			SecretHash: match.MetaData.Proof.SecretHash,
  2346  			LockTime:   uint64(lockTime),
  2347  		}
  2348  
  2349  		if match.FeeRateSwap > highestFeeRate {
  2350  			highestFeeRate = match.FeeRateSwap
  2351  		}
  2352  	}
  2353  
  2354  	lockChange := true
  2355  	// If the order is executed, canceled or revoked, and these are the last
  2356  	// swaps, then we don't need to lock the change coin.
  2357  	if t.metaData.Status > order.OrderStatusBooked {
  2358  		var matchesRequiringSwaps int
  2359  		for _, match := range t.matches {
  2360  			if match.MetaData.Proof.IsRevoked() {
  2361  				// Revoked matches don't require swaps.
  2362  				continue
  2363  			}
  2364  			if (match.Side == order.Maker && match.Status < order.MakerSwapCast) ||
  2365  				(match.Side == order.Taker && match.Status < order.TakerSwapCast) {
  2366  				matchesRequiringSwaps++
  2367  			}
  2368  		}
  2369  		if len(matches) == matchesRequiringSwaps { // last swaps
  2370  			lockChange = false
  2371  		}
  2372  	}
  2373  
  2374  	// Fund the swap. If this isn't the first swap, use the change coin from the
  2375  	// previous swaps.
  2376  	fromWallet := t.wallets.fromWallet
  2377  	coinIDs := t.Trade().Coins
  2378  	if len(t.metaData.ChangeCoin) > 0 {
  2379  		coinIDs = []order.CoinID{t.metaData.ChangeCoin}
  2380  		c.log.Debugf("Using stored change coin %v (%v) for order %v matches",
  2381  			coinIDString(fromWallet.AssetID, coinIDs[0]), fromWallet.Symbol, t.ID())
  2382  	}
  2383  
  2384  	inputs := make([]asset.Coin, len(coinIDs))
  2385  	for i, coinID := range coinIDs {
  2386  		coin, found := t.coins[hex.EncodeToString(coinID)]
  2387  		if !found {
  2388  			errs.add("%s coin %s not found", fromWallet.Symbol, coinIDString(fromWallet.AssetID, coinID))
  2389  			return
  2390  		}
  2391  		inputs[i] = coin
  2392  	}
  2393  
  2394  	if t.dc.IsDown() {
  2395  		errs.add("not broadcasting swap while DEX %s connection is down (could be revoked)", t.dc.acct.host)
  2396  		return
  2397  	}
  2398  
  2399  	// Use a higher swap fee rate if a local estimate is higher than the
  2400  	// prescribed rate, but not higher than the funded (max) rate.
  2401  	if highestFeeRate < t.metaData.MaxFeeRate {
  2402  		freshRate := fromWallet.feeRate()
  2403  		if freshRate == 0 { // either not a FeeRater, or FeeRate failed
  2404  			freshRate = t.dc.bestBookFeeSuggestion(fromWallet.AssetID)
  2405  		}
  2406  		if freshRate > t.metaData.MaxFeeRate {
  2407  			freshRate = t.metaData.MaxFeeRate
  2408  		}
  2409  		if highestFeeRate < freshRate {
  2410  			c.log.Infof("Prescribed %v fee rate %v looks low, using %v",
  2411  				fromWallet.Symbol, highestFeeRate, freshRate)
  2412  			highestFeeRate = freshRate
  2413  		}
  2414  	}
  2415  
  2416  	// Ensure swap is not sent with a zero fee rate.
  2417  	if highestFeeRate == 0 {
  2418  		errs.add("swap cannot proceed with a zero fee rate")
  2419  		return
  2420  	}
  2421  
  2422  	// swapMatches is no longer idempotent after this point.
  2423  
  2424  	// Send the swap. If the swap fails, set the swapErr flag for all matches.
  2425  	// A more sophisticated solution might involve tracking the error time too
  2426  	// and trying again in certain circumstances.
  2427  	swaps := &asset.Swaps{
  2428  		Version:    t.metaData.FromVersion,
  2429  		Inputs:     inputs,
  2430  		Contracts:  contracts,
  2431  		FeeRate:    highestFeeRate,
  2432  		LockChange: lockChange,
  2433  		Options:    t.options,
  2434  	}
  2435  	receipts, change, fees, err := fromWallet.Swap(swaps)
  2436  	if err != nil {
  2437  		bTimeout, tickInterval := t.broadcastTimeout(), t.dc.ticker.Dur() // bTimeout / tickCheckInterval
  2438  		for _, match := range matches {
  2439  			// Mark the matches as suspect to prevent them being grouped again.
  2440  			match.suspectSwap = true
  2441  			match.swapErrCount++
  2442  			// If we can still swap before the broadcast timeout, allow retries
  2443  			// soon.
  2444  			auditStamp := match.MetaData.Proof.Auth.AuditStamp
  2445  			lastActionTime := match.matchTime()
  2446  			if match.Side == order.Taker {
  2447  				// It is possible that AuditStamp could be zero if we're
  2448  				// recovering during startup or after a DEX reconnect. In that
  2449  				// case, allow three retries before giving up.
  2450  				lastActionTime = time.UnixMilli(int64(auditStamp))
  2451  			}
  2452  			if time.Since(lastActionTime) < bTimeout ||
  2453  				(auditStamp == 0 && match.swapErrCount < tickCheckDivisions) {
  2454  				match.delayTicks(tickInterval * 3 / 4)
  2455  			} else {
  2456  				// If we can't get a swap out before the broadcast timeout, just
  2457  				// quit. We could also self-revoke here, but we're also
  2458  				// expecting a revocation from the server, so relying on that
  2459  				// one for now.
  2460  				match.swapErr = err
  2461  			}
  2462  		}
  2463  		errs.add("error sending %s swap transaction: %v", fromWallet.Symbol, err)
  2464  		return
  2465  	}
  2466  
  2467  	refundTxs := ""
  2468  	for i, r := range receipts {
  2469  		rawRefund := r.SignedRefund()
  2470  		if len(rawRefund) == 0 { // e.g. eth
  2471  			continue // in case others are not empty for some reason
  2472  		}
  2473  		refundTxs = fmt.Sprintf("%s%q: %s", refundTxs, r.Coin(), rawRefund)
  2474  		if i != len(receipts)-1 {
  2475  			refundTxs = fmt.Sprintf("%s, ", refundTxs)
  2476  		}
  2477  	}
  2478  
  2479  	// Log the swap receipts. It is important to print the receipts as a
  2480  	// Stringer to provide important data, such as the secret hash and contract
  2481  	// address with ETH since it allows manually refunding.
  2482  	c.log.Infof("Broadcasted transaction with %d swap contracts for order %v. "+
  2483  		"Assigned fee rate = %d. Receipts (%s): %v.",
  2484  		len(receipts), t.ID(), swaps.FeeRate, fromWallet.Symbol, receipts)
  2485  	if refundTxs != "" {
  2486  		c.log.Infof("The following are contract identifiers mapped to raw refund "+
  2487  			"transactions that are only valid after the swap contract expires. "+
  2488  			"These are fallback transactions that can be used to return funds "+
  2489  			"to your wallet in the case the wallet software no longer functions. They should "+
  2490  			"NOT be used if Bison Wallet is operable. The wallet will refund failed "+
  2491  			"contracts automatically.\nRefund Txs: {%s}", refundTxs)
  2492  	}
  2493  
  2494  	// If this is the first swap (and even if not), the funding coins
  2495  	// would have been spent and unlocked.
  2496  	t.coinsLocked = false
  2497  	t.changeLocked = lockChange
  2498  	if _, dynamic := fromWallet.Wallet.(asset.DynamicSwapper); !dynamic {
  2499  		t.metaData.SwapFeesPaid += fees // dynamic tx wallets don't know the fees paid until mining
  2500  	}
  2501  
  2502  	if change == nil {
  2503  		t.metaData.ChangeCoin = nil
  2504  	} else {
  2505  		cid := change.ID()
  2506  		if rc, is := change.(asset.RecoveryCoin); is {
  2507  			cid = rc.RecoveryID()
  2508  		}
  2509  		t.coins[cid.String()] = change
  2510  		t.metaData.ChangeCoin = []byte(cid)
  2511  		c.log.Debugf("Saving change coin %v (%v) to DB for order %v",
  2512  			coinIDString(fromWallet.AssetID, t.metaData.ChangeCoin), fromWallet.Symbol, t.ID())
  2513  	}
  2514  	t.change = change
  2515  	err = t.db.UpdateOrderMetaData(t.ID(), t.metaData)
  2516  	if err != nil {
  2517  		c.log.Errorf("Error updating order metadata for order %s: %v", t.ID(), err)
  2518  	}
  2519  
  2520  	// Process the swap for each match by updating the match with swap
  2521  	// details and sending the `init` request to the DEX.
  2522  	// Saving the swap details now makes it possible to resend the `init`
  2523  	// request at a later time if sending it now fails OR to refund the
  2524  	// swap after locktime expires if the trade does not progress as expected.
  2525  	for i, receipt := range receipts {
  2526  		match := matches[i]
  2527  		coin := receipt.Coin()
  2528  		c.log.Infof("Contract coin %v (%s), value = %d, refundable at %v (receipt = %v), match = %v",
  2529  			coin, fromWallet.Symbol, coin.Value(), receipt.Expiration(), receipt.String(), match)
  2530  		if secret := match.MetaData.Proof.Secret; len(secret) > 0 {
  2531  			c.log.Tracef("Contract coin %v secret = %x", coin, secret)
  2532  		}
  2533  
  2534  		// Update the match db data with the swap details before attempting
  2535  		// to notify the server of the swap.
  2536  		proof := &match.MetaData.Proof
  2537  		contract, coinID := receipt.Contract(), []byte(coin.ID())
  2538  		// NOTE: receipt.Contract() uniquely identifies this swap. Only the
  2539  		// asset backend can decode this information, which may be a redeem
  2540  		// script with UTXO assets, or a secret hash + contract version for
  2541  		// contracts on account-based assets.
  2542  		proof.ContractData = contract
  2543  		if match.Side == order.Taker {
  2544  			proof.TakerSwap = coinID
  2545  			match.Status = order.TakerSwapCast
  2546  		} else {
  2547  			proof.MakerSwap = coinID
  2548  			match.Status = order.MakerSwapCast
  2549  		}
  2550  
  2551  		if err := t.db.UpdateMatch(&match.MetaMatch); err != nil {
  2552  			errs.add("error storing swap details in database for match %s, coin %s: %v",
  2553  				match, coinIDString(fromWallet.AssetID, coinID), err)
  2554  		}
  2555  
  2556  		c.sendInitAsync(t, match, coin.ID(), contract)
  2557  	}
  2558  }
  2559  
  2560  // sendInitAsync starts a goroutine to send an `init` request for the specified
  2561  // match and save the server's ack sig to db. Sends a notification if an error
  2562  // occurs while sending the request or validating the server's response.
  2563  func (c *Core) sendInitAsync(t *trackedTrade, match *matchTracker, coinID, contract []byte) {
  2564  	if !atomic.CompareAndSwapUint32(&match.sendingInitAsync, 0, 1) {
  2565  		return
  2566  	}
  2567  
  2568  	c.log.Debugf("Notifying DEX %s of our %s swap contract %v for match %s",
  2569  		t.dc.acct.host, t.wallets.fromWallet.Symbol, coinIDString(t.wallets.fromWallet.AssetID, coinID), match)
  2570  
  2571  	// Send the init request asynchronously.
  2572  	c.wg.Add(1) // So Core does not shut down until we're done with this request.
  2573  	go func() {
  2574  		defer c.wg.Done() // bottom of the stack
  2575  		var err error
  2576  		defer func() {
  2577  			atomic.StoreUint32(&match.sendingInitAsync, 0)
  2578  			if err != nil {
  2579  				corder := t.coreOrder()
  2580  				subject, details := c.formatDetails(TopicInitError, match, err)
  2581  				t.notify(newOrderNote(TopicInitError, subject, details, db.ErrorLevel, corder))
  2582  			}
  2583  		}()
  2584  
  2585  		ack := new(msgjson.Acknowledgement)
  2586  		init := &msgjson.Init{
  2587  			OrderID:  t.ID().Bytes(),
  2588  			MatchID:  match.MatchID[:],
  2589  			CoinID:   coinID,
  2590  			Contract: contract,
  2591  		}
  2592  		// The DEX may wait up to its configured broadcast timeout, but we will
  2593  		// retry on timeout or other error.
  2594  		timeout := t.broadcastTimeout() / 4
  2595  		if timeout < time.Minute { // sane minimum, or if we lack server config for any reason
  2596  			// Send would fail right away anyway if the server is really down,
  2597  			// but at least attempt it with a non-zero timeout.
  2598  			timeout = time.Minute
  2599  		}
  2600  		err = t.dc.signAndRequest(init, msgjson.InitRoute, ack, timeout)
  2601  		if err != nil {
  2602  			var msgErr *msgjson.Error
  2603  			if errors.As(err, &msgErr) {
  2604  				if msgErr.Code == msgjson.SettlementSequenceError {
  2605  					c.log.Errorf("Starting match status resolution for 'init' request SettlementSequenceError")
  2606  					c.resolveMatchConflicts(t.dc, map[order.OrderID]*matchStatusConflict{
  2607  						t.ID(): {
  2608  							trade:   t,
  2609  							matches: []*matchTracker{match},
  2610  						},
  2611  					})
  2612  				} else if msgErr.Code == msgjson.RPCUnknownMatch {
  2613  					t.mtx.Lock()
  2614  					oid := t.ID()
  2615  					c.log.Warnf("DEX %s did not report active match %s on order %s - assuming revoked, status %v.",
  2616  						t.dc.acct.host, match, oid, match.Status)
  2617  					// We must have missed the revoke notification. Flag to allow recovery
  2618  					// and subsequent retirement of the match and parent trade.
  2619  					match.MetaData.Proof.SelfRevoked = true
  2620  					if err := c.db.UpdateMatch(&match.MetaMatch); err != nil {
  2621  						c.log.Errorf("Failed to update missing/revoked match: %v", err)
  2622  					}
  2623  					t.mtx.Unlock()
  2624  					numMissing := 1
  2625  					subject, details := c.formatDetails(TopicMissingMatches,
  2626  						numMissing, makeOrderToken(t.token()), t.dc.acct.host)
  2627  					c.notify(newOrderNote(TopicMissingMatches, subject, details, db.ErrorLevel, t.coreOrderInternal()))
  2628  				}
  2629  			}
  2630  			err = fmt.Errorf("error sending 'init' message: %w", err)
  2631  			return
  2632  		}
  2633  
  2634  		// Validate server ack.
  2635  		err = t.dc.acct.checkSig(init.Serialize(), ack.Sig)
  2636  		if err != nil {
  2637  			err = fmt.Errorf("'init' ack signature error: %v", err)
  2638  			return
  2639  		}
  2640  
  2641  		c.log.Debugf("Received valid ack for 'init' request for match %s", match)
  2642  
  2643  		// Save init ack sig.
  2644  		t.mtx.Lock()
  2645  		auth := &match.MetaData.Proof.Auth
  2646  		auth.InitSig = ack.Sig
  2647  		auth.InitStamp = uint64(time.Now().UnixMilli())
  2648  		err = t.db.UpdateMatch(&match.MetaMatch)
  2649  		if err != nil {
  2650  			err = fmt.Errorf("error storing init ack sig in database: %v", err)
  2651  		}
  2652  		t.mtx.Unlock()
  2653  	}()
  2654  }
  2655  
  2656  // redeemMatches will send a transaction redeeming the specified matches.
  2657  // The matches will be de-grouped so that matches marked as suspect are redeemed
  2658  // individually and separate from the non-suspect group.
  2659  //
  2660  // This method modifies match fields and MUST be called with the trackedTrade
  2661  // mutex lock held for writes.
  2662  func (c *Core) redeemMatches(t *trackedTrade, matches []*matchTracker) error {
  2663  	errs := newErrorSet("redeemMatches order %s - ", t.ID())
  2664  	groupables := make([]*matchTracker, 0, len(matches)) // Over-allocating if there are suspect matches
  2665  	var suspects []*matchTracker
  2666  	for _, m := range matches {
  2667  		if m.suspectRedeem {
  2668  			suspects = append(suspects, m)
  2669  		} else {
  2670  			groupables = append(groupables, m)
  2671  		}
  2672  	}
  2673  	if len(groupables) > 0 {
  2674  		if !t.wallets.toWallet.connected() {
  2675  			return errWalletNotConnected // don't ungroup, just return
  2676  		}
  2677  		maxRedeemsInTx := int(t.wallets.toWallet.Info().MaxRedeemsInTx)
  2678  		if maxRedeemsInTx <= 0 || len(groupables) < maxRedeemsInTx {
  2679  			c.redeemMatchGroup(t, groupables, errs)
  2680  		} else {
  2681  			for i := 0; i < len(groupables); i += maxRedeemsInTx {
  2682  				if i+maxRedeemsInTx < len(groupables) {
  2683  					c.redeemMatchGroup(t, groupables[i:i+maxRedeemsInTx], errs)
  2684  				} else {
  2685  					c.redeemMatchGroup(t, groupables[i:], errs)
  2686  				}
  2687  			}
  2688  		}
  2689  	}
  2690  	for _, m := range suspects {
  2691  		c.redeemMatchGroup(t, []*matchTracker{m}, errs)
  2692  	}
  2693  	return errs.ifAny()
  2694  }
  2695  
  2696  // lcm finds the Least Common Multiple (LCM) via GCD. Use to add fractions. The
  2697  // last two returns should be used to multiply the numerators when adding. a
  2698  // and b cannot be zero.
  2699  func lcm(a, b uint64) (lowest, multA, multB uint64) {
  2700  	// greatest common divisor (GCD) via Euclidean algorithm
  2701  	gcd := func(a, b uint64) uint64 {
  2702  		for b != 0 {
  2703  			t := b
  2704  			b = a % b
  2705  			a = t
  2706  		}
  2707  		return a
  2708  	}
  2709  	cd := gcd(a, b)
  2710  	return a * b / cd, b / cd, a / cd
  2711  }
  2712  
  2713  // redeemMatchGroup will send a transaction redeeming the specified matches.
  2714  //
  2715  // This method modifies match fields and MUST be called with the trackedTrade
  2716  // mutex lock held for writes.
  2717  func (c *Core) redeemMatchGroup(t *trackedTrade, matches []*matchTracker, errs *errorSet) {
  2718  	// Collect an asset.Redemption for each match into a slice of redemptions that
  2719  	// will be grouped into a single transaction.
  2720  	redemptions := make([]*asset.Redemption, 0, len(matches))
  2721  	for _, match := range matches {
  2722  		redemptions = append(redemptions, &asset.Redemption{
  2723  			Spends: match.counterSwap,
  2724  			Secret: match.MetaData.Proof.Secret,
  2725  		})
  2726  	}
  2727  
  2728  	// Send the transaction.
  2729  	redeemWallet := t.wallets.toWallet // this is our redeem
  2730  	if !redeemWallet.connected() {
  2731  		errs.add("%v", errWalletNotConnected)
  2732  		return
  2733  	}
  2734  	coinIDs, outCoin, fees, err := redeemWallet.Redeem(&asset.RedeemForm{
  2735  		Redemptions:   redemptions,
  2736  		FeeSuggestion: t.redeemFee(), // fallback - wallet will try to get a rate internally for configured redeem conf target
  2737  		Options:       t.options,
  2738  	})
  2739  	// If an error was encountered, fail all of the matches. A failed match will
  2740  	// not run again on during ticks.
  2741  	if err != nil {
  2742  		// Retry delays are based in part on this server's broadcast timeout.
  2743  		bTimeout, tickInterval := t.broadcastTimeout(), t.dc.ticker.Dur() // bTimeout / tickCheckInterval
  2744  		// If we lack bTimeout or tickInterval, we likely have no server config
  2745  		// on account of server down, so fallback to reasonable delay values.
  2746  		if bTimeout == 0 || tickInterval == 0 {
  2747  			tickInterval = defaultTickInterval
  2748  			bTimeout = 30 * time.Minute // don't declare missed too soon
  2749  		}
  2750  		// The caller will notify the user that there is a problem. We really
  2751  		// have no way of knowing whether this is recoverable (so we can't set
  2752  		// swapErr), but we do want to prevent redemptions every tick.
  2753  		for _, match := range matches {
  2754  			// Mark these matches as suspect. Suspect matches will not be
  2755  			// grouped for redemptions in future attempts.
  2756  			match.suspectRedeem = true
  2757  			match.redeemErrCount++
  2758  			// If we can still make a broadcast timeout, allow retries soon. It
  2759  			// is possible for RedemptionStamp or AuditStamp to be zero if we're
  2760  			// recovering during startup or after a DEX reconnect. In that case,
  2761  			// allow three retries before giving up.
  2762  			lastActionStamp := match.MetaData.Proof.Auth.AuditStamp
  2763  			if match.Side == order.Taker {
  2764  				lastActionStamp = match.MetaData.Proof.Auth.RedemptionStamp
  2765  			}
  2766  			lastActionTime := time.UnixMilli(int64(lastActionStamp))
  2767  			// Try to wait until about the next auto-tick to try again.
  2768  			waitTime := tickInterval * 3 / 4
  2769  			if time.Since(lastActionTime) > bTimeout ||
  2770  				(lastActionStamp == 0 && match.redeemErrCount >= tickCheckDivisions) {
  2771  				// If we already missed the broadcast timeout, we're not in as
  2772  				// much of a hurry. but keep trying and sending errors, because
  2773  				// we do want the user to recover.
  2774  				waitTime = 15 * time.Minute
  2775  			}
  2776  			match.delayTicks(waitTime)
  2777  		}
  2778  		errs.add("error sending redeem transaction: %v", err)
  2779  		return
  2780  	}
  2781  
  2782  	c.log.Infof("Broadcasted redeem transaction spending %d contracts for order %v, paying to %s (%s)",
  2783  		len(redemptions), t.ID(), outCoin, redeemWallet.Symbol)
  2784  
  2785  	if _, dynamic := t.wallets.toWallet.Wallet.(asset.DynamicSwapper); !dynamic {
  2786  		t.metaData.RedemptionFeesPaid += fees // dynamic tx wallets don't know the fees paid until mining
  2787  	}
  2788  
  2789  	err = t.db.UpdateOrderMetaData(t.ID(), t.metaData)
  2790  	if err != nil {
  2791  		c.log.Errorf("Error updating order metadata for order %s: %v", t.ID(), err)
  2792  	}
  2793  
  2794  	for _, match := range matches {
  2795  		c.log.Infof("Match %s complete: %s %d %s", match, sellString(t.Trade().Sell),
  2796  			match.Quantity, unbip(t.Prefix().BaseAsset),
  2797  		)
  2798  	}
  2799  
  2800  	// Find the least common multiplier to use as the denom for adding
  2801  	// reserve fractions.
  2802  	denom, marketMult, limitMult := lcm(uint64(len(t.matches)), t.Trade().Quantity)
  2803  	var refundNum, redeemNum uint64
  2804  
  2805  	// Save redemption details and send the redeem message to the DEX.
  2806  	// Saving the redemption details now makes it possible to resend the
  2807  	// `redeem` request at a later time if sending it now fails.
  2808  	for i, match := range matches {
  2809  		proof := &match.MetaData.Proof
  2810  		coinID := []byte(coinIDs[i])
  2811  		if match.Side == order.Taker {
  2812  			// The match won't be retired before the redeem request succeeds
  2813  			// because RedeemSig is required unless the match is revoked.
  2814  			match.Status = order.MatchComplete
  2815  			proof.TakerRedeem = coinID
  2816  		} else {
  2817  			// If we are taker we already released the refund
  2818  			// reserves when maker's redemption was found.
  2819  			if t.isMarketBuy() {
  2820  				refundNum += marketMult // * 1
  2821  			} else {
  2822  				refundNum += match.Quantity * limitMult
  2823  			}
  2824  			match.Status = order.MakerRedeemed
  2825  			proof.MakerRedeem = coinID
  2826  		}
  2827  		if t.isMarketBuy() {
  2828  			redeemNum += marketMult // * 1
  2829  		} else {
  2830  			redeemNum += match.Quantity * limitMult
  2831  		}
  2832  		if err := t.db.UpdateMatch(&match.MetaMatch); err != nil {
  2833  			errs.add("error storing swap details in database for match %s, coin %s: %v",
  2834  				match, coinIDString(t.wallets.fromWallet.AssetID, coinID), err)
  2835  		}
  2836  		if !match.matchCompleteSent {
  2837  			c.sendRedeemAsync(t, match, coinIDs[i], proof.Secret)
  2838  		}
  2839  	}
  2840  	if refundNum != 0 {
  2841  		t.unlockRefundFraction(refundNum, denom)
  2842  	}
  2843  	if redeemNum != 0 {
  2844  		t.unlockRedemptionFraction(redeemNum, denom)
  2845  	}
  2846  }
  2847  
  2848  // sendRedeemAsync starts a goroutine to send a `redeem` request for the specified
  2849  // match and save the server's ack sig to db. Sends a notification if an error
  2850  // occurs while sending the request or validating the server's response.
  2851  func (c *Core) sendRedeemAsync(t *trackedTrade, match *matchTracker, coinID, secret []byte) {
  2852  	if !atomic.CompareAndSwapUint32(&match.sendingRedeemAsync, 0, 1) {
  2853  		return
  2854  	}
  2855  
  2856  	c.log.Debugf("Notifying DEX %s of our %s swap redemption %v for match %s",
  2857  		t.dc.acct.host, t.wallets.toWallet.Symbol, coinIDString(t.wallets.toWallet.AssetID, coinID), match)
  2858  
  2859  	// Send the redeem request asynchronously.
  2860  	c.wg.Add(1) // So Core does not shut down until we're done with this request.
  2861  	go func() {
  2862  		defer c.wg.Done() // bottom of the stack
  2863  		var err error
  2864  		defer func() {
  2865  			atomic.StoreUint32(&match.sendingRedeemAsync, 0)
  2866  			if err != nil {
  2867  				corder := t.coreOrder()
  2868  				subject, details := c.formatDetails(TopicReportRedeemError, match, err)
  2869  				t.notify(newOrderNote(TopicReportRedeemError, subject, details, db.ErrorLevel, corder))
  2870  			}
  2871  		}()
  2872  
  2873  		msgRedeem := &msgjson.Redeem{
  2874  			OrderID: t.ID().Bytes(),
  2875  			MatchID: match.MatchID.Bytes(),
  2876  			CoinID:  coinID,
  2877  			Secret:  secret,
  2878  		}
  2879  		ack := new(msgjson.Acknowledgement)
  2880  		// The DEX may wait up to its configured broadcast timeout, but we will
  2881  		// retry on timeout or other error.
  2882  		timeout := t.broadcastTimeout() / 4
  2883  		if timeout < time.Minute { // sane minimum, or if we lack server config for any reason
  2884  			// Send would fail right away anyway if the server is really down,
  2885  			// but at least attempt it with a non-zero timeout.
  2886  			timeout = time.Minute
  2887  		}
  2888  		err = t.dc.signAndRequest(msgRedeem, msgjson.RedeemRoute, ack, timeout)
  2889  		if err != nil {
  2890  			var msgErr *msgjson.Error
  2891  			if errors.As(err, &msgErr) {
  2892  				if msgErr.Code == msgjson.SettlementSequenceError {
  2893  					c.log.Errorf("Starting match status resolution for 'redeem' request SettlementSequenceError")
  2894  					c.resolveMatchConflicts(t.dc, map[order.OrderID]*matchStatusConflict{
  2895  						t.ID(): {
  2896  							trade:   t,
  2897  							matches: []*matchTracker{match},
  2898  						},
  2899  					})
  2900  				} else if msgErr.Code == msgjson.RPCUnknownMatch {
  2901  					t.mtx.Lock()
  2902  					oid := t.ID()
  2903  					c.log.Warnf("DEX %s did not report active match %s on order %s - assuming revoked, status %v.",
  2904  						t.dc.acct.host, match, oid, match.Status)
  2905  					// We must have missed the revoke notification. Flag to allow recovery
  2906  					// and subsequent retirement of the match and parent trade.
  2907  					match.MetaData.Proof.SelfRevoked = true
  2908  					if err := c.db.UpdateMatch(&match.MetaMatch); err != nil {
  2909  						c.log.Errorf("Failed to update missing/revoked match: %v", err)
  2910  					}
  2911  					t.mtx.Unlock()
  2912  					numMissing := 1
  2913  					subject, details := c.formatDetails(TopicMissingMatches,
  2914  						numMissing, makeOrderToken(t.token()), t.dc.acct.host)
  2915  					c.notify(newOrderNote(TopicMissingMatches, subject, details, db.ErrorLevel, t.coreOrderInternal()))
  2916  				}
  2917  			}
  2918  			err = fmt.Errorf("error sending 'redeem' message: %w", err)
  2919  			return
  2920  		}
  2921  
  2922  		// Validate server ack.
  2923  		err = t.dc.acct.checkSig(msgRedeem.Serialize(), ack.Sig)
  2924  		if err != nil {
  2925  			err = fmt.Errorf("'redeem' ack signature error: %v", err)
  2926  			return
  2927  		}
  2928  
  2929  		c.log.Debugf("Received valid ack for 'redeem' request for match %s)", match)
  2930  
  2931  		// Save redeem ack sig.
  2932  		t.mtx.Lock()
  2933  		auth := &match.MetaData.Proof.Auth
  2934  		auth.RedeemSig = ack.Sig
  2935  		auth.RedeemStamp = uint64(time.Now().UnixMilli())
  2936  		if match.Side == order.Maker && match.Status < order.MatchComplete {
  2937  			// As maker, this is the end. However, this diverges from server,
  2938  			// which still needs taker's redeem.
  2939  			if conf := match.redemptionConfs; conf > 0 && conf >= match.redemptionConfsReq {
  2940  				match.Status = order.MatchConfirmed // redeem tx already confirmed before redeem request accepted by server
  2941  			} else {
  2942  				match.Status = order.MatchComplete
  2943  			}
  2944  		} else if match.Side == order.Taker {
  2945  			match.matchCompleteSent = true
  2946  		}
  2947  		err = t.db.UpdateMatch(&match.MetaMatch)
  2948  		if err != nil {
  2949  			err = fmt.Errorf("error storing redeem ack sig in database: %v", err)
  2950  		}
  2951  		if match.Status == order.MatchConfirmed {
  2952  			subject, details := t.formatDetails(TopicRedemptionConfirmed, match.token(), makeOrderToken(t.token()))
  2953  			note := newMatchNote(TopicRedemptionConfirmed, subject, details, db.Success, t, match)
  2954  			t.notify(note)
  2955  		}
  2956  		t.mtx.Unlock()
  2957  	}()
  2958  }
  2959  
  2960  func (t *trackedTrade) redeemFee() uint64 {
  2961  	// Try not to use (*Core).feeSuggestion here, since it can incur an RPC
  2962  	// request to the server. t.redeemFeeSuggestion is updated every tick and
  2963  	// uses a rate directly from our wallet, if available. Only go looking for
  2964  	// one if we don't have one cached.
  2965  	var feeSuggestion uint64
  2966  	if _, is := t.accountRedeemer(); is {
  2967  		feeSuggestion = t.metaData.RedeemMaxFeeRate
  2968  	} else {
  2969  		feeSuggestion = t.redeemFeeSuggestion.get()
  2970  	}
  2971  	if feeSuggestion == 0 {
  2972  		feeSuggestion = t.dc.bestBookFeeSuggestion(t.wallets.toWallet.AssetID)
  2973  	}
  2974  	return feeSuggestion
  2975  }
  2976  
  2977  // confirmRedemption attempts to confirm the redemptions for each match, and
  2978  // then return any refund addresses that we won't be using.
  2979  func (c *Core) confirmRedemptions(t *trackedTrade, matches []*matchTracker) {
  2980  	var refundContracts [][]byte
  2981  	for _, m := range matches {
  2982  		if confirmed, err := c.confirmRedemption(t, m); err != nil {
  2983  			t.dc.log.Errorf("Unable to confirm redemption: %v", err)
  2984  		} else if confirmed {
  2985  			refundContracts = append(refundContracts, m.MetaData.Proof.ContractData)
  2986  		}
  2987  	}
  2988  	if len(refundContracts) == 0 {
  2989  		return
  2990  	}
  2991  	if ar, is := t.wallets.fromWallet.Wallet.(asset.AddressReturner); is {
  2992  		ar.ReturnRefundContracts(refundContracts)
  2993  	}
  2994  }
  2995  
  2996  // confirmRedemption checks if the user's redemption has been confirmed,
  2997  // and if so, updates the match's status to MatchConfirmed.
  2998  //
  2999  // This method accesses match fields and MUST be called with the trackedTrade
  3000  // mutex lock held for writes.
  3001  func (c *Core) confirmRedemption(t *trackedTrade, match *matchTracker) (bool, error) {
  3002  	if confs := match.redemptionConfs; confs > 0 && confs >= match.redemptionConfsReq { // already there, stop checking
  3003  		if len(match.MetaData.Proof.Auth.RedeemSig) == 0 && (!t.isSelfGoverned() && !match.MetaData.Proof.IsRevoked()) {
  3004  			return false, nil // waiting on redeem request to succeed
  3005  		}
  3006  		// Redeem request just succeeded or we gave up on the server.
  3007  		if match.Status == order.MatchConfirmed {
  3008  			return true, nil // raced with concurrent sendRedeemAsync
  3009  		}
  3010  		match.Status = order.MatchConfirmed
  3011  		err := t.db.UpdateMatch(&match.MetaMatch)
  3012  		if err != nil {
  3013  			t.dc.log.Errorf("failed to update match in db: %v", err)
  3014  		}
  3015  		subject, details := t.formatDetails(TopicRedemptionConfirmed, match.token(), makeOrderToken(t.token()))
  3016  		note := newMatchNote(TopicRedemptionConfirmed, subject, details, db.Success, t, match)
  3017  		t.notify(note)
  3018  		return true, nil
  3019  	}
  3020  
  3021  	// In some cases the wallet will need to send a new redeem transaction.
  3022  	toWallet := t.wallets.toWallet
  3023  
  3024  	if err := toWallet.checkPeersAndSyncStatus(); err != nil {
  3025  		return false, err
  3026  	}
  3027  
  3028  	didUnlock, err := toWallet.refreshUnlock()
  3029  	if err != nil { // Just log it and try anyway.
  3030  		t.dc.log.Errorf("refreshUnlock error checking redeem %s: %v", toWallet.Symbol, err)
  3031  	}
  3032  	if didUnlock {
  3033  		t.dc.log.Warnf("Unexpected unlock needed for the %s wallet to check a redemption", toWallet.Symbol)
  3034  	}
  3035  
  3036  	proof := &match.MetaData.Proof
  3037  	var redeemCoinID order.CoinID
  3038  	if match.Side == order.Maker {
  3039  		redeemCoinID = proof.MakerRedeem
  3040  	} else {
  3041  		redeemCoinID = proof.TakerRedeem
  3042  	}
  3043  
  3044  	match.confirmRedemptionNumTries++
  3045  
  3046  	redemptionStatus, err := toWallet.Wallet.ConfirmRedemption(dex.Bytes(redeemCoinID), &asset.Redemption{
  3047  		Spends: match.counterSwap,
  3048  		Secret: proof.Secret,
  3049  	}, t.redeemFee())
  3050  	switch {
  3051  	case err == nil:
  3052  	case errors.Is(err, asset.ErrSwapRefunded):
  3053  		subject, details := t.formatDetails(TopicSwapRefunded, match.token(), makeOrderToken(t.token()))
  3054  		note := newMatchNote(TopicSwapRefunded, subject, details, db.ErrorLevel, t, match)
  3055  		t.notify(note)
  3056  		match.Status = order.MatchConfirmed
  3057  		err := t.db.UpdateMatch(&match.MetaMatch)
  3058  		if err != nil {
  3059  			t.dc.log.Errorf("Failed to update match in db %v", err)
  3060  		}
  3061  		return false, errors.New("swap was already refunded by the counterparty")
  3062  
  3063  	case errors.Is(err, asset.ErrTxRejected):
  3064  		match.redemptionRejected = true
  3065  		// We need to seek user approval before trying again, since new fees
  3066  		// could be incurred.
  3067  		actionRequest, note := newRejectedRedemptionNote(toWallet.AssetID, t.ID(), redeemCoinID)
  3068  		t.notify(note)
  3069  		c.requestedActionMtx.Lock()
  3070  		c.requestedActions[dex.Bytes(redeemCoinID).String()] = actionRequest
  3071  		c.requestedActionMtx.Unlock()
  3072  		return false, fmt.Errorf("%s transaction %s was rejected. Seeking user approval before trying again",
  3073  			unbip(toWallet.AssetID), coinIDString(toWallet.AssetID, redeemCoinID))
  3074  	case errors.Is(err, asset.ErrTxLost):
  3075  		// The transaction was nonce-replaced or otherwise lost without
  3076  		// rejection or with user acknowlegement. Try again.
  3077  		var coinID order.CoinID
  3078  		if match.Side == order.Taker {
  3079  			coinID = match.MetaData.Proof.TakerRedeem
  3080  			match.MetaData.Proof.TakerRedeem = nil
  3081  			match.Status = order.MakerRedeemed
  3082  		} else {
  3083  			coinID = match.MetaData.Proof.MakerRedeem
  3084  			match.MetaData.Proof.MakerRedeem = nil
  3085  			match.Status = order.TakerSwapCast
  3086  		}
  3087  		c.log.Infof("Redemption %s (%s) has been noted as lost.", coinID, unbip(toWallet.AssetID))
  3088  
  3089  		if err := t.db.UpdateMatch(&match.MetaMatch); err != nil {
  3090  			t.dc.log.Errorf("failed to update match after lost tx reported: %v", err)
  3091  		}
  3092  		return false, nil
  3093  	default:
  3094  		match.delayTicks(time.Minute * 15)
  3095  		return false, fmt.Errorf("error confirming redemption for coin %v. already tried %d times, will retry later: %v",
  3096  			redeemCoinID, match.confirmRedemptionNumTries, err)
  3097  	}
  3098  
  3099  	var redemptionResubmitted, redemptionConfirmed bool
  3100  	if !bytes.Equal(redeemCoinID, redemptionStatus.CoinID) {
  3101  		redemptionResubmitted = true
  3102  		if match.Side == order.Maker {
  3103  			proof.MakerRedeem = order.CoinID(redemptionStatus.CoinID)
  3104  		} else {
  3105  			proof.TakerRedeem = order.CoinID(redemptionStatus.CoinID)
  3106  		}
  3107  	}
  3108  
  3109  	match.redemptionConfs, match.redemptionConfsReq = redemptionStatus.Confs, redemptionStatus.Req
  3110  
  3111  	if redemptionStatus.Confs >= redemptionStatus.Req &&
  3112  		(len(match.MetaData.Proof.Auth.RedeemSig) > 0 || t.isSelfGoverned()) {
  3113  		redemptionConfirmed = true
  3114  		match.Status = order.MatchConfirmed
  3115  	}
  3116  
  3117  	if redemptionResubmitted || redemptionConfirmed {
  3118  		err := t.db.UpdateMatch(&match.MetaMatch)
  3119  		if err != nil {
  3120  			t.dc.log.Errorf("failed to update match in db: %v", err)
  3121  		}
  3122  	}
  3123  
  3124  	if redemptionResubmitted {
  3125  		subject, details := t.formatDetails(TopicRedemptionResubmitted, match.token(), makeOrderToken(t.token()))
  3126  		note := newMatchNote(TopicRedemptionResubmitted, subject, details, db.WarningLevel, t, match)
  3127  		t.notify(note)
  3128  	}
  3129  
  3130  	if redemptionConfirmed {
  3131  		subject, details := t.formatDetails(TopicRedemptionConfirmed, match.token(), makeOrderToken(t.token()))
  3132  		note := newMatchNote(TopicRedemptionConfirmed, subject, details, db.Success, t, match)
  3133  		t.notify(note)
  3134  	} else {
  3135  		note := newMatchNote(TopicConfirms, "", "", db.Data, t, match)
  3136  		t.notify(note)
  3137  	}
  3138  	return redemptionConfirmed, nil
  3139  }
  3140  
  3141  // findMakersRedemption starts a goroutine to search for the redemption of
  3142  // taker's contract.
  3143  //
  3144  // This method modifies trackedTrade fields and MUST be called with the
  3145  // trackedTrade mutex lock held for writes.
  3146  func (t *trackedTrade) findMakersRedemption(ctx context.Context, match *matchTracker) {
  3147  	if match.cancelRedemptionSearch != nil {
  3148  		return
  3149  	}
  3150  
  3151  	// NOTE: Use Core's ctx to auto-cancel this search when Core is shut down.
  3152  	ctx, cancel := context.WithCancel(ctx)
  3153  	match.cancelRedemptionSearch = cancel
  3154  	swapCoinID := dex.Bytes(match.MetaData.Proof.TakerSwap)
  3155  	swapContract := dex.Bytes(match.MetaData.Proof.ContractData)
  3156  
  3157  	wallet := t.wallets.fromWallet
  3158  	if !wallet.connected() {
  3159  		t.dc.log.Errorf("Cannot find redemption with wallet not connected")
  3160  		return
  3161  	}
  3162  
  3163  	// Run redemption finder in goroutine.
  3164  	go func() {
  3165  		defer cancel() // don't leak the context when we reset match.cancelRedemptionSearch
  3166  		redemptionCoinID, secret, err := wallet.FindRedemption(ctx, swapCoinID, swapContract)
  3167  
  3168  		// Redemption search done, with or without error.
  3169  		// Keep the mutex locked for the remainder of this goroutine execution to
  3170  		// read and write match fields while processing the find redemption result.
  3171  		t.mtx.Lock()
  3172  		defer t.mtx.Unlock()
  3173  
  3174  		match.cancelRedemptionSearch = nil
  3175  		symbol, assetID := wallet.Symbol, wallet.AssetID
  3176  
  3177  		if err != nil {
  3178  			// Ignore the error if we've refunded, the error would likely be context
  3179  			// canceled or secret parse error (if redemption search encountered the
  3180  			// refund before the search could be canceled).
  3181  			if len(match.MetaData.Proof.RefundCoin) == 0 {
  3182  				t.dc.log.Errorf("Error finding redemption of taker's %s contract %s (%s) for order %s, match %s: %v.",
  3183  					symbol, coinIDString(assetID, swapCoinID), swapContract, t.ID(), match, err)
  3184  			}
  3185  			return
  3186  		}
  3187  
  3188  		if match.Status != order.TakerSwapCast {
  3189  			t.dc.log.Errorf("Received find redemption result at wrong step, order %s, match %s, status %s.",
  3190  				t.ID(), match, match.Status)
  3191  			return
  3192  		}
  3193  
  3194  		proof := &match.MetaData.Proof
  3195  		if !t.wallets.toWallet.ValidateSecret(secret, proof.SecretHash) {
  3196  			t.dc.log.Errorf("Found invalid redemption of taker's %s contract %s (%s) for order %s, match %s: invalid secret %s, hash %s.",
  3197  				symbol, coinIDString(assetID, swapCoinID), swapContract, t.ID(), match, secret, proof.SecretHash)
  3198  			return
  3199  		}
  3200  
  3201  		if t.isMarketBuy() {
  3202  			t.unlockRefundFraction(1, uint64(len(t.matches)))
  3203  		} else {
  3204  			t.unlockRefundFraction(match.Quantity, t.Trade().Quantity)
  3205  		}
  3206  
  3207  		// Update the match status and set the secret so that Maker's swap
  3208  		// will be redeemed in the next call to trade.tick().
  3209  		match.Status = order.MakerRedeemed
  3210  		proof.MakerRedeem = []byte(redemptionCoinID)
  3211  		proof.Secret = secret
  3212  		proof.SelfRevoked = true // Set match as revoked.
  3213  		err = t.db.UpdateMatch(&match.MetaMatch)
  3214  		if err != nil {
  3215  			t.dc.log.Errorf("waitForRedemptions: error storing match info in database: %v", err)
  3216  		}
  3217  
  3218  		t.dc.log.Infof("Found redemption of contract %s (%s) for order %s, match %s. Redeem: %v",
  3219  			coinIDString(assetID, swapCoinID), symbol, t.ID(), match,
  3220  			coinIDString(assetID, redemptionCoinID))
  3221  
  3222  		subject, details := t.formatDetails(TopicMatchRecovered,
  3223  			symbol, coinIDString(assetID, redemptionCoinID), match)
  3224  		t.notify(newOrderNote(TopicMatchRecovered, subject, details, db.Poke, t.coreOrderInternal()))
  3225  	}()
  3226  }
  3227  
  3228  // refundMatches will send refund transactions for the specified matches.
  3229  //
  3230  // This method modifies match fields and MUST be called with the trackedTrade
  3231  // mutex lock held for writes.
  3232  func (c *Core) refundMatches(t *trackedTrade, matches []*matchTracker) (uint64, error) {
  3233  	errs := newErrorSet("refundMatches: order %s - ", t.ID())
  3234  
  3235  	refundWallet := t.wallets.fromWallet // refunding to our wallet
  3236  	symbol, assetID := refundWallet.Symbol, refundWallet.AssetID
  3237  	var refundedQty uint64
  3238  
  3239  	for _, match := range matches {
  3240  		if len(match.MetaData.Proof.RefundCoin) != 0 {
  3241  			c.log.Errorf("attempted to execute duplicate refund for match %s, side %s, status %s",
  3242  				match, match.Side, match.Status)
  3243  			continue
  3244  		}
  3245  		contractToRefund := match.MetaData.Proof.ContractData
  3246  		var swapCoinID dex.Bytes
  3247  		var matchFailureReason string
  3248  		switch {
  3249  		case match.Side == order.Maker && match.Status == order.MakerSwapCast:
  3250  			swapCoinID = dex.Bytes(match.MetaData.Proof.MakerSwap)
  3251  			matchFailureReason = "no valid counterswap received from Taker"
  3252  		case match.Side == order.Maker && match.Status == order.TakerSwapCast && len(match.MetaData.Proof.MakerRedeem) == 0:
  3253  			swapCoinID = dex.Bytes(match.MetaData.Proof.MakerSwap)
  3254  			matchFailureReason = "unable to redeem Taker's swap"
  3255  		case match.Side == order.Taker && match.Status == order.TakerSwapCast:
  3256  			swapCoinID = dex.Bytes(match.MetaData.Proof.TakerSwap)
  3257  			matchFailureReason = "no valid redemption received from Maker"
  3258  		default:
  3259  			c.log.Errorf("attempted to execute invalid refund for match %s, side %s, status %s",
  3260  				match, match.Side, match.Status)
  3261  			continue
  3262  		}
  3263  
  3264  		swapCoinString := coinIDString(assetID, swapCoinID)
  3265  		c.log.Infof("Refunding %s contract %s for match %s (%s)",
  3266  			symbol, swapCoinString, match, matchFailureReason)
  3267  
  3268  		var feeRate uint64
  3269  		if _, is := t.accountRefunder(); is {
  3270  			feeRate = t.metaData.MaxFeeRate
  3271  		}
  3272  		if feeRate == 0 {
  3273  			feeRate = c.feeSuggestionAny(assetID) // includes wallet itself
  3274  		}
  3275  
  3276  		refundCoin, err := refundWallet.Refund(swapCoinID, contractToRefund, feeRate)
  3277  		if err != nil {
  3278  			// CRITICAL - Refund must indicate if the swap is spent (i.e.
  3279  			// redeemed already) so that as taker we will start the
  3280  			// auto-redemption path.
  3281  			if errors.Is(err, asset.CoinNotFoundError) && match.Side == order.Taker {
  3282  				match.refundErr = err
  3283  				// Could not find the contract coin, which means it has been
  3284  				// spent. Unless the locktime is expired, we would have already
  3285  				// started FindRedemption for this contract.
  3286  				c.log.Debugf("Failed to refund %s contract %s, already redeemed. Beginning find redemption.",
  3287  					symbol, swapCoinString)
  3288  				t.findMakersRedemption(c.ctx, match)
  3289  			} else {
  3290  				match.delayTicks(time.Minute * 5)
  3291  				errs.add("error sending refund tx for match %s, swap coin %s: %v",
  3292  					match, swapCoinString, err)
  3293  				if match.Status == order.TakerSwapCast && match.Side == order.Taker {
  3294  					// Check for a redeem even though Refund did not indicate it
  3295  					// was spent via CoinNotFoundError, but do not set refundErr
  3296  					// so that a refund can be tried again.
  3297  					t.findMakersRedemption(c.ctx, match)
  3298  				}
  3299  			}
  3300  			continue
  3301  		}
  3302  
  3303  		if t.isMarketBuy() {
  3304  			t.unlockRedemptionFraction(1, uint64(len(t.matches)))
  3305  			t.unlockRefundFraction(1, uint64(len(t.matches)))
  3306  		} else {
  3307  			t.unlockRedemptionFraction(match.Quantity, t.Trade().Quantity)
  3308  			t.unlockRefundFraction(match.Quantity, t.Trade().Quantity)
  3309  		}
  3310  
  3311  		// Refund successful, cancel any previously started attempt to find
  3312  		// counter-party's redemption.
  3313  		if match.cancelRedemptionSearch != nil {
  3314  			match.cancelRedemptionSearch()
  3315  		}
  3316  		if t.Trade().Sell {
  3317  			refundedQty += match.Quantity
  3318  		} else {
  3319  			refundedQty += calc.BaseToQuote(match.Rate, match.Quantity)
  3320  		}
  3321  		match.MetaData.Proof.RefundCoin = []byte(refundCoin)
  3322  		match.MetaData.Proof.SelfRevoked = true // Set match as revoked.
  3323  		err = t.db.UpdateMatch(&match.MetaMatch)
  3324  		if err != nil {
  3325  			errs.add("error storing match info in database: %v", err)
  3326  		}
  3327  	}
  3328  
  3329  	return refundedQty, errs.ifAny()
  3330  }
  3331  
  3332  // processAuditMsg processes the audit request from the server. A non-nil error
  3333  // is only returned if the match referenced by the Audit message is not known.
  3334  func (t *trackedTrade) processAuditMsg(msgID uint64, audit *msgjson.Audit) error {
  3335  	t.mtx.Lock()
  3336  	defer t.mtx.Unlock()
  3337  	// Find the match and check the server's signature.
  3338  	var mid order.MatchID
  3339  	copy(mid[:], audit.MatchID)
  3340  	match, found := t.matches[mid]
  3341  	if !found {
  3342  		return fmt.Errorf("processAuditMsg: match %v not found for order %s", mid, t.ID())
  3343  	}
  3344  	// Check the server signature.
  3345  	sigMsg := audit.Serialize()
  3346  	err := t.dc.acct.checkSig(sigMsg, audit.Sig)
  3347  	if err != nil {
  3348  		// Log, but don't quit. If the audit passes, great.
  3349  		t.dc.log.Warnf("Server audit signature error: %v", err)
  3350  	}
  3351  
  3352  	// Start searching for and audit the contract. This can take some time
  3353  	// depending on node connectivity, so this is run in a goroutine. If the
  3354  	// contract and coin (amount) are successfully validated, the matchTracker
  3355  	// data are updated.
  3356  	go func() {
  3357  		// Search until it's known to be revoked.
  3358  		err := t.auditContract(match, audit.CoinID, audit.Contract, audit.TxData)
  3359  		if err != nil {
  3360  			contractID := coinIDString(t.wallets.toWallet.AssetID, audit.CoinID)
  3361  			t.dc.log.Errorf("Failed to audit contract coin %v (%s) for match %s: %v",
  3362  				contractID, t.wallets.toWallet.Symbol, match, err)
  3363  			// Don't revoke in case server sends a revised audit request before
  3364  			// the match is revoked.
  3365  			return
  3366  		}
  3367  
  3368  		// The audit succeeded. Update and store match data.
  3369  		t.mtx.Lock()
  3370  		auth := &match.MetaData.Proof.Auth
  3371  		auth.AuditStamp, auth.AuditSig = audit.Time, audit.Sig
  3372  		t.notify(newMatchNote(TopicAudit, "", "", db.Data, t, match))
  3373  		err = t.db.UpdateMatch(&match.MetaMatch)
  3374  		t.mtx.Unlock()
  3375  		if err != nil {
  3376  			t.dc.log.Errorf("Error updating database for match %s: %v", match, err)
  3377  		}
  3378  
  3379  		// Respond to DEX, but this is not consequential.
  3380  		err = t.dc.ack(msgID, mid, audit)
  3381  		if err != nil {
  3382  			t.dc.log.Debugf("Error acknowledging audit to server (not necessarily an error): %v", err)
  3383  			// The server's response timeout may have just passed, but we got
  3384  			// what we needed to do our swap or redeem if the match is still
  3385  			// live, so do not log this as an error.
  3386  		}
  3387  	}()
  3388  
  3389  	return nil
  3390  }
  3391  
  3392  // searchAuditInfo tries to obtain the asset.AuditInfo from the ExchangeWallet.
  3393  // Handle network latency or other transient node errors. The coin waiter will
  3394  // run once every recheckInterval until successful or until the match is
  3395  // revoked. The client is asked by the server to audit a contract transaction,
  3396  // and they have until broadcast timeout to do it before they get penalized and
  3397  // the match revoked. Thus, there is no reason to give up on the request sooner
  3398  // since the server will not ask again and the client will not solicit the
  3399  // counterparty contract data again except on reconnect. This may block for a
  3400  // long time and should be run in a goroutine. The trackedTrade mtx must NOT be
  3401  // locked.
  3402  //
  3403  // NOTE: This assumes the Wallet's AuditContract method may need to actually
  3404  // locate the contract transaction on the network. However, for some (or all)
  3405  // assets, the audit may be performed with just txData, which makes this
  3406  // "search" obsolete. We may wish to remove the latencyQ and have this be a
  3407  // single call to AuditContract. Leaving as-is for now.
  3408  func (t *trackedTrade) searchAuditInfo(match *matchTracker, coinID []byte, contract, txData []byte) (*asset.AuditInfo, error) {
  3409  	errChan := make(chan error, 1)
  3410  	var auditInfo *asset.AuditInfo
  3411  	var tries int
  3412  	toWallet := t.wallets.toWallet
  3413  	if !toWallet.connected() {
  3414  		return nil, errWalletNotConnected
  3415  	}
  3416  	contractID, contractSymb := coinIDString(toWallet.AssetID, coinID), toWallet.Symbol
  3417  	tLastWarning := time.Now()
  3418  	t.latencyQ.Wait(&wait.Waiter{
  3419  		Expiration: time.Now().Add(24 * time.Hour), // effectively forever
  3420  		TryFunc: func() wait.TryDirective {
  3421  			var err error
  3422  			auditInfo, err = toWallet.AuditContract(coinID, contract, txData, true)
  3423  			if err == nil {
  3424  				// Success.
  3425  				errChan <- nil
  3426  				return wait.DontTryAgain
  3427  			}
  3428  			if errors.Is(err, asset.CoinNotFoundError) {
  3429  				// Didn't find it that time.
  3430  				t.dc.log.Tracef("Still searching for counterparty's contract coin %v (%s) for match %s.", contractID, contractSymb, match)
  3431  				if t.matchIsRevoked(match) {
  3432  					errChan <- ExpirationErr(fmt.Sprintf("match revoked while waiting to find counterparty contract coin %v (%s). "+
  3433  						"Check your internet and wallet connections!", contractID, contractSymb))
  3434  					return wait.DontTryAgain
  3435  				}
  3436  				if time.Since(tLastWarning) > 30*time.Minute {
  3437  					tLastWarning = time.Now()
  3438  					subject, detail := t.formatDetails(TopicAuditTrouble, contractID, contractSymb, match)
  3439  					t.notify(newOrderNote(TopicAuditTrouble, subject, detail, db.WarningLevel, t.coreOrder()))
  3440  				}
  3441  				tries++
  3442  				return wait.TryAgain
  3443  			}
  3444  			// Even retry for unrecognized errors, at least for a little while.
  3445  			// With a default recheckInterval of 5 seconds, this is 2 minutes.
  3446  			if tries < 24 {
  3447  				t.dc.log.Errorf("Unexpected audit contract %v (%s) error (will try again): %v", contractID, contractSymb, err)
  3448  				tries++
  3449  				return wait.TryAgain
  3450  			}
  3451  			errChan <- err
  3452  			return wait.DontTryAgain
  3453  
  3454  		},
  3455  		ExpireFunc: func() {
  3456  			errChan <- ExpirationErr(fmt.Sprintf("failed to find counterparty contract coin %v (%s). "+
  3457  				"Check your internet and wallet connections!", contractID, contractSymb))
  3458  		},
  3459  	})
  3460  
  3461  	// Wait for the coin waiter to find and audit the contract coin, or timeout.
  3462  	err := <-errChan
  3463  	if err != nil {
  3464  		return nil, err
  3465  	}
  3466  	return auditInfo, nil
  3467  }
  3468  
  3469  // auditContract audits the contract for the match and relevant MatchProof
  3470  // fields are set. This may block for a long period, and should be run in a
  3471  // goroutine. The trackedTrade mtx must NOT be locked. The match is updated in
  3472  // the DB if the audit succeeds.
  3473  func (t *trackedTrade) auditContract(match *matchTracker, coinID, contract, txData []byte) error {
  3474  	auditInfo, err := t.searchAuditInfo(match, coinID, contract, txData)
  3475  	if err != nil {
  3476  		return err
  3477  	}
  3478  
  3479  	assetID, contractSymb := t.wallets.toWallet.AssetID, t.wallets.toWallet.Symbol
  3480  	contractID := coinIDString(assetID, coinID)
  3481  
  3482  	// Audit the contract.
  3483  	// 1. Recipient Address
  3484  	// 2. Contract value
  3485  	// 3. Secret hash: maker compares, taker records
  3486  	if auditInfo.Recipient != t.Trade().Address {
  3487  		return fmt.Errorf("swap recipient %s in contract coin %v (%s) is not the order address %s",
  3488  			auditInfo.Recipient, contractID, contractSymb, t.Trade().Address)
  3489  	}
  3490  
  3491  	auditQty := match.Quantity
  3492  	if t.Trade().Sell {
  3493  		auditQty = calc.BaseToQuote(match.Rate, auditQty)
  3494  	}
  3495  	if auditInfo.Coin.Value() < auditQty {
  3496  		return fmt.Errorf("swap contract coin %v (%s) value %d was lower than expected %d",
  3497  			contractID, contractSymb, auditInfo.Coin.Value(), auditQty)
  3498  	}
  3499  
  3500  	// TODO: Consider having the server supply the contract txn's fee rate to
  3501  	// improve the taker's audit with a check of the maker's contract fee rate.
  3502  	// The server should be checking the fee rate, but the client should not
  3503  	// trust it. The maker could also check the taker's contract txn fee rate,
  3504  	// but their contract is already broadcasted, so the check is of less value
  3505  	// as they can only wait for it to be mined to redeem it, in which case the
  3506  	// fee rate no longer matters, or wait for the lock time to expire to refund.
  3507  
  3508  	// Check and store the counterparty contract data.
  3509  	matchTime := match.matchTime()
  3510  	reqLockTime := encode.DropMilliseconds(matchTime.Add(t.lockTimeMaker)) // counterparty == maker
  3511  	if match.Side == order.Maker {
  3512  		reqLockTime = encode.DropMilliseconds(matchTime.Add(t.lockTimeTaker)) // counterparty == taker
  3513  	}
  3514  	if auditInfo.Expiration.Before(reqLockTime) {
  3515  		return fmt.Errorf("lock time too early. Need %s, got %s", reqLockTime, auditInfo.Expiration)
  3516  	}
  3517  
  3518  	t.mtx.Lock()
  3519  	defer t.mtx.Unlock()
  3520  	proof := &match.MetaData.Proof
  3521  	if match.Side == order.Maker {
  3522  		// Check that the secret hash is correct.
  3523  		if !bytes.Equal(proof.SecretHash, auditInfo.SecretHash) {
  3524  			return fmt.Errorf("secret hash mismatch for contract coin %v (%s), contract %v. expected %x, got %v",
  3525  				auditInfo.Coin, contractSymb, contract, proof.SecretHash, auditInfo.SecretHash)
  3526  		}
  3527  		// Audit successful. Update status and other match data.
  3528  		match.Status = order.TakerSwapCast
  3529  		proof.TakerSwap = coinID
  3530  	} else {
  3531  		proof.SecretHash = auditInfo.SecretHash
  3532  		match.Status = order.MakerSwapCast
  3533  		proof.MakerSwap = coinID
  3534  	}
  3535  	proof.CounterTxData = txData
  3536  	proof.CounterContract = contract
  3537  	match.counterSwap = auditInfo
  3538  
  3539  	err = t.db.UpdateMatch(&match.MetaMatch)
  3540  	if err != nil {
  3541  		t.dc.log.Errorf("Error updating database for match %v: %s", match, err)
  3542  	}
  3543  
  3544  	t.dc.log.Infof("Audited contract (%s: %v) paying to %s for order %s, match %s, "+
  3545  		"with tx data = %t. Script: %x", contractSymb, auditInfo.Coin,
  3546  		auditInfo.Recipient, t.ID(), match, len(txData) > 0, contract)
  3547  
  3548  	return nil
  3549  }
  3550  
  3551  // processRedemption processes the redemption request from the server.
  3552  func (t *trackedTrade) processRedemption(msgID uint64, redemption *msgjson.Redemption) error {
  3553  	t.mtx.Lock()
  3554  	defer t.mtx.Unlock()
  3555  	var mid order.MatchID
  3556  	copy(mid[:], redemption.MatchID)
  3557  	errs := newErrorSet("processRedemption order %s, match %s - ", t.ID(), mid)
  3558  	match, found := t.matches[mid]
  3559  	if !found {
  3560  		return errs.add("match not known")
  3561  	}
  3562  
  3563  	// Validate that this request satisfies expected preconditions if
  3564  	// we're the Taker. Not necessary if we're maker as redemption
  3565  	// requests are pretty much just a formality for Maker. Also, if
  3566  	// the order was loaded from the DB and we've already redeemed
  3567  	// Taker's swap, the counterSwap (AuditInfo for Taker's swap) will
  3568  	// not have been retrieved.
  3569  	if match.Side == order.Taker {
  3570  		switch {
  3571  		case match.counterSwap == nil:
  3572  			return errs.add("redemption received before audit request")
  3573  		case match.Status == order.TakerSwapCast:
  3574  			// redemption requests should typically arrive when the match
  3575  			// is at TakerSwapCast
  3576  		case match.Status > order.TakerSwapCast && len(match.MetaData.Proof.Auth.RedemptionSig) == 0:
  3577  			// status might have moved 1+ steps forward if this redemption
  3578  			// request is received after we've already found the redemption
  3579  		default:
  3580  			return fmt.Errorf("maker redemption received at incorrect step %d", match.Status)
  3581  		}
  3582  	} else {
  3583  		if match.Status < order.MakerRedeemed { // only makes sense if we've redeemed
  3584  			return fmt.Errorf("redemption request received as maker for match %v in status %v",
  3585  				mid, match.Status)
  3586  		}
  3587  		t.dc.log.Tracef("Received a courtesy redemption request for match %v as maker", mid)
  3588  	}
  3589  
  3590  	// Respond to the DEX.
  3591  	err := t.dc.ack(msgID, match.MatchID, redemption)
  3592  	if err != nil {
  3593  		return errs.add("Audit - %v", err)
  3594  	}
  3595  
  3596  	// Update the database.
  3597  	match.MetaData.Proof.Auth.RedemptionSig = redemption.Sig
  3598  	match.MetaData.Proof.Auth.RedemptionStamp = redemption.Time
  3599  
  3600  	if match.Side == order.Taker {
  3601  		// As taker, this step is important because we validate that the
  3602  		// provided secret corresponds to the secret hash in our contract.
  3603  		err = t.processMakersRedemption(match, redemption.CoinID, redemption.Secret)
  3604  		if err != nil {
  3605  			errs.addErr(err)
  3606  		}
  3607  	} else {
  3608  		// Historically, the server does not send a redemption request to the
  3609  		// maker for the taker's redeem since match negotiation is complete
  3610  		// client-side at time of redeem. Just store the redeem CoinID as
  3611  		// TakerRedeem. Our own match negotiation has or will advance the status
  3612  		// and handle coin unlocking as needed.
  3613  		match.MetaData.Proof.TakerRedeem = order.CoinID(redemption.CoinID)
  3614  	}
  3615  
  3616  	err = t.db.UpdateMatch(&match.MetaMatch)
  3617  	if err != nil {
  3618  		errs.add("error storing match info in database: %v", err)
  3619  	}
  3620  	return errs.ifAny()
  3621  }
  3622  
  3623  func (t *trackedTrade) processMakersRedemption(match *matchTracker, coinID, secret []byte) error {
  3624  	if match.Side == order.Maker {
  3625  		return fmt.Errorf("processMakersRedemption called when we are the maker, which is nonsense. order = %s, match = %s", t.ID(), match)
  3626  	}
  3627  
  3628  	proof := &match.MetaData.Proof
  3629  	secretHash := proof.SecretHash
  3630  	wallet := t.wallets.toWallet
  3631  	if !wallet.ValidateSecret(secret, secretHash) {
  3632  		return fmt.Errorf("secret %x received does not hash to the reported secret hash, %x",
  3633  			secret, secretHash)
  3634  	}
  3635  
  3636  	t.dc.log.Infof("Notified of maker's redemption (%s: %v) and validated secret for order %v...",
  3637  		t.wallets.fromWallet.Symbol, coinIDString(t.wallets.fromWallet.AssetID, coinID), t.ID())
  3638  
  3639  	if match.Status < order.MakerRedeemed {
  3640  		if t.isMarketBuy() {
  3641  			t.unlockRefundFraction(1, uint64(len(t.matches)))
  3642  		} else {
  3643  			t.unlockRefundFraction(match.Quantity, t.Trade().Quantity)
  3644  		}
  3645  	}
  3646  
  3647  	match.Status = order.MakerRedeemed
  3648  	proof.MakerRedeem = coinID
  3649  	proof.Secret = secret
  3650  	return nil
  3651  }
  3652  
  3653  // Coins will be returned if
  3654  //   - the trade status is not OrderStatusEpoch or OrderStatusBooked, that is to
  3655  //     say, there won't be future matches for this order.
  3656  //   - there are no matches in the trade that MAY later require sending swaps,
  3657  //     that is to say, all matches have been either swapped or revoked.
  3658  //
  3659  // This method modifies match fields and MUST be called with the trackedTrade
  3660  // mutex lock held for writes.
  3661  func (t *trackedTrade) maybeReturnCoins() bool {
  3662  	// Status of the order itself.
  3663  	if t.metaData.Status < order.OrderStatusExecuted {
  3664  		// Booked and epoch orders may get matched any moment from
  3665  		// now, keep the coins locked.
  3666  		t.dc.log.Tracef("Not unlocking coins for order with status %s", t.metaData.Status)
  3667  		return false
  3668  	}
  3669  
  3670  	// Status of all matches for the order. If a match exists for
  3671  	// which a swap MAY be sent later, keep the coins locked.
  3672  	for _, match := range t.matches {
  3673  		if match.MetaData.Proof.IsRevoked() {
  3674  			// Won't be sending swap for this match regardless of
  3675  			// the match's status.
  3676  			continue
  3677  		}
  3678  
  3679  		status, side := match.Status, match.Side
  3680  		if side == order.Maker && status < order.MakerSwapCast ||
  3681  			side == order.Taker && status < order.TakerSwapCast {
  3682  			// Match is active (not revoked, not refunded) and client
  3683  			// is yet to execute swap. Keep coins locked.
  3684  			t.dc.log.Tracef("Not unlocking coins for order %v with match side %s, status %s", t.ID(), side, status)
  3685  			return false
  3686  		}
  3687  	}
  3688  
  3689  	// Safe to unlock coins now.
  3690  	t.returnCoins()
  3691  	return true
  3692  }
  3693  
  3694  // returnCoins unlocks this trade's funding coins (if unspent) or the change
  3695  // coin if a previous swap created a change coin that is locked.
  3696  // Coins are auto-unlocked once spent in a swap tx, including intermediate
  3697  // change coins, such that only the last change coin (if locked), will need
  3698  // to be unlocked.
  3699  //
  3700  // This method modifies match fields and MUST be called with the trackedTrade
  3701  // mutex lock held for writes.
  3702  func (t *trackedTrade) returnCoins() {
  3703  	if !t.wallets.fromWallet.connected() {
  3704  		t.dc.log.Warnf("Unable to return %s funding coins: %v", t.wallets.fromWallet.Symbol, errWalletNotConnected)
  3705  		return
  3706  	}
  3707  	if t.change == nil && t.coinsLocked {
  3708  		fundingCoins := make([]asset.Coin, 0, len(t.coins))
  3709  		for _, coin := range t.coins {
  3710  			fundingCoins = append(fundingCoins, coin)
  3711  		}
  3712  		err := t.wallets.fromWallet.ReturnCoins(fundingCoins)
  3713  		if err != nil {
  3714  			t.dc.log.Warnf("Unable to return %s funding coins: %v", t.wallets.fromWallet.Symbol, err)
  3715  		} else {
  3716  			t.coinsLocked = false
  3717  		}
  3718  		if returner, is := t.wallets.toWallet.Wallet.(asset.AddressReturner); is {
  3719  			returner.ReturnRedemptionAddress(t.Trade().Address)
  3720  		}
  3721  	} else if t.change != nil && t.changeLocked {
  3722  		err := t.wallets.fromWallet.ReturnCoins(asset.Coins{t.change})
  3723  		if err != nil {
  3724  			t.dc.log.Warnf("Unable to return %s change coin %v: %v", t.wallets.fromWallet.Symbol, t.change, err)
  3725  		} else {
  3726  			t.changeLocked = false
  3727  		}
  3728  	}
  3729  }
  3730  
  3731  // requiredForRemainingSwaps determines the amount of the from asset that is
  3732  // still needed in order initiate the remaining swaps in the order.
  3733  func (t *trackedTrade) requiredForRemainingSwaps() (uint64, error) {
  3734  	mkt := t.dc.marketConfig(t.mktID)
  3735  	if mkt == nil {
  3736  		return 0, fmt.Errorf("could not find market: %v", t.mktID)
  3737  	}
  3738  	lotSize := mkt.LotSize
  3739  
  3740  	accelWallet, ok := t.wallets.fromWallet.Wallet.(asset.Accelerator)
  3741  	if !ok {
  3742  		return 0, fmt.Errorf("the %s wallet is not an accelerator", t.wallets.fromWallet.Symbol)
  3743  	}
  3744  
  3745  	var requiredForRemainingSwaps uint64
  3746  	var maxSwapsRemaining uint64
  3747  
  3748  	if t.metaData.Status <= order.OrderStatusExecuted {
  3749  		if !t.Trade().Sell {
  3750  			requiredForRemainingSwaps += calc.BaseToQuote(t.rate(), t.Trade().Remaining())
  3751  		} else {
  3752  			requiredForRemainingSwaps += t.Trade().Remaining()
  3753  		}
  3754  		maxSwapsRemaining += t.Trade().Remaining() / lotSize
  3755  	}
  3756  
  3757  	for _, match := range t.matches {
  3758  		if (match.Side == order.Maker && match.Status < order.MakerSwapCast) ||
  3759  			(match.Side == order.Taker && match.Status < order.TakerSwapCast) {
  3760  			if !t.Trade().Sell {
  3761  				requiredForRemainingSwaps += calc.BaseToQuote(match.Rate, match.Quantity)
  3762  			} else {
  3763  				requiredForRemainingSwaps += match.Quantity
  3764  			}
  3765  			maxSwapsRemaining++
  3766  		}
  3767  	}
  3768  
  3769  	// Add the fees.
  3770  	requiredForRemainingSwaps += accelWallet.FeesForRemainingSwaps(maxSwapsRemaining, t.metaData.MaxFeeRate)
  3771  
  3772  	return requiredForRemainingSwaps, nil
  3773  }
  3774  
  3775  // orderAccelerationParameters returns the parameters needed to accelerate the
  3776  // swap transactions in this trade.
  3777  // MUST be called with the trackedTrade mutex held.
  3778  func (t *trackedTrade) orderAccelerationParameters() (swapCoins, accelerationCoins []dex.Bytes, changeCoin dex.Bytes, requiredForRemainingSwaps uint64, err error) {
  3779  	makeError := func(err error) ([]dex.Bytes, []dex.Bytes, dex.Bytes, uint64, error) {
  3780  		return nil, nil, nil, 0, err
  3781  	}
  3782  
  3783  	if t.metaData.ChangeCoin == nil {
  3784  		return makeError(fmt.Errorf("order does not have change which can be accelerated"))
  3785  	}
  3786  
  3787  	if len(t.metaData.AccelerationCoins) >= 10 {
  3788  		return makeError(fmt.Errorf("order has already been accelerated too many times"))
  3789  	}
  3790  
  3791  	requiredForRemainingSwaps, err = t.requiredForRemainingSwaps()
  3792  	if err != nil {
  3793  		return makeError(err)
  3794  	}
  3795  
  3796  	swapCoins = make([]dex.Bytes, 0, len(t.matches))
  3797  	for _, match := range t.matches {
  3798  		var swapCoinID order.CoinID
  3799  		if match.Side == order.Maker && match.Status >= order.MakerSwapCast {
  3800  			swapCoinID = match.MetaData.Proof.MakerSwap
  3801  		} else if match.Side == order.Taker && match.Status >= order.TakerSwapCast {
  3802  			swapCoinID = match.MetaData.Proof.TakerSwap
  3803  		} else {
  3804  			continue
  3805  		}
  3806  
  3807  		swapCoins = append(swapCoins, dex.Bytes(swapCoinID))
  3808  	}
  3809  
  3810  	if len(swapCoins) == 0 {
  3811  		return makeError(fmt.Errorf("cannot accelerate an order without any swaps"))
  3812  	}
  3813  
  3814  	accelerationCoins = make([]dex.Bytes, 0, len(t.metaData.AccelerationCoins))
  3815  	for _, coin := range t.metaData.AccelerationCoins {
  3816  		accelerationCoins = append(accelerationCoins, dex.Bytes(coin))
  3817  	}
  3818  
  3819  	return swapCoins, accelerationCoins, dex.Bytes(t.metaData.ChangeCoin), requiredForRemainingSwaps, nil
  3820  }
  3821  
  3822  func (t *trackedTrade) likelyTaker(midGap uint64) bool {
  3823  	if t.Type() == order.MarketOrderType {
  3824  		return true
  3825  	}
  3826  	lo := t.Order.(*order.LimitOrder)
  3827  	if lo.Force == order.ImmediateTiF {
  3828  		return true
  3829  	}
  3830  
  3831  	if midGap == 0 {
  3832  		return false
  3833  	}
  3834  
  3835  	if lo.Sell {
  3836  		return lo.Rate < midGap
  3837  	}
  3838  
  3839  	return lo.Rate > midGap
  3840  }
  3841  
  3842  func (t *trackedTrade) baseQty(midGap, lotSize uint64) uint64 {
  3843  	qty := t.Trade().Quantity
  3844  
  3845  	if t.Type() == order.MarketOrderType && !t.Trade().Sell {
  3846  		if midGap == 0 {
  3847  			qty = lotSize
  3848  		} else {
  3849  			qty = calc.QuoteToBase(midGap, qty)
  3850  		}
  3851  	}
  3852  
  3853  	return qty
  3854  }
  3855  
  3856  func (t *trackedTrade) epochWeight(midGap, lotSize uint64) uint64 {
  3857  	if t.status() >= order.OrderStatusBooked {
  3858  		return 0
  3859  	}
  3860  
  3861  	if t.likelyTaker(midGap) {
  3862  		return 2 * t.baseQty(midGap, lotSize)
  3863  	}
  3864  
  3865  	return t.baseQty(midGap, lotSize)
  3866  }
  3867  
  3868  func (t *trackedTrade) bookedWeight() uint64 {
  3869  	if t.status() != order.OrderStatusBooked {
  3870  		return 0
  3871  	}
  3872  
  3873  	return t.Trade().Remaining()
  3874  }
  3875  
  3876  func (t *trackedTrade) settlingWeight() (weight uint64) {
  3877  	for _, match := range t.matches {
  3878  		if (match.Side == order.Maker && match.Status >= order.MakerRedeemed) ||
  3879  			(match.Side == order.Taker && match.Status >= order.MatchComplete) {
  3880  			continue
  3881  		}
  3882  		weight += match.Quantity
  3883  	}
  3884  	return
  3885  }
  3886  
  3887  func (t *trackedTrade) isEpochOrder() bool {
  3888  	return t.status() == order.OrderStatusEpoch
  3889  }
  3890  
  3891  func (t *trackedTrade) marketWeight(midGap, lotSize uint64) uint64 {
  3892  	return t.epochWeight(midGap, lotSize) + t.bookedWeight() + t.settlingWeight()
  3893  }
  3894  
  3895  // mapifyCoins converts the slice of coins to a map keyed by hex coin ID.
  3896  func mapifyCoins(coins asset.Coins) map[string]asset.Coin {
  3897  	coinMap := make(map[string]asset.Coin, len(coins))
  3898  	for _, c := range coins {
  3899  		var cid string
  3900  		if rc, is := c.(asset.RecoveryCoin); is {
  3901  			// Account coins are keyed by a coin that includes
  3902  			// address and value. The ID only returns address.
  3903  			cid = hex.EncodeToString(rc.RecoveryID())
  3904  		} else {
  3905  			cid = hex.EncodeToString(c.ID())
  3906  		}
  3907  		coinMap[cid] = c
  3908  	}
  3909  	return coinMap
  3910  }
  3911  
  3912  func sellString(sell bool) string {
  3913  	if sell {
  3914  		return "sell"
  3915  	}
  3916  	return "buy"
  3917  }
  3918  
  3919  func applyFraction(num, denom, target uint64) uint64 {
  3920  	return uint64(math.Round(float64(num) / float64(denom) * float64(target)))
  3921  }