diff --git a/client/asset/btc/btc.go b/client/asset/btc/btc.go index 3a6b48ac2e..3b7f44ee97 100644 --- a/client/asset/btc/btc.go +++ b/client/asset/btc/btc.go @@ -82,10 +82,10 @@ const ( multiSplitBufferKey = "multisplitbuffer" redeemFeeBumpFee = "redeemfeebump" - // requiredRedeemConfirms is the amount of confirms a redeem transaction - // needs before the trade is considered confirmed. The redeem is + // requiredConfTxConfirms is the amount of confirms a redeem or refund + // transaction needs before the trade is considered confirmed. The tx is // monitored until this number of confirms is reached. - requiredRedeemConfirms = 1 + requiredConfTxConfirms = 1 ) const ( @@ -5995,7 +5995,7 @@ func (btc *intermediaryWallet) syncTxHistory(tip uint64) { if tx.BlockNumber > 0 && tip >= tx.BlockNumber { confs = tip - tx.BlockNumber + 1 } - if confs >= requiredRedeemConfirms { + if confs >= requiredConfTxConfirms { tx.Confirmed = true updated = true } @@ -6325,82 +6325,97 @@ func msgTxFromBytes(txB []byte) (*wire.MsgTx, error) { return deserializeMsgTx(bytes.NewReader(txB)) } -// ConfirmRedemption returns how many confirmations a redemption has. Normally -// this is very straightforward. However, with fluxuating fees, there's the +// ConfirmTransaction returns how many confirmations a redemption or refund has. +// Normally this is very straightforward. However, with fluctuating fees, there's the // possibility that the tx is never mined and eventually purged from the // mempool. In that case we use the provided fee suggestion to create and send // a new redeem transaction, returning the new transactions hash. -func (btc *baseWallet) ConfirmRedemption(coinID dex.Bytes, redemption *asset.Redemption, feeSuggestion uint64) (*asset.ConfirmRedemptionStatus, error) { +func (btc *baseWallet) ConfirmTransaction(coinID dex.Bytes, confirmTx *asset.ConfirmTx, feeSuggestion uint64) (*asset.ConfirmTxStatus, error) { txHash, _, err := decodeCoinID(coinID) if err != nil { return nil, err } _, confs, err := btc.rawWalletTx(txHash) - // redemption transaction found, return its confirms. + // Transaction found, return its confirms. // - // TODO: Investigate the case where this redeem has been sitting in the + // TODO: Investigate the case where this tx has been sitting in the // mempool for a long amount of time, possibly requiring some action by // us to get it unstuck. if err == nil { - return &asset.ConfirmRedemptionStatus{ + return &asset.ConfirmTxStatus{ Confs: uint64(confs), - Req: requiredRedeemConfirms, + Req: requiredConfTxConfirms, CoinID: coinID, }, nil } if !errors.Is(err, WalletTransactionNotFound) { - return nil, fmt.Errorf("problem searching for redemption transaction %s: %w", txHash, err) + return nil, fmt.Errorf("problem searching for %v transaction %s: %w", confirmTx.TxType(), txHash, err) } - // Redemption transaction is missing from the point of view of our node! - // Unlikely, but possible it was redeemed by another transaction. Check - // if the contract is still an unspent output. + // Redemption or refund transaction is missing from the point of view of + // our node! Unlikely, but possible it was spent by another transaction. + // Check if the contract is still an unspent output. - pkScript, err := btc.scriptHashScript(redemption.Spends.Contract) + pkScript, err := btc.scriptHashScript(confirmTx.Contract()) if err != nil { return nil, fmt.Errorf("error creating contract script: %w", err) } - swapHash, vout, err := decodeCoinID(redemption.Spends.Coin.ID()) + swapHash, vout, err := decodeCoinID(confirmTx.SpendsCoinID()) if err != nil { return nil, err } utxo, _, err := btc.node.getTxOut(swapHash, vout, pkScript, time.Now().Add(-ContractSearchLimit)) if err != nil { - return nil, fmt.Errorf("error finding unspent contract %s with swap hash %v vout %d: %w", redemption.Spends.Coin.ID(), swapHash, vout, err) + return nil, fmt.Errorf("error finding unspent contract %s with swap hash %v vout %d: %w", confirmTx.SpendsCoinID(), swapHash, vout, err) } if utxo == nil { // TODO: Spent, but by who. Find the spending tx. - btc.log.Warnf("Contract coin %v with swap hash %v vout %d spent by someone but not sure who.", redemption.Spends.Coin.ID(), swapHash, vout) + btc.log.Warnf("Contract coin %v with swap hash %v vout %d spent by someone but not sure who.", confirmTx.SpendsCoinID(), swapHash, vout) // Incorrect, but we will be in a loop of erroring if we don't // return something. - return &asset.ConfirmRedemptionStatus{ - Confs: requiredRedeemConfirms, - Req: requiredRedeemConfirms, + return &asset.ConfirmTxStatus{ + Confs: requiredConfTxConfirms, + Req: requiredConfTxConfirms, CoinID: coinID, }, nil } - // The contract has not yet been redeemed, but it seems the redeeming + // The contract has not yet been spent, but it seems the spending // tx has disappeared. Assume the fee was too low at the time and it - // was eventually purged from the mempool. Attempt to redeem again with + // was eventually purged from the mempool. Attempt to spend again with // a currently reasonable fee. + var newCoinID dex.Bytes + if confirmTx.IsRedeem() { + form := &asset.RedeemForm{ + Redemptions: []*asset.Redemption{ + { + Spends: confirmTx.Spends(), + Secret: confirmTx.Secret(), + }, + }, + FeeSuggestion: feeSuggestion, + } + _, coin, _, err := btc.Redeem(form) + if err != nil { + return nil, fmt.Errorf("unable to re-redeem %s with swap hash %v vout %d: %w", confirmTx.SpendsCoinID(), swapHash, vout, err) + } + newCoinID = coin.ID() + } else { + spendsCoinID := confirmTx.SpendsCoinID() + newCoinID, err = btc.Refund(spendsCoinID, confirmTx.Contract(), feeSuggestion) + if err != nil { + return nil, fmt.Errorf("unable to re-refund %s: %w", spendsCoinID, err) + } - form := &asset.RedeemForm{ - Redemptions: []*asset.Redemption{redemption}, - FeeSuggestion: feeSuggestion, - } - _, coin, _, err := btc.Redeem(form) - if err != nil { - return nil, fmt.Errorf("unable to re-redeem %s with swap hash %v vout %d: %w", redemption.Spends.Coin.ID(), swapHash, vout, err) } - return &asset.ConfirmRedemptionStatus{ + return &asset.ConfirmTxStatus{ Confs: 0, - Req: requiredRedeemConfirms, - CoinID: coin.ID(), + Req: requiredConfTxConfirms, + CoinID: newCoinID, }, nil } diff --git a/client/asset/btc/btc_test.go b/client/asset/btc/btc_test.go index b7d27b3404..94166f884b 100644 --- a/client/asset/btc/btc_test.go +++ b/client/asset/btc/btc_test.go @@ -5805,7 +5805,7 @@ func TestReconfigure(t *testing.T) { } } -func TestConfirmRedemption(t *testing.T) { +func TestConfirmTransaction(t *testing.T) { segwit := true wallet, node, shutdown := tNewWallet(segwit, walletTypeRPC) defer shutdown() @@ -5822,10 +5822,7 @@ func TestConfirmRedemption(t *testing.T) { Expiration: lockTime, } - redemption := &asset.Redemption{ - Spends: ci, - Secret: secret, - } + confirmTx := asset.NewRedeemConfTx(ci, secret) coinID := coin.ID() @@ -5841,7 +5838,7 @@ func TestConfirmRedemption(t *testing.T) { tests := []struct { name string - redemption *asset.Redemption + confirmTx *asset.ConfirmTx coinID []byte wantErr bool wantConfs uint64 @@ -5852,38 +5849,43 @@ func TestConfirmRedemption(t *testing.T) { }{{ name: "ok and found", coinID: coinID, - redemption: redemption, + confirmTx: confirmTx, getTransactionResult: new(GetTransactionResult), }, { - name: "ok spent by someone but not sure who", - coinID: coinID, - redemption: redemption, - wantConfs: requiredRedeemConfirms, + name: "ok spent by someone but not sure who", + coinID: coinID, + confirmTx: confirmTx, + wantConfs: requiredConfTxConfirms, }, { - name: "ok but sending new tx", - coinID: coinID, - redemption: redemption, - txOutRes: new(btcjson.GetTxOutResult), + name: "ok but sending new tx", + coinID: coinID, + confirmTx: confirmTx, + txOutRes: new(btcjson.GetTxOutResult), }, { - name: "decode coin error", - redemption: redemption, - wantErr: true, + name: "ok but sending new refund tx", + coinID: coinID, + confirmTx: asset.NewRefundConfTx(coin.ID(), contract, secret), + txOutRes: newTxOutResult(nil, 1e8, 2), }, { - name: "error finding contract output", - coinID: coinID, - redemption: redemption, - txOutErr: errors.New(""), - wantErr: true, + name: "decode coin error", + confirmTx: confirmTx, + wantErr: true, + }, { + name: "error finding contract output", + coinID: coinID, + confirmTx: confirmTx, + txOutErr: errors.New(""), + wantErr: true, }, { name: "error finding redeem tx", coinID: coinID, - redemption: redemption, + confirmTx: confirmTx, getTransactionErr: errors.New(""), wantErr: true, }, { - name: "redemption error", + name: "redeem error", coinID: coinID, - redemption: func() *asset.Redemption { + confirmTx: func() *asset.ConfirmTx { ci := &asset.AuditInfo{ Coin: coin, // Contract: contract, @@ -5891,13 +5893,16 @@ func TestConfirmRedemption(t *testing.T) { Expiration: lockTime, } - return &asset.Redemption{ - Spends: ci, - Secret: secret, - } + return asset.NewRedeemConfTx(ci, secret) }(), txOutRes: new(btcjson.GetTxOutResult), wantErr: true, + }, { + name: "refund error", + coinID: coinID, + confirmTx: asset.NewRefundConfTx(coin.ID(), contract, secret), + txOutRes: new(btcjson.GetTxOutResult), // fee too low + wantErr: true, }} for _, test := range tests { node.txOutRes = test.txOutRes @@ -5905,7 +5910,7 @@ func TestConfirmRedemption(t *testing.T) { node.getTransactionErr = test.getTransactionErr node.getTransactionMap[tTxID] = test.getTransactionResult - status, err := wallet.ConfirmRedemption(test.coinID, test.redemption, 0) + status, err := wallet.ConfirmTransaction(test.coinID, test.confirmTx, 0) if test.wantErr { if err == nil { t.Fatalf("%q: expected error", test.name) diff --git a/client/asset/btc/electrum.go b/client/asset/btc/electrum.go index feb11f239d..f5072e71e9 100644 --- a/client/asset/btc/electrum.go +++ b/client/asset/btc/electrum.go @@ -502,7 +502,7 @@ func (btc *ExchangeWalletElectrum) syncTxHistory(tip uint64) { if tx.BlockNumber > 0 && tip >= tx.BlockNumber { confs = tip - tx.BlockNumber + 1 } - if confs >= requiredRedeemConfirms { + if confs >= requiredConfTxConfirms { tx.Confirmed = true updated = true } diff --git a/client/asset/dcr/dcr.go b/client/asset/dcr/dcr.go index c0805a2944..ab46ca849e 100644 --- a/client/asset/dcr/dcr.go +++ b/client/asset/dcr/dcr.go @@ -92,11 +92,11 @@ const ( // past which fetchFeeFromOracle should be used to refresh the rate. freshFeeAge = time.Minute - // requiredRedeemConfirms is the amount of confirms a redeem transaction - // needs before the trade is considered confirmed. The redeem is + // requiredConfTxConfirms is the amount of confirms a redeem or refund + // transaction needs before the trade is considered confirmed. The tx is // monitored until this number of confirms is reached. Two to make sure - // the block containing the redeem is stakeholder-approved - requiredRedeemConfirms = 2 + // the block containing the tx is stakeholder-approved + requiredConfTxConfirms = 2 vspFileName = "vsp.json" @@ -119,12 +119,12 @@ var ( conventionalConversionFactor = float64(dexdcr.UnitInfo.Conventional.ConversionFactor) walletBlockAllowance = time.Second * 10 - // maxRedeemMempoolAge is the max amount of time the wallet will let a - // redeem transaction sit in mempool from the time it is first seen + // maxMempoolAge is the max amount of time the wallet will let a + // redeem or refund transaction sit in mempool from the time it is first seen // until it attempts to abandon it and try to send a new transaction. // This is necessary because transactions with already spent inputs may // be tried over and over with wallet in SPV mode. - maxRedeemMempoolAge = time.Hour * 2 + maxMempoolAge = time.Hour * 2 walletOpts = []*asset.ConfigOption{ { @@ -600,9 +600,11 @@ type exchangeWalletConfig struct { apiFeeFallback bool } -type mempoolRedeem struct { +// mempoolTx holds a refund or redeem. +type mempoolTx struct { txHash chainhash.Hash firstSeen time.Time + txType asset.ConfirmTxType } // vsp holds info needed for purchasing tickets from a vsp. PubKey is from the @@ -656,9 +658,9 @@ type ExchangeWallet struct { externalTxMtx sync.RWMutex externalTxCache map[chainhash.Hash]*externalTx - // TODO: Consider persisting mempool redeems on file. - mempoolRedeemsMtx sync.RWMutex - mempoolRedeems map[[32]byte]*mempoolRedeem // keyed by secret hash + // TODO: Consider persisting mempool txs on file. + mempoolTxsMtx sync.RWMutex + mempoolTxs map[[32]byte]*mempoolTx // keyed by secret hash vspV atomic.Value // *vsp @@ -844,7 +846,7 @@ func unconnectedWallet(cfg *asset.WalletConfig, dcrCfg *walletConfig, chainParam findRedemptionQueue: make(map[outPoint]*findRedemptionReq), externalTxCache: make(map[chainhash.Hash]*externalTx), oracleFees: make(map[uint64]feeStamped), - mempoolRedeems: make(map[[32]byte]*mempoolRedeem), + mempoolTxs: make(map[[32]byte]*mempoolTx), vspFilepath: vspFilepath, walletType: cfg.Type, subsidyCache: blockchain.NewSubsidyCache(chainParams), @@ -3324,14 +3326,14 @@ func (dcr *ExchangeWallet) Redeem(form *asset.RedeemForm) ([]dex.Bytes, asset.Co }, txHash, true) coinIDs := make([]dex.Bytes, 0, len(form.Redemptions)) - dcr.mempoolRedeemsMtx.Lock() + dcr.mempoolTxsMtx.Lock() for i := range form.Redemptions { coinIDs = append(coinIDs, toCoinID(txHash, uint32(i))) var secretHash [32]byte copy(secretHash[:], form.Redemptions[i].Spends.SecretHash) - dcr.mempoolRedeems[secretHash] = &mempoolRedeem{txHash: *txHash, firstSeen: time.Now()} + dcr.mempoolTxs[secretHash] = &mempoolTx{txHash: *txHash, firstSeen: time.Now(), txType: asset.CTRedeem} } - dcr.mempoolRedeemsMtx.Unlock() + dcr.mempoolTxsMtx.Unlock() return coinIDs, newOutput(txHash, 0, uint64(txOut.Value), wire.TxTreeRegular), fee, nil } @@ -7031,9 +7033,9 @@ func (dcr *ExchangeWallet) TxHistory(n int, refID *string, past bool) ([]*asset. return txHistoryDB.GetTxs(n, refID, past) } -// ConfirmRedemption returns how many confirmations a redemption has. Normally -// this is very straightforward. However there are two situations that have come -// up that this also handles. One is when the wallet can not find the redemption +// ConfirmTransaction returns how many confirmations a redemption or refund has. +// Normally this is very straightforward. However there are two situations that +// have come up that this also handles. One is when the wallet can not find the // transaction. This is most likely because the fee was set too low and the tx // was removed from the mempool. In the case where it is not found, this will // send a new tx using the provided fee suggestion. The second situation @@ -7042,74 +7044,72 @@ func (dcr *ExchangeWallet) TxHistory(n int, refID *string, past bool) ([]*asset. // mode and the transaction inputs having been spent by another transaction. The // wallet will not pick up on this so we could tell it to abandon the original // transaction and, again, send a new one using the provided feeSuggestion, but -// only warning for now. This method should not be run for the same redemption -// concurrently as it need to watch a new redeem transaction before finishing. -func (dcr *ExchangeWallet) ConfirmRedemption(coinID dex.Bytes, redemption *asset.Redemption, feeSuggestion uint64) (*asset.ConfirmRedemptionStatus, error) { +// only warning for now. This method should not be run for the same tx concurrently. +func (dcr *ExchangeWallet) ConfirmTransaction(coinID dex.Bytes, confirmTx *asset.ConfirmTx, feeSuggestion uint64) (*asset.ConfirmTxStatus, error) { txHash, _, err := decodeCoinID(coinID) if err != nil { return nil, err } var secretHash [32]byte - copy(secretHash[:], redemption.Spends.SecretHash) - dcr.mempoolRedeemsMtx.RLock() - mRedeem, have := dcr.mempoolRedeems[secretHash] - dcr.mempoolRedeemsMtx.RUnlock() + copy(secretHash[:], confirmTx.SecretHash()) + dcr.mempoolTxsMtx.RLock() + mTx, have := dcr.mempoolTxs[secretHash] + dcr.mempoolTxsMtx.RUnlock() - var deleteMempoolRedeem bool + var deleteMempoolTx bool defer func() { - if deleteMempoolRedeem { - dcr.mempoolRedeemsMtx.Lock() - delete(dcr.mempoolRedeems, secretHash) - dcr.mempoolRedeemsMtx.Unlock() + if deleteMempoolTx { + dcr.mempoolTxsMtx.Lock() + delete(dcr.mempoolTxs, secretHash) + dcr.mempoolTxsMtx.Unlock() } }() tx, err := dcr.wallet.GetTransaction(dcr.ctx, txHash) if err != nil && !errors.Is(err, asset.CoinNotFoundError) { - return nil, fmt.Errorf("problem searching for redemption transaction %s: %w", txHash, err) + return nil, fmt.Errorf("problem searching for %s transaction %s: %w", confirmTx.TxType(), txHash, err) } if err == nil { - if have && mRedeem.txHash == *txHash { - if tx.Confirmations == 0 && time.Now().After(mRedeem.firstSeen.Add(maxRedeemMempoolAge)) { + if have && mTx.txHash == *txHash { + if tx.Confirmations == 0 && time.Now().After(mTx.firstSeen.Add(maxMempoolAge)) { // Transaction has been sitting in the mempool // for a long time now. // // TODO: Consider abandoning. - redeemAge := time.Since(mRedeem.firstSeen) - dcr.log.Warnf("Redemption transaction %v has been in the mempool for %v which is too long.", txHash, redeemAge) + txAge := time.Since(mTx.firstSeen) + dcr.log.Warnf("%s transaction %v has been in the mempool for %v which is too long.", confirmTx.TxType(), txHash, txAge) } } else { if have { // This should not happen. Core has told us to - // watch a new redeem with a different transaction - // hash for a trade we were already watching. - return nil, fmt.Errorf("tx were were watching %s for redeem with secret hash %x being "+ - "replaced by tx %s. core should not be replacing the transaction. maybe ConfirmRedemption "+ - "is being run concurrently for the same redeem", mRedeem.txHash, secretHash, *txHash) + // watch a new redeem or refund with a different + // transaction hash for a trade we were already watching. + return nil, fmt.Errorf("tx were were watching %s for %s with secret hash %x being "+ + "replaced by tx %s. core should not be replacing the transaction. maybe ConfirmTransaction "+ + "is being run concurrently for the same tx", mTx.txHash, confirmTx.TxType(), secretHash, *txHash) } // Will hit this if bisonw was restarted with an actively - // redeeming swap. - dcr.mempoolRedeemsMtx.Lock() - dcr.mempoolRedeems[secretHash] = &mempoolRedeem{txHash: *txHash, firstSeen: time.Now()} - dcr.mempoolRedeemsMtx.Unlock() + // redeeming or maybe refunding swap. + dcr.mempoolTxsMtx.Lock() + dcr.mempoolTxs[secretHash] = &mempoolTx{txHash: *txHash, firstSeen: time.Now(), txType: confirmTx.TxType()} + dcr.mempoolTxsMtx.Unlock() } - if tx.Confirmations >= requiredRedeemConfirms { - deleteMempoolRedeem = true + if tx.Confirmations >= requiredConfTxConfirms { + deleteMempoolTx = true } - return &asset.ConfirmRedemptionStatus{ + return &asset.ConfirmTxStatus{ Confs: uint64(tx.Confirmations), - Req: requiredRedeemConfirms, + Req: requiredConfTxConfirms, CoinID: coinID, }, nil } - // Redemption transaction is missing from the point of view of our wallet! - // Unlikely, but possible it was redeemed by another transaction. We - // assume a contract past its locktime cannot make it here, so it must - // not be refunded. Check if the contract is still an unspent output. + // Transaction is missing from the point of view of our wallet! + // Unlikely, but possible it was spent by another transaction.Check if + // the contract is still an unspent output. - swapHash, vout, err := decodeCoinID(redemption.Spends.Coin.ID()) + swapHash, vout, err := decodeCoinID(confirmTx.SpendsCoinID()) if err != nil { return nil, err } @@ -7122,7 +7122,7 @@ func (dcr *ExchangeWallet) ConfirmRedemption(coinID dex.Bytes, redemption *asset switch spentStatus { case -1, 1: // First find the block containing the output itself. - scriptAddr, err := stdaddr.NewAddressScriptHashV0(redemption.Spends.Contract, dcr.chainParams) + scriptAddr, err := stdaddr.NewAddressScriptHashV0(confirmTx.Contract(), dcr.chainParams) if err != nil { return nil, fmt.Errorf("error encoding contract address: %w", err) } @@ -7171,59 +7171,73 @@ func (dcr *ExchangeWallet) ConfirmRedemption(coinID dex.Bytes, redemption *asset } confs := uint64(height - block.height) hash := spendTx.TxHash() - if confs < requiredRedeemConfirms { - dcr.mempoolRedeemsMtx.Lock() - dcr.mempoolRedeems[secretHash] = &mempoolRedeem{txHash: hash, firstSeen: time.Now()} - dcr.mempoolRedeemsMtx.Unlock() + if confs < requiredConfTxConfirms { + dcr.mempoolTxsMtx.Lock() + dcr.mempoolTxs[secretHash] = &mempoolTx{txHash: hash, firstSeen: time.Now(), txType: confirmTx.TxType()} + dcr.mempoolTxsMtx.Unlock() } - return &asset.ConfirmRedemptionStatus{ + return &asset.ConfirmTxStatus{ Confs: confs, - Req: requiredRedeemConfirms, + Req: requiredConfTxConfirms, CoinID: toCoinID(&hash, uint32(vin)), }, nil } - dcr.log.Warnf("Contract coin %v spent by someone but not sure who.", redemption.Spends.Coin.ID()) + dcr.log.Warnf("Contract coin %v spent by someone but not sure who.", confirmTx.SpendsCoinID()) // Incorrect, but we will be in a loop of erroring if we don't // return something. We were unable to find the spender for some // reason. // May be still in the map if abandonTx failed. - deleteMempoolRedeem = true + deleteMempoolTx = true - return &asset.ConfirmRedemptionStatus{ - Confs: requiredRedeemConfirms, - Req: requiredRedeemConfirms, + return &asset.ConfirmTxStatus{ + Confs: requiredConfTxConfirms, + Req: requiredConfTxConfirms, CoinID: coinID, }, nil } - // The contract has not yet been redeemed, but it seems the redeeming - // tx has disappeared. Assume the fee was too low at the time and it - // was eventually purged from the mempool. Attempt to redeem again with - // a currently reasonable fee. + // The contract has not yet been redeemed or refunded, but it seems the + // spending tx has disappeared. Assume the fee was too low at the time + // and it was eventually purged from the mempool. Attempt to spend again + // with a currently reasonable fee. - form := &asset.RedeemForm{ - Redemptions: []*asset.Redemption{redemption}, - FeeSuggestion: feeSuggestion, - } - _, coin, _, err := dcr.Redeem(form) - if err != nil { - return nil, fmt.Errorf("unable to re-redeem %s: %w", redemption.Spends.Coin.ID(), err) + var newCoinID dex.Bytes + if confirmTx.IsRedeem() { + form := &asset.RedeemForm{ + Redemptions: []*asset.Redemption{ + { + Spends: confirmTx.Spends(), + Secret: confirmTx.Secret(), + }, + }, + FeeSuggestion: feeSuggestion, + } + _, coin, _, err := dcr.Redeem(form) + if err != nil { + return nil, fmt.Errorf("unable to re-redeem %s: %w", confirmTx.SpendsCoinID(), err) + } + newCoinID = coin.ID() + } else { + spendsCoinID := confirmTx.SpendsCoinID() + newCoinID, err = dcr.Refund(spendsCoinID, confirmTx.Contract(), feeSuggestion) + if err != nil { + return nil, fmt.Errorf("unable to re-refund %s: %w", spendsCoinID, err) + } } - coinID = coin.ID() - newRedeemHash, _, err := decodeCoinID(coinID) + newTxHash, _, err := decodeCoinID(newCoinID) if err != nil { return nil, err } - dcr.mempoolRedeemsMtx.Lock() - dcr.mempoolRedeems[secretHash] = &mempoolRedeem{txHash: *newRedeemHash, firstSeen: time.Now()} - dcr.mempoolRedeemsMtx.Unlock() + dcr.mempoolTxsMtx.Lock() + dcr.mempoolTxs[secretHash] = &mempoolTx{txHash: *newTxHash, firstSeen: time.Now(), txType: confirmTx.TxType()} + dcr.mempoolTxsMtx.Unlock() - return &asset.ConfirmRedemptionStatus{ + return &asset.ConfirmTxStatus{ Confs: 0, - Req: requiredRedeemConfirms, + Req: requiredConfTxConfirms, CoinID: coinID, }, nil } diff --git a/client/asset/dcr/dcr_test.go b/client/asset/dcr/dcr_test.go index 7caf8e9dd9..b4f08007d4 100644 --- a/client/asset/dcr/dcr_test.go +++ b/client/asset/dcr/dcr_test.go @@ -4197,7 +4197,7 @@ func TestEstimateSendTxFee(t *testing.T) { } } -func TestConfirmRedemption(t *testing.T) { +func TestConfirmTransaction(t *testing.T) { wallet, node, shutdown := tNewWallet() defer shutdown() @@ -4207,6 +4207,7 @@ func TestConfirmRedemption(t *testing.T) { lockTime := time.Now().Add(time.Hour * 12) addr := tPKHAddr.String() + node.newAddr = tPKHAddr contract, err := dexdcr.MakeContract(addr, addr, secretHash[:], lockTime.Unix(), tChainParams) if err != nil { t.Fatalf("error making swap contract: %v", err) @@ -4272,10 +4273,7 @@ func TestConfirmRedemption(t *testing.T) { SecretHash: secretHash[:], } - redemption := &asset.Redemption{ - Spends: ci, - Secret: secret, - } + confirmTx := asset.NewRedeemConfTx(ci, secret) coinID := coin.ID() // Inverting the first byte. @@ -4283,56 +4281,62 @@ func TestConfirmRedemption(t *testing.T) { tests := []struct { name string - redemption *asset.Redemption + confirmTx *asset.ConfirmTx coinID []byte wantErr bool bestBlockErr error txRes func() (*walletjson.GetTransactionResult, error) wantConfs uint64 - mempoolRedeems map[[32]byte]*mempoolRedeem + mempoolTxs map[[32]byte]*mempoolTx txOutRes map[outPoint]*chainjson.GetTxOutResult unspentOutputErr error }{{ - name: "ok tx never seen before now", + name: "ok tx never seen before now", + coinID: coinID, + confirmTx: confirmTx, + txRes: txFn([]bool{false}), + }, { + name: "ok tx in map", coinID: coinID, - redemption: redemption, + confirmTx: confirmTx, + txRes: txFn([]bool{false}), + mempoolTxs: map[[32]byte]*mempoolTx{secretHash: {txHash: txHash, firstSeen: time.Now(), txType: asset.CTRedeem}}, + }, { + name: "tx in map has different hash than coin id", + coinID: badCoinID, + confirmTx: confirmTx, txRes: txFn([]bool{false}), + mempoolTxs: map[[32]byte]*mempoolTx{secretHash: {txHash: txHash, firstSeen: time.Now(), txType: asset.CTRedeem}}, + wantErr: true, }, { - name: "ok tx in map", - coinID: coinID, - redemption: redemption, - txRes: txFn([]bool{false}), - mempoolRedeems: map[[32]byte]*mempoolRedeem{secretHash: {txHash: txHash, firstSeen: time.Now()}}, + name: "ok tx not found new tx", + coinID: coinID, + confirmTx: confirmTx, + txRes: txFn([]bool{true, false}), + txOutRes: map[outPoint]*chainjson.GetTxOutResult{newOutPoint(&txHash, 0): makeGetTxOutRes(0, 5, nil)}, }, { - name: "tx in map has different hash than coin id", - coinID: badCoinID, - redemption: redemption, - txRes: txFn([]bool{false}), - mempoolRedeems: map[[32]byte]*mempoolRedeem{secretHash: {txHash: txHash, firstSeen: time.Now()}}, - wantErr: true, + name: "ok refund tx not found new tx", + coinID: coinID, + confirmTx: asset.NewRefundConfTx(coin.ID(), contract, secret), + txRes: txFn([]bool{true, false}), + txOutRes: map[outPoint]*chainjson.GetTxOutResult{newOutPoint(&txHash, 0): makeGetTxOutRes(0, 5, nil)}, }, { - name: "ok tx not found spent new tx", + name: "ok old tx should maybe be abandoned", coinID: coinID, - redemption: redemption, + confirmTx: confirmTx, txRes: txFn([]bool{false}), - txOutRes: map[outPoint]*chainjson.GetTxOutResult{newOutPoint(&txHash, 0): makeGetTxOutRes(0, 5, nil)}, + mempoolTxs: map[[32]byte]*mempoolTx{secretHash: {txHash: txHash, firstSeen: time.Now().Add(-maxMempoolAge - time.Second), txType: asset.CTRedeem}}, }, { - name: "ok old tx should maybe be abandoned", - coinID: coinID, - redemption: redemption, - txRes: txFn([]bool{false}), - mempoolRedeems: map[[32]byte]*mempoolRedeem{secretHash: {txHash: txHash, firstSeen: time.Now().Add(-maxRedeemMempoolAge - time.Second)}}, - }, { - name: "ok and spent", - coinID: coinID, - txRes: txFn([]bool{true, false}), - redemption: redemption, - wantConfs: 1, // one confirm because this tx is in the best block + name: "ok and spent", + coinID: coinID, + txRes: txFn([]bool{true, false}), + confirmTx: confirmTx, + wantConfs: 1, // one confirm because this tx is in the best block }, { name: "ok and spent but we dont know who spent it", coinID: coinID, txRes: txFn([]bool{true, false}), - redemption: func() *asset.Redemption { + confirmTx: func() *asset.ConfirmTx { ci := &asset.AuditInfo{ Coin: coin, Contract: contract, @@ -4340,30 +4344,27 @@ func TestConfirmRedemption(t *testing.T) { Expiration: lockTime, SecretHash: make([]byte, 32), // fake secret hash } - return &asset.Redemption{ - Spends: ci, - Secret: secret, - } + return asset.NewRedeemConfTx(ci, secret) }(), - wantConfs: requiredRedeemConfirms, + wantConfs: requiredConfTxConfirms, }, { - name: "get transaction error", - coinID: coinID, - redemption: redemption, - txRes: txFn([]bool{true, true}), - wantErr: true, + name: "get transaction error", + coinID: coinID, + confirmTx: confirmTx, + txRes: txFn([]bool{true, true}), + wantErr: true, }, { - name: "decode coin error", - coinID: nil, - redemption: redemption, - txRes: txFn([]bool{true, false}), - wantErr: true, + name: "decode coin error", + coinID: nil, + confirmTx: confirmTx, + txRes: txFn([]bool{true, false}), + wantErr: true, }, { name: "redeem error", coinID: coinID, txOutRes: map[outPoint]*chainjson.GetTxOutResult{newOutPoint(&txHash, 0): makeGetTxOutRes(0, 5, nil)}, txRes: txFn([]bool{true, false}), - redemption: func() *asset.Redemption { + confirmTx: func() *asset.ConfirmTx { ci := &asset.AuditInfo{ Coin: coin, // Contract: contract, @@ -4371,25 +4372,29 @@ func TestConfirmRedemption(t *testing.T) { Expiration: lockTime, SecretHash: secretHash[:], } - return &asset.Redemption{ - Spends: ci, - Secret: secret, - } + return asset.NewRedeemConfTx(ci, secret) }(), wantErr: true, + }, { + name: "refund error", + coinID: coinID, + txOutRes: map[outPoint]*chainjson.GetTxOutResult{newOutPoint(&txHash, 0): makeGetTxOutRes(0, 5, nil)}, + txRes: txFn([]bool{true, false}), + confirmTx: asset.NewRefundConfTx(coin.ID(), nil, secret), + wantErr: true, }} for _, test := range tests { node.walletTxFn = test.txRes node.bestBlockErr = test.bestBlockErr - wallet.mempoolRedeems = test.mempoolRedeems - if wallet.mempoolRedeems == nil { - wallet.mempoolRedeems = make(map[[32]byte]*mempoolRedeem) + wallet.mempoolTxs = test.mempoolTxs + if wallet.mempoolTxs == nil { + wallet.mempoolTxs = make(map[[32]byte]*mempoolTx) } node.txOutRes = test.txOutRes if node.txOutRes == nil { node.txOutRes = make(map[outPoint]*chainjson.GetTxOutResult) } - status, err := wallet.ConfirmRedemption(test.coinID, test.redemption, 0) + status, err := wallet.ConfirmTransaction(test.coinID, test.confirmTx, 0) if test.wantErr { if err == nil { t.Fatalf("%q: expected error", test.name) diff --git a/client/asset/eth/eth.go b/client/asset/eth/eth.go index 8eef685728..abca514de5 100644 --- a/client/asset/eth/eth.go +++ b/client/asset/eth/eth.go @@ -3733,34 +3733,34 @@ func (eth *ETHWallet) checkForNewBlocks(ctx context.Context) { } } -// ConfirmRedemption checks the status of a redemption. If a transaction has -// been fee-replaced, the caller is notified of this by having a different -// coinID in the returned asset.ConfirmRedemptionStatus as was used to call the +// ConfirmTransaction checks the status of a redemption or refund. If a tx +// has been fee-replaced, the caller is notified of this by having a different +// coinID in the returned asset.ConfirmTxStatus as was used to call the // function. Fee argument is ignored since it is calculated from the best // header. -func (w *ETHWallet) ConfirmRedemption(coinID dex.Bytes, redemption *asset.Redemption, _ uint64) (*asset.ConfirmRedemptionStatus, error) { - return w.confirmRedemption(coinID, redemption) +func (w *ETHWallet) ConfirmTransaction(coinID dex.Bytes, confirmTx *asset.ConfirmTx, _ uint64) (*asset.ConfirmTxStatus, error) { + return w.confirmTransaction(coinID, confirmTx) } -// ConfirmRedemption checks the status of a redemption. If a transaction has -// been fee-replaced, the caller is notified of this by having a different -// coinID in the returned asset.ConfirmRedemptionStatus as was used to call the +// ConfirmTransaction checks the status of a redemption or refund. If a tx +// has been fee-replaced, the caller is notified of this by having a different +// coinID in the returned asset.ConfirmTxStatus as was used to call the // function. Fee argument is ignored since it is calculated from the best // header. -func (w *TokenWallet) ConfirmRedemption(coinID dex.Bytes, redemption *asset.Redemption, _ uint64) (*asset.ConfirmRedemptionStatus, error) { - return w.confirmRedemption(coinID, redemption) +func (w *TokenWallet) ConfirmTransaction(coinID dex.Bytes, confirmTx *asset.ConfirmTx, _ uint64) (*asset.ConfirmTxStatus, error) { + return w.confirmTransaction(coinID, confirmTx) } -func confStatus(confs, req uint64, txHash common.Hash) *asset.ConfirmRedemptionStatus { - return &asset.ConfirmRedemptionStatus{ +func confStatus(confs, req uint64, txHash common.Hash) *asset.ConfirmTxStatus { + return &asset.ConfirmTxStatus{ Confs: confs, Req: req, CoinID: txHash[:], } } -// confirmRedemption checks the confirmation status of a redemption transaction. -func (w *assetWallet) confirmRedemption(coinID dex.Bytes, redemption *asset.Redemption) (*asset.ConfirmRedemptionStatus, error) { +// confirmTransaction checks the confirmation status of a transaction. +func (w *assetWallet) confirmTransaction(coinID dex.Bytes, confirmTx *asset.ConfirmTx) (*asset.ConfirmTxStatus, error) { if len(coinID) != common.HashLength { return nil, fmt.Errorf("expected coin ID to be a transaction hash, but it has a length of %d", len(coinID)) @@ -3768,7 +3768,7 @@ func (w *assetWallet) confirmRedemption(coinID dex.Bytes, redemption *asset.Rede var txHash common.Hash copy(txHash[:], coinID) - contractVer, secretHash, err := dexeth.DecodeContractData(redemption.Spends.Contract) + contractVer, secretHash, err := dexeth.DecodeContractData(confirmTx.Contract()) if err != nil { return nil, fmt.Errorf("failed to decode contract data: %w", err) } @@ -3786,7 +3786,7 @@ func (w *assetWallet) confirmRedemption(coinID dex.Bytes, redemption *asset.Rede } } - var confirmStatus *asset.ConfirmRedemptionStatus + var confirmStatus *asset.ConfirmTxStatus if s.blockNum != 0 && s.blockNum <= tip { confirmStatus = confStatus(tip-s.blockNum+1, w.finalizeConfs, txHash) } else { @@ -3827,15 +3827,25 @@ func (w *assetWallet) confirmRedemption(coinID dex.Bytes, redemption *asset.Rede if err != nil { return nil, fmt.Errorf("error pulling swap data from contract: %v", err) } - switch swap.State { - case dexeth.SSRedeemed: - w.log.Infof("Redemption in tx %s was apparently redeemed by another tx. OK.", txHash) - return confStatus(w.finalizeConfs, w.finalizeConfs, txHash), nil - case dexeth.SSRefunded: - return nil, asset.ErrSwapRefunded + if confirmTx.IsRedeem() { + switch swap.State { + case dexeth.SSRedeemed: + w.log.Infof("Redemption in tx %s was apparently redeemed by another tx. OK.", txHash) + return confStatus(w.finalizeConfs, w.finalizeConfs, txHash), nil + case dexeth.SSRefunded: + return nil, asset.ErrSwapRefunded + } + } else { + switch swap.State { + case dexeth.SSRedeemed: + return nil, asset.ErrSwapRedeemed + case dexeth.SSRefunded: + w.log.Infof("Refund in tx %s was apparently refunded by another tx. OK.", txHash) + return confStatus(w.finalizeConfs, w.finalizeConfs, txHash), nil + } } - err = fmt.Errorf("tx %s failed to redeem %s funds", txHash, dex.BipIDSymbol(w.assetID)) + err = fmt.Errorf("tx %s failed to %s %s funds", txHash, confirmTx.TxType(), dex.BipIDSymbol(w.assetID)) return nil, errors.Join(err, asset.ErrTxRejected) } return confStatus(confs, w.finalizeConfs, txHash), nil diff --git a/client/asset/eth/eth_test.go b/client/asset/eth/eth_test.go index 36e7752fe6..847b1886d6 100644 --- a/client/asset/eth/eth_test.go +++ b/client/asset/eth/eth_test.go @@ -4566,12 +4566,12 @@ func testSend(t *testing.T, assetID uint32) { } } -func TestConfirmRedemption(t *testing.T) { - t.Run("eth", func(t *testing.T) { testConfirmRedemption(t, BipID) }) - t.Run("token", func(t *testing.T) { testConfirmRedemption(t, usdcTokenID) }) +func TestConfirmTransaction(t *testing.T) { + t.Run("eth", func(t *testing.T) { testConfirmTransaction(t, BipID) }) + t.Run("token", func(t *testing.T) { testConfirmTransaction(t, usdcTokenID) }) } -func testConfirmRedemption(t *testing.T, assetID uint32) { +func testConfirmTransaction(t *testing.T, assetID uint32) { wi, eth, node, shutdown := tassetWallet(assetID) defer shutdown() @@ -4586,12 +4586,9 @@ func testConfirmRedemption(t *testing.T, assetID uint32) { var txHash common.Hash copy(txHash[:], encode.RandomBytes(32)) - redemption := &asset.Redemption{ - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(0, secretHash), - }, - Secret: secret[:], - } + confirmTx := asset.NewRedeemConfTx(&asset.AuditInfo{ + Contract: dexeth.EncodeContractData(0, secretHash), + }, secret[:]) pendingTx := &extendedWalletTx{ WalletTransaction: &asset.WalletTransaction{ @@ -4619,6 +4616,7 @@ func testConfirmRedemption(t *testing.T, assetID uint32) { step dexeth.SwapStep receipt *types.Receipt receiptErr error + confirmTx *asset.ConfirmTx } tests := []*test{ @@ -4628,6 +4626,7 @@ func testConfirmRedemption(t *testing.T, assetID uint32) { Status: types.ReceiptStatusSuccessful, BlockNumber: big.NewInt(confBlock + 1), }, + confirmTx: confirmTx, expectedConfs: txConfsNeededToConfirm - 1, }, { @@ -4638,11 +4637,13 @@ func testConfirmRedemption(t *testing.T, assetID uint32) { Status: types.ReceiptStatusSuccessful, BlockNumber: big.NewInt(confBlock), }, + confirmTx: confirmTx, }, { name: "found in pending txs", step: dexeth.SSRedeemed, pendingTx: pendingTx, + confirmTx: confirmTx, expectedConfs: txConfsNeededToConfirm - 1, }, { @@ -4650,6 +4651,7 @@ func testConfirmRedemption(t *testing.T, assetID uint32) { step: dexeth.SSRedeemed, dbTx: dbTx, expectedConfs: txConfsNeededToConfirm, + confirmTx: confirmTx, receipt: &types.Receipt{ Status: types.ReceiptStatusSuccessful, BlockNumber: big.NewInt(confBlock), @@ -4660,6 +4662,7 @@ func testConfirmRedemption(t *testing.T, assetID uint32) { step: dexeth.SSRedeemed, dbErr: errors.New("test error"), expectedConfs: txConfsNeededToConfirm - 1, + confirmTx: confirmTx, receipt: &types.Receipt{ Status: types.ReceiptStatusSuccessful, BlockNumber: big.NewInt(confBlock + 1), @@ -4670,6 +4673,7 @@ func testConfirmRedemption(t *testing.T, assetID uint32) { step: dexeth.SSInitiated, expectErr: true, expectRedemptionFailedErr: true, + confirmTx: confirmTx, receipt: &types.Receipt{ Status: types.ReceiptStatusFailed, BlockNumber: big.NewInt(confBlock), @@ -4682,8 +4686,39 @@ func testConfirmRedemption(t *testing.T, assetID uint32) { Status: types.ReceiptStatusFailed, BlockNumber: big.NewInt(confBlock), }, + confirmTx: confirmTx, + expectedConfs: txConfsNeededToConfirm, + }, + { + name: "refund found on-chain. refunded by another unknown transaction", + step: dexeth.SSRefunded, + receipt: &types.Receipt{ + Status: types.ReceiptStatusFailed, + BlockNumber: big.NewInt(confBlock), + }, + confirmTx: asset.NewRefundConfTx(txHash[:], dexeth.EncodeContractData(0, secretHash), secret[:]), expectedConfs: txConfsNeededToConfirm, }, + { + name: "redeem refunded by another unknown transaction", + step: dexeth.SSRefunded, + receipt: &types.Receipt{ + Status: types.ReceiptStatusFailed, + BlockNumber: big.NewInt(confBlock), + }, + confirmTx: confirmTx, + expectErr: true, + }, + { + name: "refund redeemed by another unknown transaction", + step: dexeth.SSRedeemed, + receipt: &types.Receipt{ + Status: types.ReceiptStatusFailed, + BlockNumber: big.NewInt(confBlock), + }, + confirmTx: asset.NewRefundConfTx(txHash[:], dexeth.EncodeContractData(0, secretHash), secret[:]), + expectErr: true, + }, } runTest := func(test *test) { @@ -4709,7 +4744,7 @@ func testConfirmRedemption(t *testing.T, assetID uint32) { node.receipt = test.receipt node.receiptErr = test.receiptErr - result, err := wi.ConfirmRedemption(txHash[:], redemption, 0) + result, err := wi.ConfirmTransaction(txHash[:], test.confirmTx, 0) if test.expectErr { if err == nil { t.Fatalf("%s: expected error but did not get", test.name) diff --git a/client/asset/interface.go b/client/asset/interface.go index 68af08d7f4..bb248153a6 100644 --- a/client/asset/interface.go +++ b/client/asset/interface.go @@ -235,9 +235,12 @@ const ( ErrConnectionDown = dex.ErrorKind("wallet not connected") ErrNotImplemented = dex.ErrorKind("not implemented") ErrUnsupported = dex.ErrorKind("unsupported") - // ErrSwapRefunded is returned from ConfirmRedemption when the swap has + // ErrSwapRefunded is returned from ConfirmTransaction when the swap has // been refunded before the user could redeem. ErrSwapRefunded = dex.ErrorKind("swap refunded") + // ErrSwapRedeemed is returned from ConfirmTransaction when the swap has + // been redeemed before the user could refund. + ErrSwapRedeemed = dex.ErrorKind("swap redeemed") // ErrNotEnoughConfirms is returned when a transaction is confirmed, // but does not have enough confirmations to be trusted. ErrNotEnoughConfirms = dex.ErrorKind("transaction does not have enough confirmations") @@ -400,10 +403,10 @@ type WalletConfig struct { DataDir string } -// ConfirmRedemptionStatus contains the coinID which redeemed a swap, the +// ConfirmTxStatus contains the coinID which redeemed or refunded a swap, the // number of confirmations the transaction has, and the number of confirmations // required for it to be considered confirmed. -type ConfirmRedemptionStatus struct { +type ConfirmTxStatus struct { Confs uint64 Req uint64 CoinID dex.Bytes @@ -547,15 +550,15 @@ type Wallet interface { Send(address string, value, feeRate uint64) (Coin, error) // ValidateAddress checks that the provided address is valid. ValidateAddress(address string) bool - // ConfirmRedemption checks the status of a redemption. It returns the - // number of confirmations the redemption has, the number of confirmations + // ConfirmTransaction checks the status of a redemption or refund. It + // returns the number of confirmations the tx has, the number of confirmations // that are required for it to be considered fully confirmed, and the - // CoinID used to do the redemption. If it is determined that a transaction - // will not be mined, this function will submit a new transaction to - // replace the old one. The caller is notified of this by having a - // different CoinID in the returned asset.ConfirmRedemptionStatus as was - // used to call the function. - ConfirmRedemption(coinID dex.Bytes, redemption *Redemption, feeSuggestion uint64) (*ConfirmRedemptionStatus, error) + // CoinID it is currently watching for confirms. If it is determined that + // a transaction will not be mined, this function will submit a new transaction + // to replace the old one. The caller is notified of this by having a + // different CoinID in the returned asset.ConfirmTxStatus as was used to + // call the function. + ConfirmTransaction(coinID dex.Bytes, confirmTx *ConfirmTx, feeSuggestion uint64) (*ConfirmTxStatus, error) // SingleLotSwapRefundFees returns the fees for a swap and refund transaction for a single lot. SingleLotSwapRefundFees(version uint32, feeRate uint64, useSafeTxSize bool) (uint64, uint64, error) // SingleLotRedeemFees returns the fees for a redeem transaction for a single lot. @@ -1367,6 +1370,107 @@ type Contract struct { LockTime uint64 } +// ConfirmTxType is the confirm tx type. +type ConfirmTxType int + +const ( + // CTRedeem is a redeem tx. + CTRedeem = iota + // CTRefund is a refund tx. + CTRefund +) + +// String satisfies Stringer. +func (ct ConfirmTxType) String() string { + switch ct { + case CTRedeem: + return "redeem" + case CTRefund: + return "refund" + } + return "unknown" +} + +// ConfirmTx is a redemption transaction that spends a counter-party's swap +// contract or a refund that spends our own swap. +type ConfirmTx struct { + // spends is the AuditInfo for the swap output being spent. Only needed for redeems. + spends *AuditInfo + // secret is the secret key needed to satisfy the swap contract. Only needed for redeems. + secret dex.Bytes + // spendsCoinID is the tx to refund. Only needed for refunds. + spendsCoinID dex.Bytes + // contract is the contract to refund. Only needed for refunds. + contract dex.Bytes + // secretHash is the secret hash of the swap to refund. Only needed for refunds. + secretHash dex.Bytes + // confirmTxType is redeem or refund. + txType ConfirmTxType +} + +// NewRedeemConfTx creates a new reddem conf tx. +func NewRedeemConfTx(spends *AuditInfo, secret dex.Bytes) *ConfirmTx { + return &ConfirmTx{ + spends: spends, + secret: secret, + txType: CTRedeem, + } +} + +// NewRefundConfTx creates a new refund conf tx. +func NewRefundConfTx(spendsCoinID, contract, secretHash dex.Bytes) *ConfirmTx { + return &ConfirmTx{ + spendsCoinID: spendsCoinID, + contract: contract, + secretHash: secretHash, + txType: CTRefund, + } +} + +// Spends returns the conf tx spends. Only use for redeems. +func (ct *ConfirmTx) Spends() *AuditInfo { + return ct.spends +} + +// Secret returns the conf tx secret. Only use for redeems. +func (ct *ConfirmTx) Secret() dex.Bytes { + return ct.secret +} + +// SpendsCoinID returns the coin id the confirm tx spends. +func (ct *ConfirmTx) SpendsCoinID() dex.Bytes { + if ct.txType == CTRedeem { + return ct.spends.Coin.ID() + } + return ct.spendsCoinID +} + +// SecretHash returns the swap's secret hash. +func (ct *ConfirmTx) SecretHash() dex.Bytes { + if ct.txType == CTRedeem { + return ct.spends.SecretHash + } + return ct.secretHash +} + +// Contract returns the swap's contract. +func (ct *ConfirmTx) Contract() dex.Bytes { + if ct.txType == CTRedeem { + return ct.spends.Contract + } + return ct.contract +} + +// TxType returns the conf tx type. +func (ct *ConfirmTx) TxType() ConfirmTxType { + return ct.txType +} + +// IsRedeem return true if it is a redeem. +func (ct *ConfirmTx) IsRedeem() bool { + return ct.txType == CTRedeem +} + // Redemption is a redemption transaction that spends a counter-party's swap // contract. type Redemption struct { diff --git a/client/asset/zec/zec.go b/client/asset/zec/zec.go index bd68bb57d1..38fa46f867 100644 --- a/client/asset/zec/zec.go +++ b/client/asset/zec/zec.go @@ -70,10 +70,10 @@ const ( blockTicker = time.Second peerCountTicker = 5 * time.Second - // requiredRedeemConfirms is the amount of confirms a redeem transaction - // needs before the trade is considered confirmed. The redeem is - // monitored until this number of confirms is reached. - requiredRedeemConfirms = 1 + // requiredConfTxConfirms is the amount of confirms a redeem or refund + // transaction needs before the trade is considered confirmed. The + // redeem is monitored until this number of confirms is reached. + requiredConfTxConfirms = 1 depositAddrPrefix = "unified:" ) @@ -1567,70 +1567,86 @@ func (w *zecWallet) AuditContract(coinID, contract, txData dex.Bytes, rebroadcas }, nil } -func (w *zecWallet) ConfirmRedemption(coinID dex.Bytes, redemption *asset.Redemption, feeSuggestion uint64) (*asset.ConfirmRedemptionStatus, error) { +func (w *zecWallet) ConfirmTransaction(coinID dex.Bytes, confirmTx *asset.ConfirmTx, feeSuggestion uint64) (*asset.ConfirmTxStatus, error) { txHash, _, err := decodeCoinID(coinID) if err != nil { return nil, err } tx, err := getWalletTransaction(w, txHash) - // redemption transaction found, return its confirms. + // transaction found, return its confirms. // - // TODO: Investigate the case where this redeem has been sitting in the + // TODO: Investigate the case where this tx has been sitting in the // mempool for a long amount of time, possibly requiring some action by // us to get it unstuck. if err == nil { if tx.Confirmations < 0 { tx.Confirmations = 0 } - return &asset.ConfirmRedemptionStatus{ + return &asset.ConfirmTxStatus{ Confs: uint64(tx.Confirmations), - Req: requiredRedeemConfirms, + Req: requiredConfTxConfirms, CoinID: coinID, }, nil } - // Redemption transaction is missing from the point of view of our node! + // Transaction is missing from the point of view of our node! // Unlikely, but possible it was redeemed by another transaction. Check // if the contract is still an unspent output. - swapHash, vout, err := decodeCoinID(redemption.Spends.Coin.ID()) + swapHash, vout, err := decodeCoinID(confirmTx.SpendsCoinID()) if err != nil { return nil, err } utxo, _, err := getTxOut(w, swapHash, vout) if err != nil { - return nil, newError(errNoTx, "error finding unspent contract %s with swap hash %v vout %d: %w", redemption.Spends.Coin.ID(), swapHash, vout, err) + return nil, newError(errNoTx, "error finding unspent contract %s with swap hash %v vout %d: %w", confirmTx.SpendsCoinID(), swapHash, vout, err) } if utxo == nil { // TODO: Spent, but by who. Find the spending tx. - w.log.Warnf("Contract coin %v with swap hash %v vout %d spent by someone but not sure who.", redemption.Spends.Coin.ID(), swapHash, vout) + w.log.Warnf("Contract coin %v with swap hash %v vout %d spent by someone but not sure who.", confirmTx.SpendsCoinID(), swapHash, vout) // Incorrect, but we will be in a loop of erroring if we don't // return something. - return &asset.ConfirmRedemptionStatus{ - Confs: requiredRedeemConfirms, - Req: requiredRedeemConfirms, + return &asset.ConfirmTxStatus{ + Confs: requiredConfTxConfirms, + Req: requiredConfTxConfirms, CoinID: coinID, }, nil } - // The contract has not yet been redeemed, but it seems the redeeming - // tx has disappeared. Assume the fee was too low at the time and it - // was eventually purged from the mempool. Attempt to redeem again with - // a currently reasonable fee. - - form := &asset.RedeemForm{ - Redemptions: []*asset.Redemption{redemption}, - } - _, coin, _, err := w.Redeem(form) - if err != nil { - return nil, fmt.Errorf("unable to re-redeem %s with swap hash %v vout %d: %w", redemption.Spends.Coin.ID(), swapHash, vout, err) + // The contract has not yet been redeemed or refunded, but it seems the + // spending tx has disappeared. Assume the fee was too low at the time + // and it was eventually purged from the mempool. Attempt to spend again + // with a currently reasonable fee. + var newCoinID dex.Bytes + if confirmTx.IsRedeem() { + form := &asset.RedeemForm{ + Redemptions: []*asset.Redemption{ + { + Spends: confirmTx.Spends(), + Secret: confirmTx.Secret(), + }, + }, + FeeSuggestion: feeSuggestion, + } + _, coin, _, err := w.Redeem(form) + if err != nil { + return nil, fmt.Errorf("unable to re-redeem %s with swap hash %v vout %d: %w", confirmTx.SpendsCoinID(), swapHash, vout, err) + } + newCoinID = coin.ID() + } else { + spendsCoinID := confirmTx.SpendsCoinID() + newCoinID, err = w.Refund(spendsCoinID, confirmTx.Contract(), feeSuggestion) + if err != nil { + return nil, fmt.Errorf("unable to re-refund %s: %w", spendsCoinID, err) + } } - return &asset.ConfirmRedemptionStatus{ + + return &asset.ConfirmTxStatus{ Confs: 0, - Req: requiredRedeemConfirms, - CoinID: coin.ID(), + Req: requiredConfTxConfirms, + CoinID: newCoinID, }, nil } diff --git a/client/asset/zec/zec_test.go b/client/asset/zec/zec_test.go index 7795d32073..2ddf91b892 100644 --- a/client/asset/zec/zec_test.go +++ b/client/asset/zec/zec_test.go @@ -1411,19 +1411,16 @@ func TestConfirmRedemption(t *testing.T) { Expiration: lockTime, } - redemption := &asset.Redemption{ - Spends: ci, - Secret: secret, - } + confirmTx := asset.NewRedeemConfTx(ci, secret) walletTx := &btc.GetTransactionResult{ Confirmations: 1, } cl.queueResponse("gettransaction", walletTx) - st, err := w.ConfirmRedemption(coinID, redemption, 0) + st, err := w.ConfirmTransaction(coinID, confirmTx, 0) if err != nil { - t.Fatalf("Initial ConfirmRedemption error: %v", err) + t.Fatalf("Initial ConfirmTransaction error: %v", err) } if st.Confs != walletTx.Confirmations { t.Fatalf("wrongs confs, %d != %d", st.Confs, walletTx.Confirmations) @@ -1431,19 +1428,19 @@ func TestConfirmRedemption(t *testing.T) { cl.queueResponse("gettransaction", tErr) cl.queueResponse("gettxout", tErr) - _, err = w.ConfirmRedemption(coinID, redemption, 0) + _, err = w.ConfirmTransaction(coinID, confirmTx, 0) if !errorHasCode(err, errNoTx) { t.Fatalf("wrong error for gettxout error: %v", err) } cl.queueResponse("gettransaction", tErr) cl.queueResponse("gettxout", nil) - st, err = w.ConfirmRedemption(coinID, redemption, 0) + st, err = w.ConfirmTransaction(coinID, confirmTx, 0) if err != nil { - t.Fatalf("ConfirmRedemption error for spent redemption: %v", err) + t.Fatalf("ConfirmTransaction error for spent redemption: %v", err) } - if st.Confs != requiredRedeemConfirms { - t.Fatalf("wrong confs for spent redemption: %d != %d", st.Confs, requiredRedeemConfirms) + if st.Confs != requiredConfTxConfirms { + t.Fatalf("wrong confs for spent redemption: %d != %d", st.Confs, requiredConfTxConfirms) } // Re-submission path is tested by TestRedemption