diff --git a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java index 1b7bb841ab..fd29e1583c 100644 --- a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java @@ -598,8 +598,8 @@ public abstract class DisputeManager> extends Sup // TODO: DisputeOpenedMessage should include arbitrator's updated multisig hex too // TODO: arbitrator needs to import multisig info then scan for updated state? - // arbitrator syncs and polls wallet unless finalized - if (trade.isArbitrator() && !trade.isPayoutFinalized()) { + // sync and poll wallet unless finalized + if (!trade.isPayoutFinalized()) { trade.syncAndPollWallet(); trade.recoverIfMissingWalletData(); } @@ -1007,7 +1007,7 @@ public abstract class DisputeManager> extends Sup // update trade state if (updateState) { trade.getProcessModel().setUnsignedPayoutTx(payoutTx); - trade.updatePayout(payoutTx); + trade.setPayoutTx(payoutTx); if (trade.getBuyer().getUpdatedMultisigHex() != null) trade.getBuyer().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); if (trade.getSeller().getUpdatedMultisigHex() != null) trade.getSeller().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); } diff --git a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java index 3b5eab267d..4e43f7ed79 100644 --- a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java +++ b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java @@ -520,7 +520,7 @@ public final class ArbitrationManager extends DisputeManager { if (!isInitialized || isShutDownStarted) return; ThreadUtils.submitToPool(() -> { - if (isPayoutPublished()) updatePollPeriod(); + updatePollPeriod(); // handle when payout published if (newValue == Trade.PayoutState.PAYOUT_PUBLISHED) { @@ -769,7 +759,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { xmrWalletService.addWalletListener(idlePayoutSyncer); } - // TODO: buyer's payment sent message state property became unsynced if shut down while awaiting ack from seller. fixed mismatch in v1.0.19, but can this check can be removed? + // TODO: buyer's payment sent message state property became unsynced if shut down while awaiting ack from seller. fixed mismatch in v1.0.19, but can this check be removed? if (isBuyer()) { MessageState expectedState = getPaymentSentMessageState(); if (expectedState != null && expectedState != getSeller().getPaymentSentMessageStateProperty().get()) { @@ -960,7 +950,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } public boolean isIdling() { - if (isPayoutUnlocked() && !Config.baseCurrencyNetwork().isTestnet()) return true; // idle after payout unlocked (unless testnet) + if (isPayoutUnlocked()) return true; // idle after payout unlocked return this instanceof ArbitratorTrade && isDepositsConfirmed() && walletExists() && pollNormalStartTimeMs == null; // arbitrator idles trade after deposits confirm unless overriden } @@ -1474,7 +1464,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { MoneroTxSet describedTxSet = wallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex)); if (describedTxSet.getTxs() == null || describedTxSet.getTxs().size() != 1) throw new IllegalArgumentException("Bad payout tx"); // TODO (woodser): test nack MoneroTxWallet payoutTx = describedTxSet.getTxs().get(0); - if (payoutTxId == null) updatePayout(payoutTx); // update payout tx if id currently unknown + if (payoutTxId == null) setPayoutTx(payoutTx); // update payout tx if id currently unknown // verify payout tx has exactly 2 destinations if (payoutTx.getOutgoingTransfer() == null || payoutTx.getOutgoingTransfer().getDestinations() == null || payoutTx.getOutgoingTransfer().getDestinations().size() != 2) throw new IllegalArgumentException("Payout tx does not have exactly two destinations"); @@ -1506,7 +1496,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (!sellerPayoutDestination.getAmount().equals(expectedSellerPayout)) throw new IllegalArgumentException("Seller destination amount is not deposit amount - trade amount - 1/2 tx costs, " + sellerPayoutDestination.getAmount() + " vs " + expectedSellerPayout); // update payout tx - updatePayout(payoutTx); + setPayoutTx(payoutTx); // check connection boolean doSign = sign && getPayoutTxHex() == null; @@ -1543,7 +1533,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // describe result describedTxSet = wallet.describeMultisigTxSet(getPayoutTxHex()); payoutTx = describedTxSet.getTxs().get(0); - updatePayout(payoutTx); + setPayoutTx(payoutTx); } // save trade state @@ -1577,9 +1567,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // poll the main wallet log.warn("Processing payout tx for {} {} by polling main wallet", getClass().getSimpleName(), getShortId()); - long startTime = System.currentTimeMillis(); xmrWalletService.doPollWallet(true); - log.info("Done polling main wallet to verify payout tx for {} {} in {} ms", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime); // fetch payout tx from main wallet MoneroTxWallet payoutTx = xmrWalletService.getWallet().getTx(payoutTxId); @@ -1593,7 +1581,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (!payoutTx.getIncomingAmount().equals(expectedAmount)) throw new IllegalStateException("Payout tx incoming amount is not deposit amount + trade amount - 1/2 tx costs, " + payoutTx.getIncomingAmount() + " vs " + getBuyer().getSecurityDeposit().add(getAmount()).subtract(txCostSplit)); // update payout tx - updatePayout(payoutTx); + setPayoutTx(payoutTx); } /** @@ -2043,7 +2031,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } if (payoutState.ordinal() < this.payoutState.ordinal()) { String message = "We got a payout state change to a previous phase (id=" + getShortId() + ").\n" + - "Old payout state is: " + this.state + ". New payout state is: " + payoutState; + "Old payout state is: " + this.payoutState + ". New payout state is: " + payoutState; log.warn(message); } @@ -2099,45 +2087,6 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { getVolumeProperty().set(getVolume()); } - public void updatePayout(MoneroTx payoutTx) { - - // set payout tx fields - this.payoutTx = payoutTx; - this.payoutTxId = payoutTx.getHash(); - this.payoutTxFee = payoutTx.getFee() == null ? 0 : payoutTx.getFee().longValueExact(); - this.payoutTxKey = payoutTx.getKey(); - if ("".equals(payoutTxId)) this.payoutTxId = null; // tx id is empty until signed - - // set payout tx id in dispute(s) - for (Dispute dispute : getDisputes()) dispute.setDisputePayoutTxId(payoutTxId); - - // set final payout amounts - if (isPaymentReceived()) { - BigInteger splitTxFee = payoutTx.getFee().divide(BigInteger.valueOf(2)); - getBuyer().setPayoutTxFee(splitTxFee); - getSeller().setPayoutTxFee(splitTxFee); - getBuyer().setPayoutAmount(getBuyer().getSecurityDeposit().subtract(getBuyer().getPayoutTxFee()).add(getAmount())); - getSeller().setPayoutAmount(getSeller().getSecurityDeposit().subtract(getSeller().getPayoutTxFee())); - } else { - DisputeResult disputeResult = getDisputeResult(); - if (disputeResult != null) { - BigInteger[] buyerSellerPayoutTxFees = ArbitrationManager.getBuyerSellerPayoutTxCost(disputeResult, payoutTx.getFee()); - getBuyer().setPayoutTxFee(buyerSellerPayoutTxFees[0]); - getSeller().setPayoutTxFee(buyerSellerPayoutTxFees[1]); - getBuyer().setPayoutAmount(disputeResult.getBuyerPayoutAmountBeforeCost().subtract(getBuyer().getPayoutTxFee())); - getSeller().setPayoutAmount(disputeResult.getSellerPayoutAmountBeforeCost().subtract(getSeller().getPayoutTxFee())); - } - } - - // set payout tx state - if (Boolean.TRUE.equals(payoutTx.isRelayed())) setPayoutStatePublished(); - if (payoutTx.isConfirmed()) setPayoutStateConfirmed(); - if (payoutTx.getNumConfirmations() != null) { - if (payoutTx.getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK) setPayoutStateUnlocked(); - if (payoutTx.getNumConfirmations() >= NUM_BLOCKS_PAYOUT_FINALIZED) setPayoutStateFinalized(); - } - } - public DisputeResult getDisputeResult() { if (getDisputes().isEmpty()) return null; return getDisputes().get(getDisputes().size() - 1).getDisputeResultProperty().get(); @@ -2707,7 +2656,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { /////////////////////////////////////////////////////////////////////////////////////////// private boolean tradeAmountTransferred() { - return isPaymentReceived() || (getDisputeResult() != null && getDisputeResult().getWinner() == DisputeResult.Winner.SELLER); + return isPayoutPublished() && (isPaymentReceived() || (getDisputeResult() != null && getDisputeResult().getWinner() == DisputeResult.Winner.SELLER)); } private void doPublishTradeStatistics() { @@ -2769,17 +2718,16 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (isShutDownStarted) return; // set known deposit txs - List depositTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true).setInTxPool(false)); - setDepositTxs(depositTxs); + doPollWallet(true); // start polling - if (!isIdling()) { - doTryInitSyncing(); - } else { - long startSyncingInMs = ThreadLocalRandom.current().nextLong(0, getPollPeriod()); // random time to start polling + if (isIdling()) { + long startSyncingInSec = Math.max(1, ThreadLocalRandom.current().nextLong(0, getPollPeriod()) / 1000l); // random seconds to start polling UserThread.runAfter(() -> ThreadUtils.execute(() -> { if (!isShutDownStarted) doTryInitSyncing(); - }, getId()), startSyncingInMs / 1000l); + }, getId()), startSyncingInSec); + } else { + doTryInitSyncing(); } } @@ -2930,50 +2878,13 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // sync wallet if behind if (!offlinePoll) syncWalletIfBehind(); - // get txs from trade wallet - boolean updatePool = !offlinePoll && !isDepositsConfirmed() && (getMaker().getDepositTx() == null || (getTaker().getDepositTx() == null && !hasBuyerAsTakerWithoutDeposit())); - List txs = getTxs(updatePool); - setDepositTxs(txs); - - // set actual buyer security deposit - if (isSeen(getBuyer().getDepositTx())) { - BigInteger buyerSecurityDeposit = ((MoneroTxWallet) getBuyer().getDepositTx()).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); - } - getBuyer().setSecurityDeposit(buyerSecurityDeposit); - } - - // set actual seller security deposit - if (isSeen(getSeller().getDepositTx())) { - BigInteger sellerSecurityDeposit = ((MoneroTxWallet) getSeller().getDepositTx()).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); - } - getSeller().setSecurityDeposit(sellerSecurityDeposit); - } - - // handle both deposits seen - if (isSeen(getMaker().getDepositTx()) && (hasBuyerAsTakerWithoutDeposit() || isSeen(getTaker().getDepositTx()))) { - setStateDepositsSeen(); - - // check for deposit txs confirmed - if (getMaker().getDepositTx().isConfirmed() && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().isConfirmed())) { - setStateDepositsConfirmed(); - } - - // check for deposit txs unlocked - if (getMaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().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)) { - setStateDepositsFinalized(); - } - } else if (isDepositsSeen()) { - log.warn("Resetting state to {} for {} {} because one or both deposit txs no longer seen as valid", Trade.State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS, getClass().getSimpleName(), getShortId()); - setState(Trade.State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS); + // set deposit txs from trade wallet + List txs = getTxs(false); + if (getValidMakerTx(txs) != null && (getValidTakerTx(txs) != null || hasBuyerAsTakerWithoutDeposit())) { + setDepositTxs(txs); + } else if (!offlinePoll) { + txs = getTxs(true); // check pool if deposits not found + setDepositTxs(txs); } } @@ -2988,7 +2899,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (!offlinePoll && (isPayoutExpected || isPayoutPublished())) syncWalletIfBehind(); // rescan spent outputs to detect unconfirmed payout tx - if (isPayoutExpected && wallet.getBalance().compareTo(BigInteger.ZERO) > 0) { + if (getPayoutState() == PayoutState.PAYOUT_PUBLISHED || (isPayoutExpected && wallet.getBalance().compareTo(BigInteger.ZERO) > 0)) { try { rescanSpent(true); } catch (Exception e) { @@ -2997,30 +2908,27 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } // get txs from trade wallet - boolean updatePool = !offlinePoll && isPayoutExpected && !isPayoutConfirmed(); - List txs = getTxs(updatePool); + boolean checkPool = !offlinePoll && isPayoutExpected && !isPayoutConfirmed(); + List txs = getTxs(checkPool); setDepositTxs(txs); // update payout state - boolean hasValidPayout = false; + boolean hasPayoutTx = false; + MoneroTxWallet payoutTx = null; for (MoneroTxWallet tx : txs) { - boolean isOutgoing = !Boolean.TRUE.equals(tx.isIncoming()); // outgoing tx observed after wallet submits payout or on first confirmation - if (isOutgoing && !tx.isFailed()) { - hasValidPayout = true; - updatePayout(tx); + if (!Boolean.TRUE.equals(tx.isIncoming()) && !tx.isFailed()) { + payoutTx = tx; + hasPayoutTx = true; + break; } else { for (MoneroOutputWallet output : tx.getOutputsWallet()) { - if (Boolean.TRUE.equals(output.isSpent())) hasValidPayout = true; // spent outputs observed on payout published (after rescanning) + if (Boolean.TRUE.equals(output.isSpent())) hasPayoutTx = true; // spent outputs observed on payout published (after rescanning) } } } - - // handle payout validity - if (hasValidPayout) { - onValidPayoutTxPoll(); - } else if (isPayoutPublished() && !offlinePoll) { - onMissingPayoutTxPoll(); - } + if (payoutTx != null) setPayoutTx(payoutTx); + else if (hasPayoutTx) setPayoutStatePublished(); + else if (checkPool && isPayoutPublished()) onPayoutUnseen(); // payout tx seen then lost (e.g. reorg) } } catch (Exception e) { if (!(e instanceof IllegalStateException) && !isShutDownStarted && !wasWalletPolled.get()) { // request connection switch if failure on first poll @@ -3049,11 +2957,11 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } - private List getTxs(boolean updatePool) { + private List getTxs(boolean checkPool) { MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true); - if (!updatePool) query.setInTxPool(false); // avoid updating from pool if possible + if (!checkPool) query.setInTxPool(false); // avoid checking pool if possible List txs = null; - if (!updatePool) txs = wallet.getTxs(query); + if (!checkPool) txs = wallet.getTxs(query); else { synchronized (walletLock) { synchronized (HavenoUtils.getDaemonLock()) { @@ -3064,72 +2972,37 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { return txs; } - private void onValidPayoutTxPoll() { - setPayoutStatePublished(); - synchronized (missingPayoutTxLock) { - handleMissingPayoutTxOnNextPoll = false; - if (missingPayoutTxTimer == null) return; - log.warn("Valid payout tx seen after failed or missing for {} {} with payout state {}", getClass().getSimpleName(), getShortId(), getPayoutState()); - missingPayoutTxTimer.stop(); - missingPayoutTxTimer = null; + private void onPayoutUnseen() { + log.warn("Payout tx unseen for {} {} with payout state {}. Possible reorg?", getClass().getSimpleName(), getShortId(), getPayoutState()); + for (TradePeer peer : getAllPeers()) { + peer.setPaymentReceivedMessage(null); + peer.setPaymentReceivedMessageState(MessageState.UNDEFINED); + peer.setDisputeClosedMessage(null); } - } - - private void onMissingPayoutTxPoll() { - - // throttle processing - if (System.currentTimeMillis() - lastMissingTxPollTime < MIN_MISSING_TX_POLL_INTERVAL_MS) return; - lastMissingTxPollTime = System.currentTimeMillis(); - - // process missing payout tx by id - if (getPayoutTxId() != null) { - log.warn("The payout tx is failed or missing for {} {}, payout state={}, payout id={}. Possibly due to reorg?", getClass().getSimpleName(), getShortId(), getPayoutState(), getPayoutTxId()); - - // buyer can process payout tx received to their main wallet - try { - if (isBuyer()) processBuyerPayout(getPayoutTxId()); - } catch (Exception e) { - log.warn("Error checking for payout tx for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage()); - } - } - - synchronized (missingPayoutTxLock) { - - // handle missing payout tx if previously set - if (handleMissingPayoutTxOnNextPoll) { - handleMissingPayoutTxOnNextPoll = false; - ThreadUtils.execute(() -> handleMissingPayoutTx(), getId()); - } else { - if (missingPayoutTxTimer != null) return; - log.info("Scheduling handling if payout becomes failed or missing for {} {}", getClass().getSimpleName(), getShortId()); - missingPayoutTxTimer = UserThread.runAfter(() -> { - ThreadUtils.execute(() -> { - synchronized (missingPayoutTxLock) { - if (missingPayoutTxTimer == null) return; - missingPayoutTxTimer.stop(); - missingPayoutTxTimer = null; - handleMissingPayoutTxOnNextPoll = true; // handle missing payout tx on next poll in case we're outdated - } - }, getId()); - }, HANDLE_MISSING_PAYOUT_AFTER_MINS, TimeUnit.MINUTES); - } - } - } - - private void handleMissingPayoutTx() { - log.warn("Handling failed or missing payout tx for {} {} with payout state {}. Possibly due to reorg?", getClass().getSimpleName(), getShortId(), getPayoutState()); setPayoutState(PayoutState.PAYOUT_UNPUBLISHED); - onPayoutError(false, null); + 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("Resetting state of {} {} from {} to {} because payout is unpublished", 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("Resetting state of {} {} from {} to {} because payout is unpublished", getClass().getSimpleName(), getId(), getState(), Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT); + setState(State.SELLER_CONFIRMED_PAYMENT_RECEIPT); + setErrorMessage(errorMsg); + } } /** * Handle a payout error due to NACK or the transaction failing (e.g. due to reorg). * * @param syncAndPoll whether to sync and poll - * @param autoMarkPaymentReceived whether to automatically mark payment received if previously confirmed - * @return true if the payment received was auto marked, false otherwise + * @param resendPaymentReceivedMessages whether to resend payment received messages if previously confirmed + * @param paymentReceivedNackSender the peer that sent the payment received NACK, or null if not applicable + * @return true if the payment received were resent, false otherwise */ - public boolean onPayoutError(boolean syncAndPoll, TradePeer paymentReceivedNackSender) { + public boolean onPayoutError(boolean syncAndPoll, boolean resendPaymentReceivedMessages, TradePeer paymentReceivedNackSender) { log.warn("Handling payout error for {} {}", getClass().getSimpleName(), getId()); if (syncAndPoll) { try { @@ -3149,12 +3022,15 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (!isPayoutPublished()) { getSelf().setUnsignedPayoutTxHex(null); setPayoutTxHex(null); + setPayoutTxId(null); } persistNow(null); // send updated payment received message when payout is confirmed - if (paymentReceivedNackSender != null && isSeller()) { + if (resendPaymentReceivedMessages) { + if (!isSeller()) throw new IllegalArgumentException("Only the seller can resend PaymentReceivedMessages after a payout error for " + getClass().getSimpleName() + " " + getId()); + if (!isPaymentReceived()) throw new IllegalStateException("Cannot resend PaymentReceivedMessages after a payout error for " + getClass().getSimpleName() + " " + getId() + " because payment not marked received"); log.warn("Sending updated PaymentReceivedMessages for {} {} after payout error", getClass().getSimpleName(), getId()); ((SellerProtocol) getProtocol()).onPaymentReceived(() -> { log.info("Done sending updated PaymentReceivedMessages on payout error for {} {}", getClass().getSimpleName(), getId()); @@ -3166,13 +3042,6 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { return false; } - private static boolean isSeen(MoneroTx tx) { - if (tx == null) return false; - if (Boolean.TRUE.equals(tx.isFailed())) return false; - if (!Boolean.TRUE.equals(tx.inTxPool()) && !Boolean.TRUE.equals(tx.isConfirmed())) return false; - return true; - } - private static boolean isUnlocked(MoneroTx tx) { if (tx == null) return false; if (tx.getNumConfirmations() == null || tx.getNumConfirmations() < XmrWalletService.NUM_BLOCKS_UNLOCK) return false; @@ -3197,17 +3066,154 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } private void setDepositTxs(List txs) { - MoneroTxWallet makerDepositTx = null; - MoneroTxWallet takerDepositTx = null; - for (MoneroTxWallet tx : txs) { - if (tx.getHash().equals(getMaker().getDepositTxHash())) makerDepositTx = tx; - if (tx.getHash().equals(getTaker().getDepositTxHash())) takerDepositTx = tx; + + // set deposit txs + getMaker().setDepositTx(getValidMakerTx(txs)); + getTaker().setDepositTx(getValidTakerTx(txs)); + + // set actual buyer security deposit + if (isSeen(getBuyer().getDepositTx())) { + BigInteger buyerSecurityDeposit = ((MoneroTxWallet) getBuyer().getDepositTx()).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); + } + getBuyer().setSecurityDeposit(buyerSecurityDeposit); } - getMaker().setDepositTx(makerDepositTx); - getTaker().setDepositTx(takerDepositTx); + + // set actual seller security deposit + if (isSeen(getSeller().getDepositTx())) { + BigInteger sellerSecurityDeposit = ((MoneroTxWallet) getSeller().getDepositTx()).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); + } + getSeller().setSecurityDeposit(sellerSecurityDeposit); + } + + // advance deposit state + if (isSeen(getMaker().getDepositTx()) && (hasBuyerAsTakerWithoutDeposit() || isSeen(getTaker().getDepositTx()))) { + setStateDepositsSeen(); + + // check for deposit txs confirmed + if (getMaker().getDepositTx().isConfirmed() && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().isConfirmed())) { + setStateDepositsConfirmed(); + } + + // check for deposit txs unlocked + if (getMaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().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)) { + setStateDepositsFinalized(); + } + } + + // revert deposit state if necessary + State depositsState = getDepositsState(); + if (!isPaymentSent() && depositsState.ordinal() < getState().ordinal()) { + log.warn("Reverting deposits state to {} for {} {}. Possible reorg?", 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."); + setState(depositsState); + } + + // announce deposits update depositTxsUpdateCounter.set(depositTxsUpdateCounter.get() + 1); } + private MoneroTxWallet getValidMakerTx(List txs) { + for (MoneroTxWallet tx : txs) { + if (tx.getHash().equals(getMaker().getDepositTxHash()) && !Boolean.TRUE.equals(tx.isFailed())) { + return tx; + } + } + return null; + } + + private MoneroTxWallet getValidTakerTx(List txs) { + for (MoneroTxWallet tx : txs) { + if (tx.getHash().equals(getTaker().getDepositTxHash()) && !Boolean.TRUE.equals(tx.isFailed())) { + return tx; + } + } + return null; + } + + private static boolean isSeen(MoneroTx tx) { + if (tx == null) return false; + if (Boolean.TRUE.equals(tx.isFailed())) return false; + if (!Boolean.TRUE.equals(tx.inTxPool()) && !Boolean.TRUE.equals(tx.isConfirmed())) return false; + 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; + return State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS; + } + + public void setPayoutTx(MoneroTx payoutTx) { + + // set payout tx fields + this.payoutTx = payoutTx; + this.payoutTxId = payoutTx.getHash(); + this.payoutTxFee = payoutTx.getFee() == null ? 0 : payoutTx.getFee().longValueExact(); + this.payoutTxKey = payoutTx.getKey(); + if ("".equals(payoutTxId)) this.payoutTxId = null; // tx id is empty until signed + + // set payout tx id in dispute(s) + for (Dispute dispute : getDisputes()) dispute.setDisputePayoutTxId(payoutTxId); + + // set final payout amounts + if (isPaymentReceived()) { + BigInteger splitTxFee = payoutTx.getFee().divide(BigInteger.valueOf(2)); + getBuyer().setPayoutTxFee(splitTxFee); + getSeller().setPayoutTxFee(splitTxFee); + getBuyer().setPayoutAmount(getBuyer().getSecurityDeposit().subtract(getBuyer().getPayoutTxFee()).add(getAmount())); + getSeller().setPayoutAmount(getSeller().getSecurityDeposit().subtract(getSeller().getPayoutTxFee())); + } else { + DisputeResult disputeResult = getDisputeResult(); + if (disputeResult != null) { + BigInteger[] buyerSellerPayoutTxFees = ArbitrationManager.getBuyerSellerPayoutTxCost(disputeResult, payoutTx.getFee()); + getBuyer().setPayoutTxFee(buyerSellerPayoutTxFees[0]); + getSeller().setPayoutTxFee(buyerSellerPayoutTxFees[1]); + getBuyer().setPayoutAmount(disputeResult.getBuyerPayoutAmountBeforeCost().subtract(getBuyer().getPayoutTxFee())); + getSeller().setPayoutAmount(disputeResult.getSellerPayoutAmountBeforeCost().subtract(getSeller().getPayoutTxFee())); + } + } + + // advance payout state + if (Boolean.TRUE.equals(payoutTx.isRelayed()) || Boolean.TRUE.equals(payoutTx.inTxPool())) setPayoutStatePublished(); + if (payoutTx.isConfirmed()) setPayoutStateConfirmed(); + if (payoutTx.getNumConfirmations() != null) { + if (payoutTx.getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK) setPayoutStateUnlocked(); + if (payoutTx.getNumConfirmations() >= NUM_BLOCKS_PAYOUT_FINALIZED) setPayoutStateFinalized(); + } + + // revert payout state if necessary + PayoutState payoutState = getPayoutState(payoutTx); + if (payoutState.ordinal() < getPayoutState().ordinal()) { + log.warn("Reverting payout state to {} for {} {}. Possible reorg?", payoutState, getClass().getSimpleName(), getShortId()); + setPayoutState(payoutState); + } + } + + private static PayoutState getPayoutState(MoneroTx payoutTx) { + if (payoutTx.getHash() == null) return PayoutState.PAYOUT_UNPUBLISHED; + if (Boolean.TRUE.equals(payoutTx.isFailed())) return PayoutState.PAYOUT_UNPUBLISHED; + if (payoutTx.getNumConfirmations() != null) { + if (payoutTx.getNumConfirmations() >= NUM_BLOCKS_PAYOUT_FINALIZED) return PayoutState.PAYOUT_FINALIZED; + if (payoutTx.getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK) return PayoutState.PAYOUT_UNLOCKED; + } + if (payoutTx.isConfirmed()) return PayoutState.PAYOUT_CONFIRMED; + return PayoutState.PAYOUT_PUBLISHED; // payout is published by default in the wallet + } + // TODO: wallet is sometimes missing balance or deposits, due to reorgs, specific daemon connections, not saving? public void recoverIfMissingWalletData() { synchronized (walletLock) { @@ -3333,7 +3339,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } private void setStateDepositsSeen() { - if (!isDepositsPublished()) setState(State.DEPOSIT_TXS_SEEN_IN_NETWORK); + if (getState().ordinal() < State.DEPOSIT_TXS_SEEN_IN_NETWORK.ordinal()) setState(State.DEPOSIT_TXS_SEEN_IN_NETWORK); } private void setStateDepositsConfirmed() { diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index 19c0c0eef1..95380554c5 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -172,7 +172,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi private final PersistenceManager> persistenceManager; private final TradableList tradableList = new TradableList<>(); @Getter - private final BooleanProperty persistedTradesInitialized = new SimpleBooleanProperty(); + private final BooleanProperty tradesInitialized = new SimpleBooleanProperty(); @Getter private final LongProperty numPendingTrades = new SimpleLongProperty(); private final ReferralIdService referralIdService; @@ -454,18 +454,21 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi // remove skipped trades trades.removeAll(tradesToSkip); - // sync idle trades once in background after active trades + // arbitrator syncs idle trades once in background after active trades for (Trade trade : trades) { - if (trade.isIdling()) ThreadUtils.submitToPool(() -> { + if (!trade.isArbitrator()) continue; + if (trade.isIdling()) { + ThreadUtils.submitToPool(() -> { - // add random delay to avoid syncing at exactly the same time - if (trades.size() > 1 && trade.walletExists()) { - int delay = (int) (Math.random() * INIT_TRADE_RANDOM_DELAY_MS); - HavenoUtils.waitFor(delay); - } - - trade.syncAndPollWallet(); - }); + // add random delay to avoid syncing at exactly the same time + if (trades.size() > 1 && trade.walletExists()) { + int delay = (int) (Math.random() * INIT_TRADE_RANDOM_DELAY_MS); + HavenoUtils.waitFor(delay); + } + + trade.syncAndPollWallet(); + }); + } } // process after all wallets initialized @@ -494,7 +497,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi // notify that persisted trades initialized if (isShutDownStarted) return; - persistedTradesInitialized.set(true); + tradesInitialized.set(true); getObservableList().addListener((ListChangeListener) change -> onTradesChanged()); onTradesChanged(); @@ -1306,8 +1309,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } } - public BooleanProperty persistedTradesInitializedProperty() { - return persistedTradesInitialized; + public BooleanProperty tradesInitializedProperty() { + return tradesInitialized; } public boolean isMyOffer(Offer offer) { diff --git a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java index be4a3a7beb..b4c809a20e 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -711,7 +711,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D // handle payout error lastAckedPaymentReceivedMessage = message; - trade.onPayoutError(false, null); + trade.onPayoutError(false, false, null); handleTaskRunnerFault(peer, message, null, errorMessage, trade.getSelf().getUpdatedMultisigHex()); // send nack } }))) @@ -812,10 +812,12 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D peer.setNodeAddress(sender); } - // TODO: arbitrator may nack maker's InitTradeRequest if reserve tx has become invalid (e.g. check_tx_key shows 0 funds received). recreate reserve tx in this case + // handle nack of InitTradeRequest from arbitrator to maker if (!ackMessage.isSuccess() && trade.isMaker() && peer == trade.getArbitrator() && ackMessage.getSourceMsgClassName().equals(InitTradeRequest.class.getSimpleName())) { - if (ackMessage.getErrorMessage() != null && ackMessage.getErrorMessage().contains(SEND_INIT_TRADE_REQUEST_FAILED)) { + if (ignoreInitTradeRequestNackFromArbitrator(ackMessage)) { + log.warn("Ignoring InitTradeRequest NACK from arbitrator, offerId={}, errorMessage={}", processModel.getOfferId(), ackMessage.getErrorMessage()); // use default postprocessing + } else { if (makerInitTradeRequestHasBeenNacked) { handleSecondMakerInitTradeRequestNack(ackMessage); // use default postprocessing @@ -892,7 +894,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D if (ackMessage.getUpdatedMultisigHex() != null) { trade.getBuyer().setUpdatedMultisigHex(ackMessage.getUpdatedMultisigHex()); processModel.getTradeManager().persistNow(null); - boolean autoResent = onPayoutError(true, peer); + boolean autoResent = onPaymentReceivedNack(true, peer); if (autoResent) return; // skip remaining processing if auto resent } } @@ -911,7 +913,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D if (ackMessage.getUpdatedMultisigHex() != null) { trade.getArbitrator().setUpdatedMultisigHex(ackMessage.getUpdatedMultisigHex()); processModel.getTradeManager().persistNow(null); - boolean autoResent = onPayoutError(true, peer); + boolean autoResent = onPaymentReceivedNack(true, peer); if (autoResent) return; // skip remaining processing if auto resent } } @@ -939,17 +941,23 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D trade.onAckMessage(ackMessage, sender); } - private boolean onPayoutError(boolean syncAndPoll, TradePeer peer) { + private static boolean ignoreInitTradeRequestNackFromArbitrator(AckMessage ackMessage) { + return ackMessage.getErrorMessage() != null && ackMessage.getErrorMessage().contains(SEND_INIT_TRADE_REQUEST_FAILED); // ignore if arbitrator's request failed to taker + } + + private boolean onPaymentReceivedNack(boolean syncAndPoll, TradePeer peer) { // prevent infinite nack loop with max attempts numPaymentReceivedNacks++; if (numPaymentReceivedNacks > MAX_PAYMENT_RECEIVED_NACKS) { - log.warn("Maximum number of PaymentReceivedMessage NACKs reached for {} {}, not retrying", trade.getClass().getSimpleName(), trade.getId()); + String errorMsg = "The maximum number of attempts to process the payment confirmation has been reached for " + trade.getClass().getSimpleName() + " " + trade.getId() + ". Restart the application to try again."; + log.warn(errorMsg); + trade.setErrorMessage(errorMsg); return false; } // handle payout error - return trade.onPayoutError(syncAndPoll, peer); + return trade.onPayoutError(syncAndPoll, true, peer); } private void handleFirstMakerInitTradeRequestNack(AckMessage ackMessage) { diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java index 05fee1374a..b4562874e3 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java @@ -90,7 +90,7 @@ public class BuyerPreparePaymentSentMessage extends TradeTask { // create payout tx log.info("Buyer creating unsigned payout tx for {} {} ", trade.getClass().getSimpleName(), trade.getShortId()); MoneroTxWallet payoutTx = trade.createPayoutTx(); - trade.updatePayout(payoutTx); + trade.setPayoutTx(payoutTx); trade.getSelf().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); trade.requestPersistence(); } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java index 13273dfbc4..adff1a9848 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java @@ -41,6 +41,7 @@ import haveno.core.trade.ArbitratorTrade; import haveno.core.trade.BuyerTrade; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; +import haveno.core.trade.Trade.State; import haveno.core.trade.messages.PaymentReceivedMessage; import haveno.core.trade.messages.PaymentSentMessage; import haveno.core.util.Validator; @@ -82,6 +83,9 @@ public class ProcessPaymentReceivedMessage extends TradeTask { return; } + // set state to confirmed payment receipt before processing + trade.advanceState(State.SELLER_CONFIRMED_PAYMENT_RECEIPT); + // cannot process until wallet sees deposits unlocked if (!trade.isDepositsUnlocked()) { trade.syncAndPollWallet(); @@ -179,9 +183,6 @@ public class ProcessPaymentReceivedMessage extends TradeTask { else throw e; } } - } else { - log.info("Payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId()); - if (message.getSignedPayoutTxHex() != null && !trade.isPayoutConfirmed()) trade.processPayoutTx(message.getSignedPayoutTxHex(), false, true); } } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java index cc276e0d58..6e49617724 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java @@ -112,9 +112,14 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask { private void createUnsignedPayoutTx() { log.info("Seller creating unsigned payout tx for trade {}", trade.getId()); - trade.getProcessModel().setPaymentSentPayoutTxStale(true); - MoneroTxWallet payoutTx = trade.createPayoutTx(); - trade.updatePayout(payoutTx); - trade.getSelf().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); + try { + trade.getProcessModel().setPaymentSentPayoutTxStale(true); + MoneroTxWallet payoutTx = trade.createPayoutTx(); + trade.setPayoutTx(payoutTx); + trade.getSelf().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); + } catch (Exception e) { + if (trade.isPayoutPublished()) log.info("Payout tx already published for {} {}", trade.getClass().getName(), trade.getId()); + else throw e; + } } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java index c930ad0924..34bb458c1a 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java @@ -255,6 +255,8 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag if (isMessageReceived()) return true; // stop if message received if (!trade.isPaymentReceived()) return true; // stop if trade state reset if (trade.isPayoutPublished() && !((SellerTrade) trade).resendPaymentReceivedMessagesWithinDuration()) return true; // stop if payout is published and we are not in the resend period + + // check if message state is outdated if (unsignedPayoutTxHex != null && !StringUtils.equals(unsignedPayoutTxHex, trade.getSelf().getUnsignedPayoutTxHex())) return true; if (signedPayoutTxHex != null && !StringUtils.equals(signedPayoutTxHex, trade.getPayoutTxHex())) return true; if (updatedMultisigHex != null && !StringUtils.equals(updatedMultisigHex, trade.getSelf().getUpdatedMultisigHex())) return true; diff --git a/desktop/src/main/java/haveno/desktop/main/MainViewModel.java b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java index 0029d46bbb..473b18d331 100644 --- a/desktop/src/main/java/haveno/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java @@ -217,7 +217,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener @Override public void onSetupComplete() { // We handle the trade period here as we display a global popup if we reached dispute time - tradesAndUIReady = EasyBind.combine(isSplashScreenRemoved, tradeManager.persistedTradesInitializedProperty(), + tradesAndUIReady = EasyBind.combine(isSplashScreenRemoved, tradeManager.tradesInitializedProperty(), (a, b) -> a && b); tradesAndUIReady.subscribe((observable, oldValue, newValue) -> { if (newValue) { diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java index 6604271799..8bbc7a7371 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java @@ -27,11 +27,8 @@ import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.network.MessageState; import haveno.core.offer.Offer; import haveno.core.offer.OfferUtil; -import haveno.core.trade.ArbitratorTrade; -import haveno.core.trade.BuyerTrade; import haveno.core.trade.ClosedTradableManager; import haveno.core.trade.HavenoUtils; -import haveno.core.trade.SellerTrade; import haveno.core.trade.Trade; import haveno.core.trade.TradeUtil; import haveno.core.user.User; @@ -363,7 +360,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel