From bee4875039c2576e9e1be22adb2dd6c5f7c3e702 Mon Sep 17 00:00:00 2001 From: JoeGruff Date: Sun, 17 Nov 2024 18:21:22 +0900 Subject: [PATCH] core: Confirm refund txs. --- client/core/core.go | 44 ++++---- client/core/core_test.go | 198 ++++++++++++++++++------------------ client/core/notification.go | 17 +++- client/core/trade.go | 182 ++++++++++++++++++++++++++++++++- dex/order/match.go | 2 +- 5 files changed, 318 insertions(+), 125 deletions(-) diff --git a/client/core/core.go b/client/core/core.go index b3884390ea..233ae8d42c 100644 --- a/client/core/core.go +++ b/client/core/core.go @@ -10665,9 +10665,9 @@ func (c *Core) deleteRequestedAction(uniqueID string) { c.requestedActionMtx.Unlock() } -// handleRetryRedemptionAction handles a response to a user response to an -// ActionRequiredNote for a rejected redemption transaction. -func (c *Core) handleRetryRedemptionAction(actionB []byte) error { +// handleRetryTxAction handles a response to a user response to an +// ActionRequiredNote for a rejected redemption or refund transaction. +func (c *Core) handleRetryTxAction(actionB []byte, isRedeem bool) error { var req struct { OrderID dex.Bytes `json:"orderID"` CoinID dex.Bytes `json:"coinID"` @@ -10703,23 +10703,31 @@ func (c *Core) handleRetryRedemptionAction(actionB []byte) error { coinID = match.MetaData.Proof.MakerRedeem } if bytes.Equal(coinID, req.CoinID) { - if match.Side == order.Taker && match.Status == order.MatchComplete { - // Try to redeem again. - match.redemptionRejected = false - match.MetaData.Proof.TakerRedeem = nil - match.Status = order.MakerRedeemed - if err := c.db.UpdateMatch(&match.MetaMatch); err != nil { - c.log.Errorf("Failed to update match in DB: %v", err) + if isRedeem { + if match.Side == order.Taker && match.Status == order.MatchComplete { + // Try to redeem again. + match.redemptionRejected = false + match.MetaData.Proof.TakerRedeem = nil + match.Status = order.MakerRedeemed + if err := c.db.UpdateMatch(&match.MetaMatch); err != nil { + c.log.Errorf("Failed to update match in DB: %v", err) + } + } else if match.Side == order.Maker && match.Status == order.MakerRedeemed { + match.redemptionRejected = false + match.MetaData.Proof.MakerRedeem = nil + match.Status = order.TakerSwapCast + if err := c.db.UpdateMatch(&match.MetaMatch); err != nil { + c.log.Errorf("Failed to update match in DB: %v", err) + } + } else { + c.log.Errorf("Redemption retry attempted for order side %s status %s", match.Side, match.Status) } - } else if match.Side == order.Maker && match.Status == order.MakerRedeemed { - match.redemptionRejected = false - match.MetaData.Proof.MakerRedeem = nil - match.Status = order.TakerSwapCast + } else { + match.MetaData.Proof.RefundCoin = nil + match.refundRejected = false if err := c.db.UpdateMatch(&match.MetaMatch); err != nil { c.log.Errorf("Failed to update match in DB: %v", err) } - } else { - c.log.Errorf("Redemption retry attempted for order side %s status %s", match.Side, match.Status) } } } @@ -10731,7 +10739,9 @@ func (c *Core) handleRetryRedemptionAction(actionB []byte) error { func (c *Core) handleCoreAction(actionID string, actionB json.RawMessage) ( /* handled */ bool, error) { switch actionID { case ActionIDRedeemRejected: - return true, c.handleRetryRedemptionAction(actionB) + return true, c.handleRetryTxAction(actionB, true) + case ActionIDRefundRejected: + return true, c.handleRetryTxAction(actionB, false) } return false, nil } diff --git a/client/core/core_test.go b/client/core/core_test.go index d470d38b54..c280bf6294 100644 --- a/client/core/core_test.go +++ b/client/core/core_test.go @@ -716,9 +716,9 @@ type TXCWallet struct { findBond *asset.BondDetails findBondErr error - confirmRedemptionResult *asset.ConfirmRedemptionStatus - confirmRedemptionErr error - confirmRedemptionCalled bool + confirmTxResult *asset.ConfirmTxStatus + confirmTxErr error + confirmTxCalled bool estFee uint64 estFeeErr error @@ -795,9 +795,9 @@ func (w *TXCWallet) Balance() (*asset.Balance, error) { return w.bal, nil } -func (w *TXCWallet) ConfirmRedemption(coinID dex.Bytes, redemption *asset.Redemption, feeSuggestion uint64) (*asset.ConfirmRedemptionStatus, error) { - w.confirmRedemptionCalled = true - return w.confirmRedemptionResult, w.confirmRedemptionErr +func (w *TXCWallet) ConfirmTransaction(coinID dex.Bytes, confirmTx *asset.ConfirmTx, feeSuggestion uint64) (*asset.ConfirmTxStatus, error) { + w.confirmTxCalled = true + return w.confirmTxResult, w.confirmTxErr } func (w *TXCWallet) FundOrder(ord *asset.Order) (asset.Coins, []dex.Bytes, uint64, error) { @@ -4537,8 +4537,8 @@ func TestTradeTracking(t *testing.T) { btcWallet.address = "12DXGkvxFjuq5btXYkwWfBZaz1rVwFgini" btcWallet.Unlock(rig.crypter) - tBtcWallet.confirmRedemptionErr = errors.New("") - tDcrWallet.confirmRedemptionErr = errors.New("") + tBtcWallet.confirmTxErr = errors.New("") + tDcrWallet.confirmTxErr = errors.New("") matchSize := 4 * dcrBtcLotSize cancelledQty := dcrBtcLotSize @@ -8740,7 +8740,7 @@ func TestMatchStatusResolution(t *testing.T) { } } -func TestConfirmRedemption(t *testing.T) { +func TestConfirmTx(t *testing.T) { rig := newTestRig() defer rig.shutdown() dc := rig.dc @@ -8838,19 +8838,19 @@ func TestConfirmRedemption(t *testing.T) { } tests := []struct { - name string - matchStatus order.MatchStatus - matchSide order.MatchSide - expectedNotifications []*note - confirmRedemptionResult *asset.ConfirmRedemptionStatus - confirmRedemptionErr error - - expectConfirmRedemptionCalled bool - expectedStatus order.MatchStatus - expectTicksDelayed bool + name string + matchStatus order.MatchStatus + matchSide order.MatchSide + expectedNotifications []*note + confirmTxResult *asset.ConfirmTxStatus + confirmTxErr error + + expectConfirmTxCalled bool + expectedStatus order.MatchStatus + expectTicksDelayed bool }{ { - name: "maker, makerRedeemed, confirmedRedemption", + name: "maker, makerRedeemed, confirmedTx", matchStatus: order.MakerRedeemed, matchSide: order.Maker, expectedNotifications: []*note{ @@ -8859,13 +8859,13 @@ func TestConfirmRedemption(t *testing.T) { topic: TopicRedemptionConfirmed, }, }, - confirmRedemptionResult: &asset.ConfirmRedemptionStatus{ + confirmTxResult: &asset.ConfirmTxStatus{ Confs: 10, Req: 10, CoinID: tCoinID, }, - expectConfirmRedemptionCalled: true, - expectedStatus: order.MatchConfirmed, + expectConfirmTxCalled: true, + expectedStatus: order.MatchConfirmed, }, { name: "maker, makerRedeemed, confirmedRedemption, more confs than required", @@ -8877,13 +8877,13 @@ func TestConfirmRedemption(t *testing.T) { topic: TopicRedemptionConfirmed, }, }, - confirmRedemptionResult: &asset.ConfirmRedemptionStatus{ + confirmTxResult: &asset.ConfirmTxStatus{ Confs: 15, Req: 10, CoinID: tCoinID, }, - expectConfirmRedemptionCalled: true, - expectedStatus: order.MatchConfirmed, + expectConfirmTxCalled: true, + expectedStatus: order.MatchConfirmed, }, { name: "taker, matchComplete, confirmedRedemption", @@ -8895,13 +8895,13 @@ func TestConfirmRedemption(t *testing.T) { topic: TopicRedemptionConfirmed, }, }, - confirmRedemptionResult: &asset.ConfirmRedemptionStatus{ + confirmTxResult: &asset.ConfirmTxStatus{ Confs: 10, Req: 10, CoinID: tCoinID, }, - expectConfirmRedemptionCalled: true, - expectedStatus: order.MatchConfirmed, + expectConfirmTxCalled: true, + expectedStatus: order.MatchConfirmed, }, { name: "maker, makerRedeemed, incomplete", @@ -8913,13 +8913,13 @@ func TestConfirmRedemption(t *testing.T) { topic: TopicConfirms, }, }, - confirmRedemptionResult: &asset.ConfirmRedemptionStatus{ + confirmTxResult: &asset.ConfirmTxStatus{ Confs: 5, Req: 10, CoinID: tCoinID, }, - expectConfirmRedemptionCalled: true, - expectedStatus: order.MakerRedeemed, + expectConfirmTxCalled: true, + expectedStatus: order.MakerRedeemed, }, { name: "maker, makerRedeemed, replacedTx", @@ -8935,13 +8935,13 @@ func TestConfirmRedemption(t *testing.T) { topic: TopicConfirms, }, }, - confirmRedemptionResult: &asset.ConfirmRedemptionStatus{ + confirmTxResult: &asset.ConfirmTxStatus{ Confs: 0, Req: 10, CoinID: tUpdatedCoinID, }, - expectConfirmRedemptionCalled: true, - expectedStatus: order.MakerRedeemed, + expectConfirmTxCalled: true, + expectedStatus: order.MakerRedeemed, }, { name: "taker, matchComplete, replacedTx", @@ -8957,13 +8957,13 @@ func TestConfirmRedemption(t *testing.T) { topic: TopicConfirms, }, }, - confirmRedemptionResult: &asset.ConfirmRedemptionStatus{ + confirmTxResult: &asset.ConfirmTxStatus{ Confs: 0, Req: 10, CoinID: tUpdatedCoinID, }, - expectConfirmRedemptionCalled: true, - expectedStatus: order.MatchComplete, + expectConfirmTxCalled: true, + expectedStatus: order.MatchComplete, }, { // This case could happen if the dex was shut down right after @@ -8981,90 +8981,90 @@ func TestConfirmRedemption(t *testing.T) { topic: TopicRedemptionConfirmed, }, }, - confirmRedemptionResult: &asset.ConfirmRedemptionStatus{ + confirmTxResult: &asset.ConfirmTxStatus{ Confs: 10, Req: 10, CoinID: tUpdatedCoinID, }, - expectConfirmRedemptionCalled: true, - expectedStatus: order.MatchConfirmed, + expectConfirmTxCalled: true, + expectedStatus: order.MatchConfirmed, }, { - name: "maker, makerRedeemed, error", - matchStatus: order.MakerRedeemed, - matchSide: order.Maker, - confirmRedemptionErr: errors.New("err"), - expectedStatus: order.MakerRedeemed, - expectTicksDelayed: true, - expectConfirmRedemptionCalled: true, + name: "maker, makerRedeemed, error", + matchStatus: order.MakerRedeemed, + matchSide: order.Maker, + confirmTxErr: errors.New("err"), + expectedStatus: order.MakerRedeemed, + expectTicksDelayed: true, + expectConfirmTxCalled: true, }, { - name: "maker, makerRedeemed, swap refunded error", - matchStatus: order.MakerRedeemed, - matchSide: order.Maker, - confirmRedemptionErr: asset.ErrSwapRefunded, - expectedStatus: order.MatchConfirmed, + name: "maker, makerRedeemed, swap refunded error", + matchStatus: order.MakerRedeemed, + matchSide: order.Maker, + confirmTxErr: asset.ErrSwapRefunded, + expectedStatus: order.MatchConfirmed, expectedNotifications: []*note{ { severity: db.ErrorLevel, topic: TopicSwapRefunded, }, }, - expectConfirmRedemptionCalled: true, + expectConfirmTxCalled: true, }, { - name: "taker, takerRedeemed, redemption tx rejected error", - matchStatus: order.MatchComplete, - matchSide: order.Taker, - confirmRedemptionErr: asset.ErrTxRejected, - expectedStatus: order.MatchComplete, + name: "taker, takerRedeemed, redemption tx rejected error", + matchStatus: order.MatchComplete, + matchSide: order.Taker, + confirmTxErr: asset.ErrTxRejected, + expectedStatus: order.MatchComplete, expectedNotifications: []*note{ { severity: db.Data, topic: TopicRedeemRejected, }, }, - expectConfirmRedemptionCalled: true, + expectConfirmTxCalled: true, }, { - name: "maker, makerRedeemed, redemption tx lost", - matchStatus: order.MakerRedeemed, - matchSide: order.Maker, - confirmRedemptionErr: asset.ErrTxLost, - expectedStatus: order.TakerSwapCast, - expectConfirmRedemptionCalled: true, + name: "maker, makerRedeemed, redemption tx lost", + matchStatus: order.MakerRedeemed, + matchSide: order.Maker, + confirmTxErr: asset.ErrTxLost, + expectedStatus: order.TakerSwapCast, + expectConfirmTxCalled: true, }, { - name: "taker, takerRedeemed, redemption tx lost", - matchStatus: order.MatchComplete, - matchSide: order.Taker, - confirmRedemptionErr: asset.ErrTxLost, - expectedStatus: order.MakerRedeemed, - expectConfirmRedemptionCalled: true, + name: "taker, takerRedeemed, redemption tx lost", + matchStatus: order.MatchComplete, + matchSide: order.Taker, + confirmTxErr: asset.ErrTxLost, + expectedStatus: order.MakerRedeemed, + expectConfirmTxCalled: true, }, { - name: "maker, matchConfirmed", - matchStatus: order.MatchConfirmed, - matchSide: order.Maker, - expectedStatus: order.MatchConfirmed, - expectedNotifications: []*note{}, - expectConfirmRedemptionCalled: false, + name: "maker, matchConfirmed", + matchStatus: order.MatchConfirmed, + matchSide: order.Maker, + expectedStatus: order.MatchConfirmed, + expectedNotifications: []*note{}, + expectConfirmTxCalled: false, }, { - name: "maker, TakerSwapCast", - matchStatus: order.TakerSwapCast, - matchSide: order.Maker, - expectedStatus: order.TakerSwapCast, - expectedNotifications: []*note{}, - expectConfirmRedemptionCalled: false, + name: "maker, TakerSwapCast", + matchStatus: order.TakerSwapCast, + matchSide: order.Maker, + expectedStatus: order.TakerSwapCast, + expectedNotifications: []*note{}, + expectConfirmTxCalled: false, }, { - name: "taker, TakerSwapCast", - matchStatus: order.TakerSwapCast, - matchSide: order.Taker, - expectedStatus: order.TakerSwapCast, - expectedNotifications: []*note{}, - expectConfirmRedemptionCalled: false, + name: "taker, TakerSwapCast", + matchStatus: order.TakerSwapCast, + matchSide: order.Taker, + expectedStatus: order.TakerSwapCast, + expectedNotifications: []*note{}, + expectConfirmTxCalled: false, }, } @@ -9075,15 +9075,15 @@ func TestConfirmRedemption(t *testing.T) { setupMatch(test.matchStatus, test.matchSide) tracker.mtx.Unlock() - tBtcWallet.confirmRedemptionResult = test.confirmRedemptionResult - tBtcWallet.confirmRedemptionErr = test.confirmRedemptionErr - tBtcWallet.confirmRedemptionCalled = false + tBtcWallet.confirmTxResult = test.confirmTxResult + tBtcWallet.confirmTxErr = test.confirmTxErr + tBtcWallet.confirmTxCalled = false tCore.tickAsset(dc, tUTXOAssetB.ID) - if tBtcWallet.confirmRedemptionCalled != test.expectConfirmRedemptionCalled { + if tBtcWallet.confirmTxCalled != test.expectConfirmTxCalled { t.Fatalf("%s: expected confirm redemption to be called=%v but got=%v", - test.name, test.expectConfirmRedemptionCalled, tBtcWallet.confirmRedemptionCalled) + test.name, test.expectConfirmTxCalled, tBtcWallet.confirmTxCalled) } for _, expectedNotification := range test.expectedNotifications { @@ -9107,17 +9107,17 @@ func TestConfirmRedemption(t *testing.T) { } tracker.mtx.RLock() - if test.confirmRedemptionResult != nil { + if test.confirmTxResult != nil { var redeemCoin order.CoinID if test.matchSide == order.Maker { redeemCoin = match.MetaData.Proof.MakerRedeem } else { redeemCoin = match.MetaData.Proof.TakerRedeem } - if !bytes.Equal(redeemCoin, test.confirmRedemptionResult.CoinID) { - t.Fatalf("%s: expected coin %v != actual %v", test.name, test.confirmRedemptionResult.CoinID, redeemCoin) + if !bytes.Equal(redeemCoin, test.confirmTxResult.CoinID) { + t.Fatalf("%s: expected coin %v != actual %v", test.name, test.confirmTxResult.CoinID, redeemCoin) } - if test.confirmRedemptionResult.Confs >= test.confirmRedemptionResult.Req { + if test.confirmTxResult.Confs >= test.confirmTxResult.Req { if len(tDcrWallet.returnedContracts) != 1 || !bytes.Equal(ourContract, tDcrWallet.returnedContracts[0]) { t.Fatalf("%s: refund address not returned", test.name) } diff --git a/client/core/notification.go b/client/core/notification.go index f4d0f844fe..b6a590fc3f 100644 --- a/client/core/notification.go +++ b/client/core/notification.go @@ -435,8 +435,11 @@ const ( TopicCounterConfirms Topic = "CounterConfirms" TopicConfirms Topic = "Confirms" TopicRedemptionResubmitted Topic = "RedemptionResubmitted" + TopicRefundResubmitted Topic = "RefundResubmitted" TopicSwapRefunded Topic = "SwapRefunded" + TopicSwapRedeemed Topic = "SwapRefunded" TopicRedemptionConfirmed Topic = "RedemptionConfirmed" + TopicRefundConfirmed Topic = "RefundConfirmed" ) func newMatchNote(topic Topic, subject, details string, severity db.Severity, t *trackedTrade, match *matchTracker) *MatchNote { @@ -761,6 +764,8 @@ func newUnknownBondTierZeroNote(subject, details string) *db.Notification { const ( ActionIDRedeemRejected = "redeemRejected" TopicRedeemRejected = "RedeemRejected" + ActionIDRefundRejected = "refundRejected" + TopicRefundRejected = "RefundRejected" ) func newActionRequiredNote(actionID, uniqueID string, payload any) *asset.ActionRequiredNote { @@ -785,7 +790,7 @@ type RejectedRedemptionData struct { // an *asset.ActionRequiredNote. This is done for compatibility reasons. type ActionRequiredNote WalletNote -func newRejectedRedemptionNote(assetID uint32, oid order.OrderID, coinID []byte) (*asset.ActionRequiredNote, *ActionRequiredNote) { +func newRejectedTxNote(assetID uint32, oid order.OrderID, coinID []byte, txType asset.ConfirmTxType) (*asset.ActionRequiredNote, *ActionRequiredNote) { data := &RejectedRedemptionData{ AssetID: assetID, OrderID: oid[:], @@ -793,9 +798,15 @@ func newRejectedRedemptionNote(assetID uint32, oid order.OrderID, coinID []byte) CoinFmt: coinIDString(assetID, coinID), } uniqueID := dex.Bytes(coinID).String() - actionNote := newActionRequiredNote(ActionIDRedeemRejected, uniqueID, data) + actionID := ActionIDRedeemRejected + topic := db.Topic(TopicRedeemRejected) + if txType == asset.CTRefund { + actionID = ActionIDRefundRejected + topic = TopicRefundRejected + } + actionNote := newActionRequiredNote(actionID, uniqueID, data) coreNote := &ActionRequiredNote{ - Notification: db.NewNotification(NoteTypeActionRequired, TopicRedeemRejected, "", "", db.Data), + Notification: db.NewNotification(NoteTypeActionRequired, topic, "", "", db.Data), Payload: actionNote, } return actionNote, coreNote diff --git a/client/core/trade.go b/client/core/trade.go index e69489b8d0..5da0cfbff2 100644 --- a/client/core/trade.go +++ b/client/core/trade.go @@ -107,6 +107,18 @@ type matchTracker struct { // if we redeem as taker anyway. matchCompleteSent bool + // confirmRefundNumTries is just used for logging. + confirmRefundNumTries int + // refundConfs and refundConfsReq are updated while the refund + // confirmation process is running. Their values are not updated after the + // match reaches MatchConfirmed status. + refundConfs uint64 + refundConfsReq uint64 + // refundRejected will be true if a refund tx was rejected. A + // a rejected tx may indicate a serious internal issue, so we will seek + // user approval before replacing the tx. + refundRejected bool + // The fields below need to be modified without the parent trackedTrade's // mutex being write locked, so they have dedicated mutexes. @@ -1843,6 +1855,23 @@ func shouldConfirmRedemption(match *matchTracker) bool { return len(proof.TakerRedeem) > 0 } +// shouldConfirmRefund will return true if a refund transaction has been +// broadcast, but it has not yet been confirmed. +// +// This method accesses match fields and MUST be called with the trackedTrade +// mutex lock held for reads. +func shouldConfirmRefund(match *matchTracker) bool { + if match.Status == order.MatchConfirmed { + return false + } + + if match.refundRejected { + return false + } + + return len(match.MetaData.Proof.RefundCoin) > 0 +} + // tick will check for and perform any match actions necessary. func (c *Core) tick(t *trackedTrade) (assetMap, error) { assets := make(assetMap) // callers expect non-nil map even on error :( @@ -2974,6 +3003,23 @@ func (t *trackedTrade) redeemFee() uint64 { return feeSuggestion } +func (t *trackedTrade) refundFee() uint64 { + // Try not to use (*Core).feeSuggestion here, since it can incur an RPC + // request to the server. t.redeemFeeSuggestion is updated every tick and + // uses a rate directly from our wallet, if available. Only go looking for + // one if we don't have one cached. + var feeSuggestion uint64 + if _, is := t.accountRefunder(); is { + feeSuggestion = t.metaData.MaxFeeRate + } else { + feeSuggestion = t.redeemFeeSuggestion.get() + } + if feeSuggestion == 0 { + feeSuggestion = t.dc.bestBookFeeSuggestion(t.wallets.fromWallet.AssetID) + } + return feeSuggestion +} + // confirmRedemption attempts to confirm the redemptions for each match, and // then return any refund addresses that we won't be using. func (c *Core) confirmRedemptions(t *trackedTrade, matches []*matchTracker) { @@ -3043,10 +3089,8 @@ func (c *Core) confirmRedemption(t *trackedTrade, match *matchTracker) (bool, er match.confirmRedemptionNumTries++ - redemptionStatus, err := toWallet.Wallet.ConfirmRedemption(dex.Bytes(redeemCoinID), &asset.Redemption{ - Spends: match.counterSwap, - Secret: proof.Secret, - }, t.redeemFee()) + redemptionStatus, err := toWallet.Wallet.ConfirmTransaction(dex.Bytes(redeemCoinID), + asset.NewRedeemConfTx(match.counterSwap, proof.Secret), t.redeemFee()) switch { case err == nil: case errors.Is(err, asset.ErrSwapRefunded): @@ -3064,7 +3108,7 @@ func (c *Core) confirmRedemption(t *trackedTrade, match *matchTracker) (bool, er match.redemptionRejected = true // We need to seek user approval before trying again, since new fees // could be incurred. - actionRequest, note := newRejectedRedemptionNote(toWallet.AssetID, t.ID(), redeemCoinID) + actionRequest, note := newRejectedTxNote(toWallet.AssetID, t.ID(), redeemCoinID, asset.CTRedeem) t.notify(note) c.requestedActionMtx.Lock() c.requestedActions[dex.Bytes(redeemCoinID).String()] = actionRequest @@ -3138,6 +3182,134 @@ func (c *Core) confirmRedemption(t *trackedTrade, match *matchTracker) (bool, er return redemptionConfirmed, nil } +// confirmRefund checks if the user's refund has been confirmed, +// and if so, updates the match's status to MatchConfirmed. +// +// This method accesses match fields and MUST be called with the trackedTrade +// mutex lock held for writes. +func (c *Core) confirmRefund(t *trackedTrade, match *matchTracker) (bool, error) { + if confs := match.refundConfs; confs > 0 && confs >= match.refundConfsReq { // already there, stop checking + if match.Status == order.MatchConfirmed { + return true, nil + } + match.Status = order.MatchConfirmed + err := t.db.UpdateMatch(&match.MetaMatch) + if err != nil { + t.dc.log.Errorf("failed to update match in db: %v", err) + } + subject, details := t.formatDetails(TopicRefundConfirmed, match.token(), makeOrderToken(t.token())) + note := newMatchNote(TopicRefundConfirmed, subject, details, db.Success, t, match) + t.notify(note) + return true, nil + } + + // In some cases the wallet will need to send a new refund transaction. + fromWallet := t.wallets.fromWallet + + if err := fromWallet.checkPeersAndSyncStatus(); err != nil { + return false, err + } + + didUnlock, err := fromWallet.refreshUnlock() + if err != nil { // Just log it and try anyway. + t.dc.log.Errorf("refreshUnlock error checking refund %s: %v", fromWallet.Symbol, err) + } + if didUnlock { + t.dc.log.Warnf("Unexpected unlock needed for the %s wallet to check a refund", fromWallet.Symbol) + } + + proof := &match.MetaData.Proof + refundCoinID := proof.RefundCoin + secretHash := proof.SecretHash + var swapCoinID dex.Bytes + if match.Side == order.Maker { + swapCoinID = dex.Bytes(match.MetaData.Proof.MakerSwap) + } else { + swapCoinID = dex.Bytes(match.MetaData.Proof.TakerSwap) + } + contractToRefund := match.MetaData.Proof.ContractData + + match.confirmRedemptionNumTries++ + + refundStatus, err := fromWallet.Wallet.ConfirmTransaction(dex.Bytes(refundCoinID), + asset.NewRefundConfTx(swapCoinID, contractToRefund, secretHash), t.refundFee()) + switch { + case err == nil: + case errors.Is(err, asset.ErrSwapRedeemed): + subject, details := t.formatDetails(TopicSwapRedeemed, match.token(), makeOrderToken(t.token())) + note := newMatchNote(TopicSwapRedeemed, subject, details, db.ErrorLevel, t, match) + t.notify(note) + match.Status = order.MatchConfirmed + err := t.db.UpdateMatch(&match.MetaMatch) + if err != nil { + t.dc.log.Errorf("Failed to update match in db %v", err) + } + return false, errors.New("swap was already redeemed by the counterparty") + + case errors.Is(err, asset.ErrTxRejected): + match.refundRejected = true + // We need to seek user approval before trying again, since new fees + // could be incurred. + actionRequest, note := newRejectedTxNote(fromWallet.AssetID, t.ID(), refundCoinID, asset.CTRefund) + t.notify(note) + c.requestedActionMtx.Lock() + c.requestedActions[dex.Bytes(refundCoinID).String()] = actionRequest + c.requestedActionMtx.Unlock() + return false, fmt.Errorf("%s transaction %s was rejected. Seeking user approval before trying again", + unbip(fromWallet.AssetID), coinIDString(fromWallet.AssetID, refundCoinID)) + case errors.Is(err, asset.ErrTxLost): + // The transaction was nonce-replaced or otherwise lost without + // rejection or with user acknowlegement. Try again. + match.MetaData.Proof.RefundCoin = nil + c.log.Infof("Redemption %s (%s) has been noted as lost.", refundCoinID, unbip(fromWallet.AssetID)) + + if err := t.db.UpdateMatch(&match.MetaMatch); err != nil { + t.dc.log.Errorf("failed to update match after lost tx reported: %v", err) + } + return false, nil + default: + match.delayTicks(time.Minute * 15) + return false, fmt.Errorf("error confirming refund for coin %v. already tried %d times, will retry later: %v", + refundCoinID, match.confirmRefundNumTries, err) + } + + var refundResubmitted, refundConfirmed bool + if !bytes.Equal(refundCoinID, refundStatus.CoinID) { + refundResubmitted = true + match.MetaData.Proof.RefundCoin = order.CoinID(refundStatus.CoinID) + } + + match.refundConfs, match.refundConfsReq = refundStatus.Confs, refundStatus.Req + + if refundStatus.Confs >= refundStatus.Req { + refundConfirmed = true + match.Status = order.MatchConfirmed + } + + if refundResubmitted || refundConfirmed { + err := t.db.UpdateMatch(&match.MetaMatch) + if err != nil { + t.dc.log.Errorf("failed to update match in db: %v", err) + } + } + + if refundResubmitted { + subject, details := t.formatDetails(TopicRefundResubmitted, match.token(), makeOrderToken(t.token())) + note := newMatchNote(TopicRefundResubmitted, subject, details, db.WarningLevel, t, match) + t.notify(note) + } + + if refundConfirmed { + subject, details := t.formatDetails(TopicRefundConfirmed, match.token(), makeOrderToken(t.token())) + note := newMatchNote(TopicRefundConfirmed, subject, details, db.Success, t, match) + t.notify(note) + } else { + note := newMatchNote(TopicConfirms, "", "", db.Data, t, match) + t.notify(note) + } + return refundConfirmed, nil +} + // findMakersRedemption starts a goroutine to search for the redemption of // taker's contract. // diff --git a/dex/order/match.go b/dex/order/match.go index ebae50e0b3..1d144b9b9c 100644 --- a/dex/order/match.go +++ b/dex/order/match.go @@ -93,7 +93,7 @@ const ( // sent the details to the maker. MatchComplete // 4 // MatchConfirmed is a status used only by the client that represents - // that the user's redemption transaction has been confirmed. + // that the user's redemption or refund transaction has been confirmed. MatchConfirmed // 5 )