diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index f7a5c7d8af..7310b63982 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -170,6 +170,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { public BooleanProperty wasWalletPolledProperty = new SimpleBooleanProperty(false); public BooleanProperty wasWalletSyncedAndPolledProperty = new SimpleBooleanProperty(false); private static final long MISSING_TXS_DELAY_MS = Config.baseCurrencyNetwork().isTestnet() ? 5000 : 30000; + private Long lastDepositTxMissingHeight; // height when we last saw missing deposit txs (to wait for a confirmation before reverting state) + private Long lastPayoutTxMissingHeight; // height when we last saw missing payout tx (to wait for a confirmation before reverting state) /////////////////////////////////////////////////////////////////////////////////////////// // Enums @@ -3041,13 +3043,19 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { private void setDepositTxs(List txs, boolean poolChecked) { - // set deposit txs - getMaker().setDepositTx(getMakerDepositTx(txs)); - getTaker().setDepositTx(getTakerDepositTx(txs)); + // get deposit txs + MoneroTxWallet makerDepositTx = getMakerDepositTx(txs); + MoneroTxWallet takerDepositTx = getTakerDepositTx(txs); + MoneroTxWallet buyerDepositTx = getBuyerDepositTx(txs); + MoneroTxWallet sellerDepositTx = getSellerDepositTx(txs); + + // set txs if known + if (makerDepositTx != null) getMaker().setDepositTx(makerDepositTx); + if (takerDepositTx != null) getTaker().setDepositTx(takerDepositTx); // set actual buyer security deposit - if (isSeen(getBuyer().getDepositTx())) { - BigInteger buyerSecurityDeposit = ((MoneroTxWallet) getBuyer().getDepositTx()).getIncomingAmount(); + if (isSeen(buyerDepositTx)) { + BigInteger buyerSecurityDeposit = buyerDepositTx.getIncomingAmount(); if (!getBuyer().getSecurityDeposit().equals(BigInteger.ZERO) && !buyerSecurityDeposit.equals(getBuyer().getSecurityDeposit())) { log.warn("Overwriting buyer security deposit for {} {}, old={}, new={}", getClass().getSimpleName(), getShortId(), getBuyer().getSecurityDeposit(), buyerSecurityDeposit); } @@ -3055,8 +3063,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } // set actual seller security deposit - if (isSeen(getSeller().getDepositTx())) { - BigInteger sellerSecurityDeposit = ((MoneroTxWallet) getSeller().getDepositTx()).getIncomingAmount().subtract(getAmount()); + if (isSeen(sellerDepositTx)) { + BigInteger sellerSecurityDeposit = sellerDepositTx.getIncomingAmount().subtract(getAmount()); if (!getSeller().getSecurityDeposit().equals(BigInteger.ZERO) && !sellerSecurityDeposit.equals(getSeller().getSecurityDeposit())) { log.warn("Overwriting seller security deposit for {} {}, old={}, new={}", getClass().getSimpleName(), getShortId(), getSeller().getSecurityDeposit(), sellerSecurityDeposit); } @@ -3064,32 +3072,43 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } // advance deposit state - if (isSeen(getMaker().getDepositTx()) && (hasBuyerAsTakerWithoutDeposit() || isSeen(getTaker().getDepositTx()))) { + if (isSeen(makerDepositTx) && (hasBuyerAsTakerWithoutDeposit() || isSeen(takerDepositTx))) { setStateDepositsSeen(); // check for deposit txs confirmed - if (getMaker().getDepositTx().isConfirmed() && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().isConfirmed())) { + if (makerDepositTx.isConfirmed() && (hasBuyerAsTakerWithoutDeposit() || takerDepositTx.isConfirmed())) { setStateDepositsConfirmed(); } // check for deposit txs unlocked - if (getMaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK)) { + if (makerDepositTx.getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK && (hasBuyerAsTakerWithoutDeposit() || takerDepositTx.getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK)) { setStateDepositsUnlocked(); } // check for deposit txs finalized - if (getMaker().getDepositTx().getNumConfirmations() >= NUM_BLOCKS_DEPOSITS_FINALIZED && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().getNumConfirmations() >= NUM_BLOCKS_DEPOSITS_FINALIZED)) { + if (makerDepositTx.getNumConfirmations() >= NUM_BLOCKS_DEPOSITS_FINALIZED && (hasBuyerAsTakerWithoutDeposit() || takerDepositTx.getNumConfirmations() >= NUM_BLOCKS_DEPOSITS_FINALIZED)) { setStateDepositsFinalized(); } } // revert deposit state if necessary - State depositsState = getDepositsState(); + State depositsState = getDepositsState(makerDepositTx, takerDepositTx); State minDepositsState = isPaymentSent() ? State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN : getState(); if (poolChecked && depositsState.ordinal() < minDepositsState.ordinal()) { - log.warn("Deposits state has reverted from {} to {} for {} {}. Possible reorg?", minDepositsState, depositsState, getClass().getSimpleName(), getShortId()); - if (depositsState == State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS) setErrorMessage("Deposit transactions are missing for trade " + getShortId() + ". This can happen after a blockchain reorganization.\n\nIf the issue continues, you can contact support or mark the trade as failed."); - if (!isPaymentSent()) setState(depositsState); // only revert state if payment not sent + + // skip reverting state until next confirmation // TODO: sometimes txs are missing from the wallet and reappear without reorg + if (lastDepositTxMissingHeight == null || lastDepositTxMissingHeight <= walletHeight.get()) { + if (lastDepositTxMissingHeight == null) log.warn("Missing deposit txs for {} {} at height {}, waiting for a block before reverting state", getClass().getSimpleName(), getShortId(), lastDepositTxMissingHeight); + lastDepositTxMissingHeight = wallet.getHeight(); + } else { + log.warn("Reverting deposits state from {} to {} for {} {}. Possible reorg?", minDepositsState, depositsState, getClass().getSimpleName(), getShortId()); + getMaker().setDepositTx(makerDepositTx); + getTaker().setDepositTx(takerDepositTx); + if (depositsState == State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS) setErrorMessage("Deposit transactions are missing for trade " + getShortId() + ". This can happen after a blockchain reorganization.\n\nIf the issue continues, you can contact support or mark the trade as failed."); + if (!isPaymentSent()) setState(depositsState); // only revert state if payment not sent + } + } else { + lastDepositTxMissingHeight = null; } // announce deposits update @@ -3117,23 +3136,36 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (payoutTx != null) setPayoutTx(payoutTx); else if (hasPayoutTx) setPayoutState(PayoutState.PAYOUT_PUBLISHED); else if (poolChecked && isPayoutPublished()) { // payout tx seen then lost (e.g. reorg) - for (TradePeer peer : getAllPeers()) { - peer.setPaymentReceivedMessage(null); - peer.setPaymentReceivedMessageState(MessageState.UNDEFINED); - peer.setDisputeClosedMessage(null); - } - setPayoutState(PayoutState.PAYOUT_UNPUBLISHED); - if (isCompleted()) processModel.getTradeManager().onMoveClosedTradeToPendingTrades(this); - String errorMsg = "The payout transaction is not seen for trade " + getShortId() + ". This can happen after a blockchain reorganization..\n\nIf the payout does not confirm automatically, you can contact support or mark the trade as failed."; - if (isSeller() && getState().ordinal() >= State.BUYER_RECEIVED_PAYMENT_RECEIVED_MSG.ordinal()) { - log.warn("Reverting state of {} {} from {} to {} because payout is unseen. Possible reorg?", getClass().getSimpleName(), getId(), getState(), Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); - setState(State.SELLER_SENT_PAYMENT_RECEIVED_MSG); - onPayoutError(false, true, null); - setErrorMessage(errorMsg); - } else if (getState().ordinal() >= State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal()) { - log.warn("Reverting state of {} {} from {} to {} because payout is unseen. Possible reorg?", getClass().getSimpleName(), getId(), getState(), Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT); - setState(State.SELLER_CONFIRMED_PAYMENT_RECEIPT); - setErrorMessage(errorMsg); + + // skip reverting state until next confirmation + if (lastPayoutTxMissingHeight == null || lastPayoutTxMissingHeight <= walletHeight.get()) { + if (lastPayoutTxMissingHeight == null) log.warn("Missing payout tx for {} {} at height {}, waiting for a block before reverting state", getClass().getSimpleName(), getShortId(), lastPayoutTxMissingHeight); + lastPayoutTxMissingHeight = wallet.getHeight(); + } else { + + // reset payment received and dispute closed messages + for (TradePeer peer : getAllPeers()) { + peer.setPaymentReceivedMessage(null); + peer.setPaymentReceivedMessageState(MessageState.UNDEFINED); + peer.setDisputeClosedMessage(null); + } + + // revert state + setPayoutState(PayoutState.PAYOUT_UNPUBLISHED); + String errorMsg = "The payout transaction is not seen for trade " + getShortId() + ". This can happen after a blockchain reorganization..\n\nIf the payout does not confirm automatically, you can contact support or mark the trade as failed."; + if (isSeller() && getState().ordinal() >= State.BUYER_RECEIVED_PAYMENT_RECEIVED_MSG.ordinal()) { + log.warn("Reverting state of {} {} from {} to {} because payout is unseen. Possible reorg?", getClass().getSimpleName(), getId(), getState(), Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); + setState(State.SELLER_SENT_PAYMENT_RECEIVED_MSG); + onPayoutError(false, true, null); + setErrorMessage(errorMsg); + } else if (getState().ordinal() >= State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal()) { + log.warn("Reverting state of {} {} from {} to {} because payout is unseen. Possible reorg?", getClass().getSimpleName(), getId(), getState(), Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT); + setState(State.SELLER_CONFIRMED_PAYMENT_RECEIPT); + setErrorMessage(errorMsg); + } + + // move trade back to pending if marked completed + if (isCompleted()) processModel.getTradeManager().onMoveClosedTradeToPendingTrades(this); } } } @@ -3257,6 +3289,14 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { return getValidDepositTx(txs, getTaker()); } + private MoneroTxWallet getBuyerDepositTx(List txs) { + return getValidDepositTx(txs, getBuyer()); + } + + private MoneroTxWallet getSellerDepositTx(List txs) { + return getValidDepositTx(txs, getSeller()); + } + private MoneroTxWallet getValidDepositTx(List txs, TradePeer peer) { for (MoneroTxWallet tx : txs) { if (tx.getHash().equals(peer.getDepositTxHash()) && !Boolean.TRUE.equals(tx.isFailed())) { @@ -3273,14 +3313,14 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { return true; } - private State getDepositsState() { - if (getMaker().getDepositTx() == null || (!hasBuyerAsTakerWithoutDeposit() && getTaker().getDepositTx() == null)) return State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS; - if (getMaker().getDepositTx().isFailed() || (!hasBuyerAsTakerWithoutDeposit() && getTaker().getDepositTx().isFailed())) return State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS; - if (getMaker().getDepositTx().getNumConfirmations() == null || (!hasBuyerAsTakerWithoutDeposit() && getTaker().getDepositTx().getNumConfirmations() == null)) return State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS; - if (getMaker().getDepositTx().getNumConfirmations() >= NUM_BLOCKS_DEPOSITS_FINALIZED && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().getNumConfirmations() >= NUM_BLOCKS_DEPOSITS_FINALIZED)) return State.DEPOSIT_TXS_FINALIZED_IN_BLOCKCHAIN; - if (getMaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK)) return State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN; - if (getMaker().getDepositTx().isConfirmed() && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().isConfirmed())) return State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN; - if (isSeen(getMaker().getDepositTx()) && (hasBuyerAsTakerWithoutDeposit() || isSeen(getTaker().getDepositTx()))) return State.DEPOSIT_TXS_SEEN_IN_NETWORK; + private State getDepositsState(MoneroTxWallet makerDepositTx, MoneroTxWallet takerDepositTx) { + if (makerDepositTx == null || (!hasBuyerAsTakerWithoutDeposit() && takerDepositTx == null)) return State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS; + if (makerDepositTx.isFailed() || (!hasBuyerAsTakerWithoutDeposit() && takerDepositTx.isFailed())) return State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS; + if (makerDepositTx.getNumConfirmations() == null || (!hasBuyerAsTakerWithoutDeposit() && takerDepositTx.getNumConfirmations() == null)) return State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS; + if (makerDepositTx.getNumConfirmations() >= NUM_BLOCKS_DEPOSITS_FINALIZED && (hasBuyerAsTakerWithoutDeposit() || takerDepositTx.getNumConfirmations() >= NUM_BLOCKS_DEPOSITS_FINALIZED)) return State.DEPOSIT_TXS_FINALIZED_IN_BLOCKCHAIN; + if (makerDepositTx.getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK && (hasBuyerAsTakerWithoutDeposit() || takerDepositTx.getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK)) return State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN; + if (makerDepositTx.isConfirmed() && (hasBuyerAsTakerWithoutDeposit() || takerDepositTx.isConfirmed())) return State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN; + if (isSeen(makerDepositTx) && (hasBuyerAsTakerWithoutDeposit() || isSeen(takerDepositTx))) return State.DEPOSIT_TXS_SEEN_IN_NETWORK; return State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS; }