diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index 0f51f1be2b..c91fb0e325 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -410,6 +410,16 @@ public final class XmrConnectionService { if (!isSyncedWithinTolerance()) throw new RuntimeException("Monero node is not synced"); } + public Long getHeight() { + if (lastInfo == null) return null; + return lastInfo.getHeight(); + } + + public Long getTargetHeight() { + if (lastInfo == null) return null; + return lastInfo.getTargetHeight() == 0 ? lastInfo.getHeight() : lastInfo.getTargetHeight(); + } + public boolean isSyncedWithinTolerance() { Long targetHeight = getTargetHeight(); if (targetHeight == null) return false; @@ -417,11 +427,6 @@ public final class XmrConnectionService { return false; } - public Long getTargetHeight() { - if (lastInfo == null) return null; - return lastInfo.getTargetHeight() == 0 ? chainHeight.get() : lastInfo.getTargetHeight(); // monerod sync_info's target_height returns 0 when node is fully synced - } - public XmrKeyImagePoller getKeyImagePoller() { synchronized (lock) { if (keyImagePoller == null) keyImagePoller = new XmrKeyImagePoller(); @@ -738,7 +743,7 @@ public final class XmrConnectionService { keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs()); // update polling - doPollMonerod(); + tryPollMonerod(); if (currentConnection != getConnection()) return; // polling can change connection UserThread.runAfter(() -> updatePolling(), getInternalRefreshPeriodMs() / 1000); @@ -759,7 +764,11 @@ public final class XmrConnectionService { private void startPolling() { synchronized (lock) { if (monerodPollLooper != null) monerodPollLooper.stop(); - monerodPollLooper = new TaskLooper(() -> pollMonerod()); + monerodPollLooper = new TaskLooper(() -> { + if (!pollInProgress) { + tryPollMonerod(); + } + }); monerodPollLooper.start(getInternalRefreshPeriodMs()); } } @@ -773,12 +782,18 @@ public final class XmrConnectionService { } } - private void pollMonerod() { - if (pollInProgress) return; - doPollMonerod(); + private void tryPollMonerod() { + try { + pollMonerod(); + } catch (Exception e) { + // error is already handled + } } - private void doPollMonerod() { + /** + * Polls monerod for the latest info and updates the connection if necessary. + */ + private void pollMonerod() { synchronized (pollLock) { pollInProgress = true; if (isShutDownStarted) return; @@ -840,6 +855,9 @@ public final class XmrConnectionService { isConnected = true; connectionServiceFallbackType.set(null); + // set chain height + chainHeight.set(lastInfo.getHeight()); + // determine if blockchain is syncing locally boolean blockchainSyncing = lastInfo.getHeight().equals(lastInfo.getHeightWithoutBootstrap()) || (lastInfo.getTargetHeight().equals(0l) && lastInfo.getHeightWithoutBootstrap().equals(0l)); // blockchain is syncing if height equals height without bootstrap, or target height and height without bootstrap both equal 0 @@ -848,7 +866,7 @@ public final class XmrConnectionService { // throttle warnings if monerod not synced if (!isSyncedWithinTolerance() && System.currentTimeMillis() - lastLogMonerodNotSyncedTimestamp > HavenoUtils.LOG_MONEROD_NOT_SYNCED_WARN_PERIOD_MS) { - log.warn("Our chain height: {} is out of sync with peer nodes chain height: {}", chainHeight.get(), getTargetHeight()); + log.warn("Our chain height: {} is out of sync with peer nodes chain height: {}", getHeight(), getTargetHeight()); lastLogMonerodNotSyncedTimestamp = System.currentTimeMillis(); } @@ -862,12 +880,9 @@ public final class XmrConnectionService { // get the number of connections, which is only available if not restricted int numOutgoingConnections = Boolean.TRUE.equals(lastInfo.isRestricted()) ? -1 : lastInfo.getNumOutgoingConnections(); - // update properties on user thread + // updates on user thread UserThread.execute(() -> { - // set chain height - chainHeight.set(lastInfo.getHeight()); - // update sync progress boolean isTestnet = Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_LOCAL; if (lastInfo.isSynchronized() || isTestnet) doneDownload(); // TODO: skipping synchronized check for testnet because CI tests do not sync 3rd local node, see "Can manage Monero daemon connections" @@ -925,6 +940,7 @@ public final class XmrConnectionService { // set error message getConnectionServiceErrorMsg().set(errorMsg); + throw e; } finally { pollInProgress = false; } 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 4e43f7ed79..52ae11acd8 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 @@ -491,7 +491,8 @@ public final class ArbitrationManager extends DisputeManager { stateProperty.set(state); @@ -2010,7 +2011,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { Long minDepositTxConfirmations = getMinDepositTxConfirmations(); if (minDepositTxConfirmations != null && minDepositTxConfirmations >= NUM_BLOCKS_DEPOSITS_FINALIZED) { log.info("Auto-advancing state to {} for {} {} because deposits are unlocked and have at least {} confirmations", State.DEPOSIT_TXS_FINALIZED_IN_BLOCKCHAIN, this.getClass().getSimpleName(), getShortId(), NUM_BLOCKS_DEPOSITS_FINALIZED); - setState(State.DEPOSIT_TXS_FINALIZED_IN_BLOCKCHAIN); + setStateDepositsFinalized(); } } } @@ -2029,15 +2030,14 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } public void setPayoutState(PayoutState payoutState) { + if (payoutState.ordinal() < this.payoutState.ordinal()) { + log.warn("Reverting payout state from {} to {} for trade {} {}. Possible reorg?", this.payoutState, payoutState, this.getClass().getSimpleName(), getShortId()); + } + if (isInitialized) { // We don't want to log at startup the setState calls from all persisted trades log.info("Set new payout state for trade {} {}: {}", getShortId(), this.getClass().getSimpleName(), payoutState); } - 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.payoutState + ". New payout state is: " + payoutState; - log.warn(message); - } this.payoutState = payoutState; persistNow(null); @@ -2884,11 +2884,19 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // set deposit txs from trade wallet List txs = getTxs(false); - if (getValidMakerTx(txs) != null && (getValidTakerTx(txs) != null || hasBuyerAsTakerWithoutDeposit())) { - setDepositTxs(txs); + if (hasDepositTxs(txs)) { + setDepositTxs(txs, false); } else if (!offlinePoll) { - txs = getTxs(true); // check pool if deposits not found - setDepositTxs(txs); + txs = getTxs(true); + + // txs may not be fetched if confirmed after last sync + if (isDepositsPublished() && !hasDepositTxs(txs)) { + log.info("Deposits are missing for {} {} after being published, resyncing", getClass().getSimpleName(), getId()); + HavenoUtils.waitFor(MISSING_TXS_DELAY_MS); + sync(); + txs = getTxs(true); + } + setDepositTxs(txs, true); } } @@ -2914,26 +2922,23 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // get txs from trade wallet boolean checkPool = !offlinePoll && isPayoutExpected && !isPayoutConfirmed(); List txs = getTxs(checkPool); - setDepositTxs(txs); - // update payout state - boolean hasPayoutTx = false; - MoneroTxWallet payoutTx = null; - for (MoneroTxWallet tx : txs) { - 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())) hasPayoutTx = true; // spent outputs observed on payout published (after rescanning) - } - } + // txs may not be fetched if confirmed after last sync + if (!offlinePoll && isPayoutPublished() && getPayoutTxId() != null && !hasPayoutTx(txs)) { + log.info("Payout is missing for {} {} after being published, resyncing", getClass().getSimpleName(), getId()); + HavenoUtils.waitFor(MISSING_TXS_DELAY_MS); + sync(); + txs = getTxs(true); + checkPool = true; } - if (payoutTx != null) setPayoutTx(payoutTx); - else if (hasPayoutTx) setPayoutStatePublished(); - else if (checkPool && isPayoutPublished()) onPayoutUnseen(); // payout tx seen then lost (e.g. reorg) + + // set deposit and payout txs + setDepositTxs(txs, checkPool); + setPayoutTx(txs, checkPool); } + + // update trade period if applicable + maybeUpdateTradePeriod(); } catch (Exception e) { if (!(e instanceof IllegalStateException) && !isShutDownStarted && !wasWalletPolled.get()) { // request connection switch if failure on first poll ThreadUtils.execute(() -> requestSwitchToNextBestConnection(sourceConnection), getId()); @@ -2961,41 +2966,184 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } + private boolean isWalletBehind() { + return walletHeight.get() < xmrConnectionService.getTargetHeight(); + } + + private boolean syncWalletIfBehind() { + synchronized (walletLock) { + if (isWalletBehind()) { + syncWithProgress(); + walletHeight.set(wallet.getHeight()); + return true; + } else { + return false; + } + } + } + + public MoneroSyncResult sync() { + synchronized (walletLock) { + log.info("Syncing wallet directly for {} {}", getClass().getSimpleName(), getShortId()); + MoneroSyncResult result = super.sync(); + log.info("Done syncing wallet directly for {} {}", getClass().getSimpleName(), getShortId()); + return result; + } + } + private List getTxs(boolean checkPool) { MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true); - if (!checkPool) query.setInTxPool(false); // avoid checking pool if possible - List txs = null; - if (!checkPool) txs = wallet.getTxs(query); - else { + if (!checkPool) query.setInTxPool(false); + if (checkPool) { synchronized (walletLock) { synchronized (HavenoUtils.getDaemonLock()) { - txs = wallet.getTxs(query); + return wallet.getTxs(query); + } + } + } else { + return wallet.getTxs(query); + } + } + + private void setDepositTxs(List txs, boolean poolChecked) { + + // set deposit txs + getMaker().setDepositTx(getMakerDepositTx(txs)); + getTaker().setDepositTx(getTakerDepositTx(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); + } + + // 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(); + 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 + } + + // announce deposits update + depositTxsUpdateCounter.set(depositTxsUpdateCounter.get() + 1); + } + + private void setPayoutTx(List txs, boolean poolChecked) { + + // collect payout info + boolean hasPayoutTx = false; + MoneroTxWallet payoutTx = null; + for (MoneroTxWallet tx : txs) { + 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())) hasPayoutTx = true; // spent outputs observed on payout published (after rescanning) } } } - return txs; + + // set payout state + 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); + } + } } - 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); + 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())); + } } - 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("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); + + // 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 + if (getPayoutState() != getPayoutState(payoutTx)) setPayoutState(getPayoutState(payoutTx)); } /** @@ -3046,6 +3194,17 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { return false; } + private boolean hasDepositTxs(List txs) { + return getMakerDepositTx(txs) != null && (getTakerDepositTx(txs) != null || hasBuyerAsTakerWithoutDeposit()); + } + + private boolean hasPayoutTx(List txs) { + for (MoneroTxWallet tx : txs) { + if (tx.getHash().equals(getPayoutTxId())) return true; + } + return false; + } + private static boolean isUnlocked(MoneroTx tx) { if (tx == null) return false; if (tx.getNumConfirmations() == null || tx.getNumConfirmations() < XmrWalletService.NUM_BLOCKS_UNLOCK) return false; @@ -3056,87 +3215,17 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { return isUnlocked(getMaker().getDepositTx()) || isUnlocked(getTaker().getDepositTx()); } - private void syncWalletIfBehind() { - synchronized (walletLock) { - if (isWalletBehind()) { - syncWithProgress(); - walletHeight.set(wallet.getHeight()); - } - } + private MoneroTxWallet getMakerDepositTx(List txs) { + return getValidDepositTx(txs, getMaker()); } - private boolean isWalletBehind() { - return walletHeight.get() < xmrConnectionService.getTargetHeight(); + private MoneroTxWallet getTakerDepositTx(List txs) { + return getValidDepositTx(txs, getTaker()); } - private void setDepositTxs(List txs) { - - // 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); - } - - // 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) { + private MoneroTxWallet getValidDepositTx(List txs, TradePeer peer) { 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())) { + if (tx.getHash().equals(peer.getDepositTxHash()) && !Boolean.TRUE.equals(tx.isFailed())) { return tx; } } @@ -3161,52 +3250,6 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { 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; @@ -3215,7 +3258,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { 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 + if (Boolean.TRUE.equals(payoutTx.isRelayed()) || Boolean.TRUE.equals(payoutTx.inTxPool())) return PayoutState.PAYOUT_PUBLISHED; + return PayoutState.PAYOUT_UNPUBLISHED; } // TODO: wallet is sometimes missing balance or deposits, due to reorgs, specific daemon connections, not saving? @@ -3355,9 +3399,11 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } private void setStateDepositsFinalized() { - if (!isDepositsFinalized()) { - setStateIfValidTransitionTo(State.DEPOSIT_TXS_FINALIZED_IN_BLOCKCHAIN); - ThreadUtils.submitToPool(() -> maybeUpdateTradePeriod()); + if (!isDepositsFinalized()) setState(State.DEPOSIT_TXS_FINALIZED_IN_BLOCKCHAIN); + try { + maybeUpdateTradePeriod(); + } catch (Exception e) { + log.warn("Error updating trade period after deposits finalized for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage()); } } diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java index 7baf9d8fa7..75853354d2 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java @@ -3,8 +3,14 @@ package haveno.core.xmr.wallet; import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import haveno.common.Timer; import haveno.common.UserThread; @@ -20,13 +26,14 @@ import monero.common.TaskLooper; import monero.daemon.model.MoneroTx; import monero.wallet.MoneroWallet; import monero.wallet.MoneroWalletFull; +import monero.wallet.model.MoneroSyncResult; import monero.wallet.model.MoneroWalletListener; @Slf4j public abstract class XmrWalletBase { // constants - public static final int SYNC_PROGRESS_TIMEOUT_SECONDS = 180; + public static final int SYNC_TIMEOUT_SECONDS = 180; public static final int DIRECT_SYNC_WITHIN_BLOCKS = 100; public static final int SAVE_WALLET_DELAY_SECONDS = 300; private static final String SYNC_PROGRESS_TIMEOUT_MSG = "Sync progress timeout called"; @@ -63,6 +70,40 @@ public abstract class XmrWalletBase { this.xmrConnectionService = HavenoUtils.xmrConnectionService; } + public MoneroSyncResult sync() { + return syncWithTimeout(SYNC_TIMEOUT_SECONDS); + } + + public MoneroSyncResult syncWithTimeout(long timeout) { + synchronized (walletLock) { + synchronized (HavenoUtils.getDaemonLock()) { + ExecutorService executor = Executors.newSingleThreadExecutor(); + + Callable task = () -> { + MoneroSyncResult result = wallet.sync(); + walletHeight.set(wallet.getHeight()); + return result; + }; + + Future future = executor.submit(task); + + try { + return future.get(timeout, TimeUnit.SECONDS); + } catch (TimeoutException e) { + future.cancel(true); + throw new RuntimeException("Sync timed out after " + timeout + " seconds", e); + } catch (ExecutionException e) { + throw new RuntimeException("Sync failed", e.getCause()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); // restore interrupt status + throw new RuntimeException("Sync was interrupted", e); + } finally { + executor.shutdownNow(); + } + } + } + } + public void syncWithProgress() { syncWithProgress(false); } @@ -223,7 +264,7 @@ public abstract class XmrWalletBase { if (isShutDownStarted) return; syncProgressError = new RuntimeException(SYNC_PROGRESS_TIMEOUT_MSG); syncProgressLatch.countDown(); - }, SYNC_PROGRESS_TIMEOUT_SECONDS, TimeUnit.SECONDS); + }, SYNC_TIMEOUT_SECONDS, TimeUnit.SECONDS); } private void setWalletSyncedWithProgress() {