Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

client: Babysit refund transactions. #3082

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 49 additions & 34 deletions client/asset/btc/btc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}

Expand Down
67 changes: 36 additions & 31 deletions client/asset/btc/btc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()

Expand All @@ -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
Expand All @@ -5852,60 +5849,68 @@ 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,
Recipient: addr.String(),
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
node.txOutErr = test.txOutErr
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)
Expand Down
2 changes: 1 addition & 1 deletion client/asset/btc/electrum.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading
Loading