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 }