From ca2d7704abc78dc5c05fcae4165c6814b2da4f29 Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 18 Apr 2024 14:02:22 -0400 Subject: [PATCH] fixes from congestion testing - refactor main wallet polling - restart main wallet if connection changes before initial sync - use cached wallet state throughout app - avoid rescanning spent outputs until payout tx expected - allow payment sent/received buttons to be clicked until arrived - apply timeout to payment sent/received buttons - load DepositView asynchronously - remove separate timeout from OpenOffer - tolerate error importing multisig hex until necessary --- Makefile | 27 +- .../haveno/core/api/XmrConnectionService.java | 2 +- .../haveno/core/offer/OfferBookService.java | 44 +- .../java/haveno/core/offer/OpenOffer.java | 32 - .../haveno/core/offer/OpenOfferManager.java | 4 +- .../core/support/dispute/DisputeManager.java | 1 + .../main/java/haveno/core/trade/Trade.java | 53 +- .../java/haveno/core/trade/TradeManager.java | 2 +- .../core/trade/protocol/BuyerProtocol.java | 6 +- .../core/trade/protocol/SellerProtocol.java | 6 +- .../ProcessDepositsConfirmedMessage.java | 14 +- .../tasks/ProcessPaymentReceivedMessage.java | 1 - .../tasks/ProcessPaymentSentMessage.java | 8 +- .../tasks/SellerPublishDepositTx.java | 71 -- .../main/java/haveno/core/xmr/Balances.java | 2 +- .../core/xmr/setup/DownloadListener.java | 2 +- .../core/xmr/wallet/XmrWalletService.java | 919 +++++++++++------- .../haveno/desktop/main/debug/DebugView.java | 3 - .../main/funds/deposit/DepositView.java | 265 ++--- .../steps/buyer/BuyerStep2View.java | 5 +- .../steps/seller/SellerStep3View.java | 12 +- docs/deployment-guide.md | 3 + 22 files changed, 802 insertions(+), 680 deletions(-) delete mode 100644 core/src/main/java/haveno/core/trade/protocol/tasks/SellerPublishDepositTx.java diff --git a/Makefile b/Makefile index 51ee87c2b1..56bda5ab72 100644 --- a/Makefile +++ b/Makefile @@ -112,16 +112,6 @@ monerod3-local: --fixed-difficulty 500 \ --disable-rpc-ban \ -funding-wallet-stagenet: - ./.localnet/monero-wallet-rpc \ - --stagenet \ - --rpc-bind-port 18084 \ - --rpc-login rpc_user:abc123 \ - --rpc-access-control-origins http://localhost:8080 \ - --wallet-dir ./.localnet \ - --daemon-ssl-allow-any-cert \ - --daemon-address http://127.0.0.1:38081 \ - #--proxy 127.0.0.1:49775 \ funding-wallet-local: @@ -133,6 +123,23 @@ funding-wallet-local: --rpc-access-control-origins http://localhost:8080 \ --wallet-dir ./.localnet \ +funding-wallet-stagenet: + ./.localnet/monero-wallet-rpc \ + --stagenet \ + --rpc-bind-port 38084 \ + --rpc-login rpc_user:abc123 \ + --rpc-access-control-origins http://localhost:8080 \ + --wallet-dir ./.localnet \ + --daemon-ssl-allow-any-cert \ + --daemon-address http://127.0.0.1:38081 \ + +funding-wallet-mainnet: + ./.localnet/monero-wallet-rpc \ + --rpc-bind-port 18084 \ + --rpc-login rpc_user:abc123 \ + --rpc-access-control-origins http://localhost:8080 \ + --wallet-dir ./.localnet \ + # use .bat extension for windows binaries APP_EXT := ifeq ($(OS),Windows_NT) diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index 642eee9a77..518a00aac2 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -607,7 +607,7 @@ public final class XmrConnectionService { long targetHeight = lastInfo.getTargetHeight(); long blocksLeft = targetHeight - lastInfo.getHeight(); if (syncStartHeight == null) syncStartHeight = lastInfo.getHeight(); - double percent = targetHeight == syncStartHeight ? 1.0 : ((double) Math.max(1, lastInfo.getHeight() - syncStartHeight) / (double) (targetHeight - syncStartHeight)) * 100d; // grant at least 1 block to show progress + double percent = Math.min(1.0, targetHeight == syncStartHeight ? 1.0 : ((double) Math.max(1, lastInfo.getHeight() - syncStartHeight) / (double) (targetHeight - syncStartHeight))); // grant at least 1 block to show progress downloadListener.progress(percent, blocksLeft, null); } diff --git a/core/src/main/java/haveno/core/offer/OfferBookService.java b/core/src/main/java/haveno/core/offer/OfferBookService.java index dcea8475d8..5453da8656 100644 --- a/core/src/main/java/haveno/core/offer/OfferBookService.java +++ b/core/src/main/java/haveno/core/offer/OfferBookService.java @@ -116,18 +116,16 @@ public class OfferBookService { @Override public void onAdded(Collection protectedStorageEntries) { protectedStorageEntries.forEach(protectedStorageEntry -> { - synchronized (offerBookChangedListeners) { - offerBookChangedListeners.forEach(listener -> { - if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) { - OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload(); - maybeInitializeKeyImagePoller(); - keyImagePoller.addKeyImages(offerPayload.getReserveTxKeyImages()); - Offer offer = new Offer(offerPayload); - offer.setPriceFeedService(priceFeedService); - setReservedFundsSpent(offer); - listener.onAdded(offer); - } - }); + if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) { + OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload(); + maybeInitializeKeyImagePoller(); + keyImagePoller.addKeyImages(offerPayload.getReserveTxKeyImages()); + Offer offer = new Offer(offerPayload); + offer.setPriceFeedService(priceFeedService); + setReservedFundsSpent(offer); + synchronized (offerBookChangedListeners) { + offerBookChangedListeners.forEach(listener -> listener.onAdded(offer)); + } } }); } @@ -135,18 +133,16 @@ public class OfferBookService { @Override public void onRemoved(Collection protectedStorageEntries) { protectedStorageEntries.forEach(protectedStorageEntry -> { - synchronized (offerBookChangedListeners) { - offerBookChangedListeners.forEach(listener -> { - if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) { - OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload(); - maybeInitializeKeyImagePoller(); - keyImagePoller.removeKeyImages(offerPayload.getReserveTxKeyImages()); - Offer offer = new Offer(offerPayload); - offer.setPriceFeedService(priceFeedService); - setReservedFundsSpent(offer); - listener.onRemoved(offer); - } - }); + if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) { + OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload(); + maybeInitializeKeyImagePoller(); + keyImagePoller.removeKeyImages(offerPayload.getReserveTxKeyImages()); + Offer offer = new Offer(offerPayload); + offer.setPriceFeedService(priceFeedService); + setReservedFundsSpent(offer); + synchronized (offerBookChangedListeners) { + offerBookChangedListeners.forEach(listener -> listener.onRemoved(offer)); + } } }); } diff --git a/core/src/main/java/haveno/core/offer/OpenOffer.java b/core/src/main/java/haveno/core/offer/OpenOffer.java index 5faaedaa4b..a3ae9a18a6 100644 --- a/core/src/main/java/haveno/core/offer/OpenOffer.java +++ b/core/src/main/java/haveno/core/offer/OpenOffer.java @@ -34,8 +34,6 @@ package haveno.core.offer; -import haveno.common.Timer; -import haveno.common.UserThread; import haveno.common.proto.ProtoUtil; import haveno.core.trade.Tradable; import javafx.beans.property.ObjectProperty; @@ -55,9 +53,6 @@ import java.util.Optional; @EqualsAndHashCode @Slf4j public final class OpenOffer implements Tradable { - // Timeout for offer reservation during takeoffer process. If deposit tx is not completed in that time we reset the offer to AVAILABLE state. - private static final long TIMEOUT = 60; - transient private Timer timeoutTimer; public enum State { SCHEDULED, @@ -227,13 +222,6 @@ public final class OpenOffer implements Tradable { public void setState(State state) { this.state = state; stateProperty.set(state); - - // We keep it reserved for a limited time, if trade preparation fails we revert to available state - if (this.state == State.RESERVED) { // TODO (woodser): remove this? - startTimeout(); - } else { - stopTimeout(); - } } public ReadOnlyObjectProperty stateProperty() { @@ -252,26 +240,6 @@ public final class OpenOffer implements Tradable { return state == State.DEACTIVATED; } - private void startTimeout() { - stopTimeout(); - - timeoutTimer = UserThread.runAfter(() -> { - log.debug("Timeout for resetting State.RESERVED reached"); - if (state == State.RESERVED) { - // we do not need to persist that as at startup any RESERVED state would be reset to AVAILABLE anyway - setState(State.AVAILABLE); - } - }, TIMEOUT); - } - - private void stopTimeout() { - if (timeoutTimer != null) { - timeoutTimer.stop(); - timeoutTimer = null; - } - } - - @Override public String toString() { return "OpenOffer{" + diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 3fbaaeb4e5..8ebf1d1bfd 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -971,8 +971,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // return if awaiting scheduled tx if (openOffer.getScheduledTxHashes() != null) return null; - // cache all transactions including from pool - List allTxs = xmrWalletService.getWallet().getTxs(new MoneroTxQuery().setIncludeOutputs(true)); + // get all transactions including from pool + List allTxs = xmrWalletService.getTransactions(false); if (preferredSubaddressIndex != null) { 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 0b7bb675a1..ac454d6b6f 100644 --- a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java @@ -766,6 +766,7 @@ public abstract class DisputeManager> extends Sup trade.getSelf().getUpdatedMultisigHex(), receiver.getUnsignedPayoutTxHex(), // include dispute payout tx if arbitrator has their updated multisig info deferPublishPayout); // instruct trader to defer publishing payout tx because peer is expected to publish imminently + receiverPeer.setDisputeClosedMessage(disputeClosedMessage); // send dispute closed message log.info("Send {} to trader {}. tradeId={}, {}.uid={}, chatMessage.uid={}", diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 777af89294..f90e3049b8 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -413,7 +413,7 @@ public abstract class Trade implements Tradable, Model { transient private Subscription tradeStateSubscription; transient private Subscription tradePhaseSubscription; transient private Subscription payoutStateSubscription; - transient private TaskLooper txPollLooper; + transient private TaskLooper pollLooper; transient private Long walletRefreshPeriodMs; transient private Long syncNormalStartTimeMs; @@ -890,6 +890,10 @@ public abstract class Trade implements Tradable, Model { public void saveWallet() { synchronized (walletLock) { + if (!walletExists()) { + log.warn("Cannot save wallet for {} {} because it does not exist", getClass().getSimpleName(), getId()); + return; + } if (wallet == null) throw new RuntimeException("Trade wallet is not open for trade " + getId()); xmrWalletService.saveWallet(wallet); maybeBackupWallet(); @@ -1195,7 +1199,7 @@ public abstract class Trade implements Tradable, Model { return trader.getDepositTx(); } catch (MoneroError e) { log.error("Error getting {} deposit tx {}: {}", getPeerRole(trader), depositId, e.getMessage()); // TODO: peer.getRole() - return null; + throw e; } } @@ -1264,9 +1268,12 @@ public abstract class Trade implements Tradable, Model { // TODO: clear other process data setPayoutTxHex(null); - for (TradePeer peer : getPeers()) { + for (TradePeer peer : getAllTradeParties()) { peer.setUnsignedPayoutTxHex(null); peer.setUpdatedMultisigHex(null); + peer.setDisputeClosedMessage(null); + peer.setPaymentSentMessage(null); + peer.setPaymentReceivedMessage(null); } } @@ -1597,11 +1604,16 @@ public abstract class Trade implements Tradable, Model { } private List getPeers() { + List peers = getAllTradeParties(); + if (!peers.remove(getSelf())) throw new IllegalStateException("Failed to remove self from list of peers"); + return peers; + } + + private List getAllTradeParties() { List peers = new ArrayList(); peers.add(getMaker()); peers.add(getTaker()); peers.add(getArbitrator()); - if (!peers.remove(getSelf())) throw new IllegalStateException("Failed to remove self from list of peers"); return peers; } @@ -1801,6 +1813,12 @@ public abstract class Trade implements Tradable, Model { return (isSeller() ? getBuyer() : getSeller()).getPaymentReceivedMessage() != null; // seller stores message to buyer and arbitrator, peers store message from seller } + public boolean hasDisputeClosedMessage() { + + // arbitrator stores message to buyer and seller, peers store message from arbitrator + return isArbitrator() ? getBuyer().getDisputeClosedMessage() != null || getSeller().getDisputeClosedMessage() != null : getArbitrator().getDisputeClosedMessage() != null; + } + public boolean isPaymentReceived() { return getState().getPhase().ordinal() >= Phase.PAYMENT_RECEIVED.ordinal(); } @@ -1883,7 +1901,7 @@ public abstract class Trade implements Tradable, Model { public BigInteger getFrozenAmount() { BigInteger sum = BigInteger.ZERO; for (String keyImage : getSelf().getReserveTxKeyImages()) { - List outputs = xmrWalletService.getWallet().getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false).setKeyImage(new MoneroKeyImage(keyImage))); // TODO: will this check tx pool? avoid + List outputs = xmrWalletService.getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false).setKeyImage(new MoneroKeyImage(keyImage))); if (!outputs.isEmpty()) sum = sum.add(outputs.get(0).getAmount()); } return sum; @@ -2077,23 +2095,23 @@ public abstract class Trade implements Tradable, Model { synchronized (walletLock) { if (isShutDownStarted || isPollInProgress()) return; log.info("Starting to poll wallet for {} {}", getClass().getSimpleName(), getId()); - txPollLooper = new TaskLooper(() -> pollWallet()); - txPollLooper.start(walletRefreshPeriodMs); + pollLooper = new TaskLooper(() -> pollWallet()); + pollLooper.start(walletRefreshPeriodMs); } } private void stopPolling() { synchronized (walletLock) { if (isPollInProgress()) { - txPollLooper.stop(); - txPollLooper = null; + pollLooper.stop(); + pollLooper = null; } } } private boolean isPollInProgress() { synchronized (walletLock) { - return txPollLooper != null; + return pollLooper != null; } } @@ -2117,8 +2135,14 @@ public abstract class Trade implements Tradable, Model { // skip if payout unlocked if (isPayoutUnlocked()) return; - // rescan spent outputs to detect payout tx after deposits unlocked - if (isDepositsUnlocked() && !isPayoutPublished()) wallet.rescanSpent(); + // rescan spent outputs to detect unconfirmed payout tx after payment received message + if (!isPayoutPublished() && (hasPaymentReceivedMessage() || hasDisputeClosedMessage())) { + try { + wallet.rescanSpent(); + } catch (Exception e) { + log.warn("Error rescanning spent outputs to detect payout tx for {} {}, errorMessage={}", getClass().getSimpleName(), getShortId(), e.getMessage()); + } + } // get txs from trade wallet boolean payoutExpected = isPaymentReceived() || getSeller().getPaymentReceivedMessage() != null || disputeState.ordinal() >= DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG.ordinal() || getArbitrator().getDisputeClosedMessage() != null; @@ -2129,7 +2153,7 @@ public abstract class Trade implements Tradable, Model { // warn on double spend // TODO: other handling? for (MoneroTxWallet tx : txs) { - if (Boolean.TRUE.equals(tx.isDoubleSpendSeen())) log.warn("Double spend seen for tx {} for {} {}", tx.getHash(), getClass().getSimpleName(), getId()); + if (Boolean.TRUE.equals(tx.isDoubleSpendSeen())) log.warn("Double spend seen for tx {} for {} {}", tx.getHash(), getClass().getSimpleName(), getShortId()); } // check deposit txs @@ -2189,9 +2213,8 @@ public abstract class Trade implements Tradable, Model { if (isConnectionRefused) forceRestartTradeWallet(); else { boolean isWalletConnected = isWalletConnectedToDaemon(); - if (!isWalletConnected) xmrConnectionService.checkConnection(); // check connection if wallet is not connected if (!isShutDownStarted && wallet != null && isWalletConnected) { - log.warn("Error polling trade wallet for {} {}: {}. Monerod={}", getClass().getSimpleName(), getId(), e.getMessage(), getXmrWalletService().getConnectionService().getConnection()); + log.warn("Error polling trade wallet for {} {}, errorMessage={}. Monerod={}", getClass().getSimpleName(), getShortId(), e.getMessage(), getXmrWalletService().getConnectionService().getConnection()); //e.printStackTrace(); } } diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index 3b2c7f950b..5ccd1ecd5e 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -1287,7 +1287,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } private void removeTradeOnError(Trade trade) { - log.warn("TradeManager.removeTradeOnError() tradeId={}, state={}", trade.getId(), trade.getState()); + log.warn("TradeManager.removeTradeOnError() trade={}, tradeId={}, state={}", trade.getClass().getSimpleName(), trade.getShortId(), trade.getState()); synchronized (tradableList) { // unreserve taker key images diff --git a/core/src/main/java/haveno/core/trade/protocol/BuyerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/BuyerProtocol.java index f7f8e91d66..d3aa6a8a14 100644 --- a/core/src/main/java/haveno/core/trade/protocol/BuyerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/BuyerProtocol.java @@ -134,14 +134,16 @@ public class BuyerProtocol extends DisputeProtocol { BuyerSendPaymentSentMessageToArbitrator.class) .using(new TradeTaskRunner(trade, () -> { + stopTimeout(); this.errorMessageHandler = null; resultHandler.handleResult(); handleTaskRunnerSuccess(event); }, (errorMessage) -> { handleTaskRunnerFault(event, errorMessage); - }))) - .run(() -> trade.setState(Trade.State.BUYER_CONFIRMED_PAYMENT_SENT)) + })) + .withTimeout(TradeProtocol.TRADE_TIMEOUT_SECONDS)) + .run(() -> trade.advanceState(Trade.State.BUYER_CONFIRMED_PAYMENT_SENT)) .executeTasks(true); } catch (Exception e) { errorMessageHandler.handleErrorMessage("Error confirming payment sent: " + e.getMessage()); diff --git a/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java index ea03054bab..a59cfee76f 100644 --- a/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java @@ -131,13 +131,15 @@ public class SellerProtocol extends DisputeProtocol { SellerSendPaymentReceivedMessageToBuyer.class, SellerSendPaymentReceivedMessageToArbitrator.class) .using(new TradeTaskRunner(trade, () -> { + stopTimeout(); this.errorMessageHandler = null; handleTaskRunnerSuccess(event); resultHandler.handleResult(); }, (errorMessage) -> { handleTaskRunnerFault(event, errorMessage); - }))) - .run(() -> trade.setState(Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT)) + })) + .withTimeout(TradeProtocol.TRADE_TIMEOUT_SECONDS)) + .run(() -> trade.advanceState(Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT)) .executeTasks(true); } catch (Exception e) { errorMessageHandler.handleErrorMessage("Error confirming payment received: " + e.getMessage()); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java index b02c16630a..94470ec99e 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java @@ -61,13 +61,17 @@ public class ProcessDepositsConfirmedMessage extends TradeTask { log.info(trade.getClass().getSimpleName() + " decrypting using seller payment account key"); trade.decryptPeerPaymentAccountPayload(request.getSellerPaymentAccountKey()); } - processModel.getTradeManager().requestPersistence(); // in case importing multisig hex fails - // import multisig hex - trade.importMultisigHex(); - - // persist and complete + // persist processModel.getTradeManager().requestPersistence(); + + // try to import multisig hex (retry later) + try { + trade.importMultisigHex(); + } catch (Exception e) { + e.printStackTrace(); + } + complete(); } catch (Throwable t) { failed(t); 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 64fe582fb6..1d7ca555ca 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 @@ -95,7 +95,6 @@ public class ProcessPaymentReceivedMessage extends TradeTask { } trade.requestPersistence(); - // process payout tx unless already unlocked if (!trade.isPayoutUnlocked()) processPayoutTx(message); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentSentMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentSentMessage.java index 6aee0ef2af..103fa79b49 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentSentMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentSentMessage.java @@ -61,8 +61,12 @@ public class ProcessPaymentSentMessage extends TradeTask { if (trade.isSeller()) trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey()); trade.requestPersistence(); - // import multisig hex - trade.importMultisigHex(); + // try to import multisig hex (retry later) + try { + trade.importMultisigHex(); + } catch (Exception e) { + e.printStackTrace(); + } // update state trade.advanceState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SellerPublishDepositTx.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SellerPublishDepositTx.java deleted file mode 100644 index b01c774dc0..0000000000 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SellerPublishDepositTx.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package haveno.core.trade.protocol.tasks; - -import haveno.common.taskrunner.TaskRunner; -import haveno.core.trade.Trade; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class SellerPublishDepositTx extends TradeTask { - public SellerPublishDepositTx(TaskRunner taskHandler, Trade trade) { - super(taskHandler, trade); - } - - @Override - protected void run() { - try { - runInterceptHook(); - throw new RuntimeException("SellerPublishesDepositTx not implemented for xmr"); - -// final Transaction depositTx = processModel.getDepositTx(); -// processModel.getTradeWalletService().broadcastTx(depositTx, -// new TxBroadcaster.Callback() { -// @Override -// public void onSuccess(Transaction transaction) { -// if (!completed) { -// // Now as we have published the deposit tx we set it in trade -// trade.applyDepositTx(depositTx); -// -// trade.setState(Trade.State.SELLER_PUBLISHED_DEPOSIT_TX); -// -// processModel.getBtcWalletService().swapAddressEntryToAvailable(processModel.getOffer().getId(), -// AddressEntry.Context.RESERVED_FOR_TRADE); -// -// processModel.getTradeManager().requestPersistence(); -// -// complete(); -// } else { -// log.warn("We got the onSuccess callback called after the timeout has been triggered a complete()."); -// } -// } -// -// @Override -// public void onFailure(TxBroadcastException exception) { -// if (!completed) { -// failed(exception); -// } else { -// log.warn("We got the onFailure callback called after the timeout has been triggered a complete()."); -// } -// } -// }); - } catch (Throwable t) { - failed(t); - } - } -} diff --git a/core/src/main/java/haveno/core/xmr/Balances.java b/core/src/main/java/haveno/core/xmr/Balances.java index 5b47daa548..54da095927 100644 --- a/core/src/main/java/haveno/core/xmr/Balances.java +++ b/core/src/main/java/haveno/core/xmr/Balances.java @@ -140,7 +140,7 @@ public class Balances { // calculate reserved offer balance reservedOfferBalance = BigInteger.ZERO; if (xmrWalletService.getWallet() != null) { - List frozenOutputs = xmrWalletService.getWallet().getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false)); + List frozenOutputs = xmrWalletService.getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false)); for (MoneroOutputWallet frozenOutput : frozenOutputs) reservedOfferBalance = reservedOfferBalance.add(frozenOutput.getAmount()); } for (Trade trade : trades) { diff --git a/core/src/main/java/haveno/core/xmr/setup/DownloadListener.java b/core/src/main/java/haveno/core/xmr/setup/DownloadListener.java index f5b1cedc78..2b88b30cd7 100644 --- a/core/src/main/java/haveno/core/xmr/setup/DownloadListener.java +++ b/core/src/main/java/haveno/core/xmr/setup/DownloadListener.java @@ -11,7 +11,7 @@ public class DownloadListener { private final DoubleProperty percentage = new SimpleDoubleProperty(-1); public void progress(double percentage, long blocksLeft, Date date) { - UserThread.await(() -> this.percentage.set(percentage / 100d)); + UserThread.await(() -> this.percentage.set(percentage)); } public void doneDownload() { diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index cfc5055c14..335b5720d4 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -21,6 +21,8 @@ import static com.google.common.base.Preconditions.checkState; import com.google.common.util.concurrent.Service.State; import com.google.inject.Inject; import com.google.inject.name.Named; + +import common.utils.GenUtils; import common.utils.JsonUtils; import haveno.common.ThreadUtils; import haveno.common.UserThread; @@ -79,6 +81,7 @@ import monero.common.MoneroRpcError; import monero.common.MoneroUtils; import monero.common.TaskLooper; import monero.daemon.MoneroDaemonRpc; +import monero.daemon.model.MoneroDaemonInfo; import monero.daemon.model.MoneroFeeEstimate; import monero.daemon.model.MoneroNetworkType; import monero.daemon.model.MoneroOutput; @@ -152,14 +155,25 @@ public class XmrWalletService { private Object walletLock = new Object(); private boolean wasWalletSynced = false; private final Map> txCache = new HashMap>(); + private boolean isClosingWallet = false; private boolean isShutDownStarted = false; private ExecutorService syncWalletThreadPool = Executors.newFixedThreadPool(10); // TODO: adjust based on connection type private Long syncStartHeight = null; - private TaskLooper syncLooper = null; - private BigInteger cachedBalance = null; + private TaskLooper syncWithProgressLooper = null; + CountDownLatch syncWithProgressLatch; + + // wallet polling and cache + private TaskLooper pollLooper; + private boolean pollInProgress; + private Long walletRefreshPeriodMs; + private final Object pollLock = new Object(); + private Long cachedHeight; + private BigInteger cachedBalance; private BigInteger cachedAvailableBalance = null; private List cachedSubaddresses; + private List cachedOutputs; private List cachedTxs; + private boolean runReconnectTestOnStartup = false; // test reconnecting on startup while syncing so the wallet is blocked @SuppressWarnings("unused") @Inject @@ -474,7 +488,7 @@ public class XmrWalletService { } // thaw unreserved outputs - Set unreservedFrozenKeyImages = wallet.getOutputs(new MoneroOutputQuery() + Set unreservedFrozenKeyImages = getOutputs(new MoneroOutputQuery() .setIsFrozen(true) .setIsSpent(false)) .stream() @@ -497,7 +511,7 @@ public class XmrWalletService { synchronized (walletLock) { for (String keyImage : keyImages) wallet.freezeOutput(keyImage); requestSaveMainWallet(); - cacheWalletState(); + doPollWallet(false); } updateBalanceListeners(); // TODO (monero-java): balance listeners not notified on freeze/thaw output } @@ -511,7 +525,7 @@ public class XmrWalletService { synchronized (walletLock) { for (String keyImage : keyImages) wallet.thawOutput(keyImage); requestSaveMainWallet(); - cacheWalletState(); + doPollWallet(false); } updateBalanceListeners(); // TODO (monero-java): balance listeners not notified on freeze/thaw output } @@ -519,7 +533,7 @@ public class XmrWalletService { private List getSubaddressesWithExactInput(BigInteger amount) { // fetch unspent, unfrozen, unlocked outputs - List exactOutputs = wallet.getOutputs(new MoneroOutputQuery() + List exactOutputs = getOutputs(new MoneroOutputQuery() .setAmount(amount) .setIsSpent(false) .setIsFrozen(false) @@ -867,13 +881,333 @@ public class XmrWalletService { log.info("Done shutting down {}", getClass().getSimpleName()); } + // -------------------------- ADDRESS ENTRIES ----------------------------- + + public synchronized XmrAddressEntry getNewAddressEntry() { + return getNewAddressEntryAux(null, XmrAddressEntry.Context.AVAILABLE); + } + + public synchronized XmrAddressEntry getNewAddressEntry(String offerId, XmrAddressEntry.Context context) { + + // try to use available and not yet used entries + try { + List unusedAddressEntries = getUnusedAddressEntries(); + if (!unusedAddressEntries.isEmpty()) return xmrAddressEntryList.swapAvailableToAddressEntryWithOfferId(unusedAddressEntries.get(0), context, offerId); + } catch (Exception e) { + log.warn("Error getting new address entry based on incoming transactions"); + e.printStackTrace(); + } + + // create new entry + return getNewAddressEntryAux(offerId, context); + } + + private XmrAddressEntry getNewAddressEntryAux(String offerId, XmrAddressEntry.Context context) { + MoneroSubaddress subaddress = wallet.createSubaddress(0); + XmrAddressEntry entry = new XmrAddressEntry(subaddress.getIndex(), subaddress.getAddress(), context, offerId, null); + log.info("Add new XmrAddressEntry {}", entry); + xmrAddressEntryList.addAddressEntry(entry); + return entry; + } + + public synchronized XmrAddressEntry getFreshAddressEntry() { + List unusedAddressEntries = getUnusedAddressEntries(); + if (unusedAddressEntries.isEmpty()) return getNewAddressEntry(); + else return unusedAddressEntries.get(0); + } + + public synchronized XmrAddressEntry recoverAddressEntry(String offerId, String address, XmrAddressEntry.Context context) { + var available = findAddressEntry(address, XmrAddressEntry.Context.AVAILABLE); + if (!available.isPresent()) return null; + return xmrAddressEntryList.swapAvailableToAddressEntryWithOfferId(available.get(), context, offerId); + } + + public synchronized XmrAddressEntry getOrCreateAddressEntry(String offerId, XmrAddressEntry.Context context) { + Optional addressEntry = getAddressEntryListAsImmutableList().stream().filter(e -> offerId.equals(e.getOfferId())).filter(e -> context == e.getContext()).findAny(); + if (addressEntry.isPresent()) return addressEntry.get(); + else return getNewAddressEntry(offerId, context); + } + + public synchronized XmrAddressEntry getArbitratorAddressEntry() { + XmrAddressEntry.Context context = XmrAddressEntry.Context.ARBITRATOR; + Optional addressEntry = getAddressEntryListAsImmutableList().stream() + .filter(e -> context == e.getContext()) + .findAny(); + return addressEntry.isPresent() ? addressEntry.get() : getNewAddressEntryAux(null, context); + } + + public synchronized Optional getAddressEntry(String offerId, XmrAddressEntry.Context context) { + List entries = getAddressEntryListAsImmutableList().stream().filter(e -> offerId.equals(e.getOfferId())).filter(e -> context == e.getContext()).collect(Collectors.toList()); + if (entries.size() > 1) throw new RuntimeException("Multiple address entries exist with offer ID " + offerId + " and context " + context + ". That should never happen."); + return entries.isEmpty() ? Optional.empty() : Optional.of(entries.get(0)); + } + + public synchronized void swapAddressEntryToAvailable(String offerId, XmrAddressEntry.Context context) { + Optional addressEntryOptional = getAddressEntryListAsImmutableList().stream().filter(e -> offerId.equals(e.getOfferId())).filter(e -> context == e.getContext()).findAny(); + addressEntryOptional.ifPresent(e -> { + log.info("swap addressEntry with address {} and offerId {} from context {} to available", e.getAddressString(), e.getOfferId(), context); + xmrAddressEntryList.swapToAvailable(e); + saveAddressEntryList(); + }); + } + + public synchronized void resetAddressEntriesForOpenOffer(String offerId) { + log.info("resetAddressEntriesForOpenOffer offerId={}", offerId); + swapAddressEntryToAvailable(offerId, XmrAddressEntry.Context.OFFER_FUNDING); + + // swap trade payout to available if applicable + if (tradeManager == null) return; + Trade trade = tradeManager.getTrade(offerId); + if (trade == null || trade.isPayoutUnlocked()) swapAddressEntryToAvailable(offerId, XmrAddressEntry.Context.TRADE_PAYOUT); + } + + public synchronized void resetAddressEntriesForTrade(String offerId) { + swapAddressEntryToAvailable(offerId, XmrAddressEntry.Context.TRADE_PAYOUT); + } + + private Optional findAddressEntry(String address, XmrAddressEntry.Context context) { + return getAddressEntryListAsImmutableList().stream().filter(e -> address.equals(e.getAddressString())).filter(e -> context == e.getContext()).findAny(); + } + + public List getAddressEntries() { + return getAddressEntryListAsImmutableList().stream().collect(Collectors.toList()); + } + + public List getAvailableAddressEntries() { + return getAddressEntryListAsImmutableList().stream().filter(addressEntry -> XmrAddressEntry.Context.AVAILABLE == addressEntry.getContext()).collect(Collectors.toList()); + } + + public List getAddressEntriesForOpenOffer() { + return getAddressEntryListAsImmutableList().stream() + .filter(addressEntry -> XmrAddressEntry.Context.OFFER_FUNDING == addressEntry.getContext()) + .collect(Collectors.toList()); + } + + public List getAddressEntriesForTrade() { + return getAddressEntryListAsImmutableList().stream() + .filter(addressEntry -> XmrAddressEntry.Context.TRADE_PAYOUT == addressEntry.getContext()) + .collect(Collectors.toList()); + } + + public List getAddressEntries(XmrAddressEntry.Context context) { + return getAddressEntryListAsImmutableList().stream().filter(addressEntry -> context == addressEntry.getContext()).collect(Collectors.toList()); + } + + public List getFundedAvailableAddressEntries() { + return getAvailableAddressEntries().stream().filter(addressEntry -> getBalanceForSubaddress(addressEntry.getSubaddressIndex()).compareTo(BigInteger.ZERO) > 0).collect(Collectors.toList()); + } + + public List getAddressEntryListAsImmutableList() { + for (MoneroSubaddress subaddress : cachedSubaddresses) { + boolean exists = xmrAddressEntryList.getAddressEntriesAsListImmutable().stream().filter(addressEntry -> addressEntry.getAddressString().equals(subaddress.getAddress())).findAny().isPresent(); + if (!exists) { + XmrAddressEntry entry = new XmrAddressEntry(subaddress.getIndex(), subaddress.getAddress(), subaddress.getIndex() == 0 ? XmrAddressEntry.Context.BASE_ADDRESS : XmrAddressEntry.Context.AVAILABLE, null, null); + xmrAddressEntryList.addAddressEntry(entry); + } + } + return xmrAddressEntryList.getAddressEntriesAsListImmutable(); + } + + public List getUnusedAddressEntries() { + return getAvailableAddressEntries().stream() + .filter(e -> e.getContext() == XmrAddressEntry.Context.AVAILABLE && !subaddressHasIncomingTransfers(e.getSubaddressIndex())) + .collect(Collectors.toList()); + } + + public boolean subaddressHasIncomingTransfers(int subaddressIndex) { + return getNumOutputsForSubaddress(subaddressIndex) > 0; + } + + public int getNumOutputsForSubaddress(int subaddressIndex) { + int numUnspentOutputs = 0; + for (MoneroTxWallet tx : cachedTxs) { + //if (tx.getTransfers(new MoneroTransferQuery().setSubaddressIndex(subaddressIndex)).isEmpty()) continue; // TODO monero-project: transfers are occluded by transfers from/to same account, so this will return unused when used + numUnspentOutputs += tx.getOutputsWallet(new MoneroOutputQuery().setAccountIndex(0).setSubaddressIndex(subaddressIndex)).size(); // TODO: monero-project does not provide outputs for unconfirmed txs + } + boolean positiveBalance = getBalanceForSubaddress(subaddressIndex).compareTo(BigInteger.ZERO) > 0; + if (positiveBalance && numUnspentOutputs == 0) return 1; // outputs do not appear until confirmed and internal transfers are occluded, so report 1 if positive balance + return numUnspentOutputs; + } + + private MoneroSubaddress getSubaddress(int subaddressIndex) { + for (MoneroSubaddress subaddress : cachedSubaddresses) { + if (subaddress.getIndex() == subaddressIndex) return subaddress; + } + return null; + } + + public int getNumTxsWithIncomingOutputs(int subaddressIndex) { + List txsWithIncomingOutputs = getTxsWithIncomingOutputs(subaddressIndex); + if (txsWithIncomingOutputs.isEmpty() && subaddressHasIncomingTransfers(subaddressIndex)) return 1; // outputs do not appear until confirmed and internal transfers are occluded, so report 1 if positive balance + return txsWithIncomingOutputs.size(); + } + + public List getTxsWithIncomingOutputs() { + return getTxsWithIncomingOutputs(null); + } + + public List getTxsWithIncomingOutputs(Integer subaddressIndex) { + List incomingTxs = new ArrayList<>(); + for (MoneroTxWallet tx : cachedTxs) { + boolean isIncoming = false; + if (tx.getIncomingTransfers() != null) { + for (MoneroIncomingTransfer transfer : tx.getIncomingTransfers()) { + if (transfer.getAccountIndex().equals(0) && (subaddressIndex == null || transfer.getSubaddressIndex().equals(subaddressIndex))) { + isIncoming = true; + break; + } + } + } + if (tx.getOutputs() != null && !isIncoming) { + for (MoneroOutputWallet output : tx.getOutputsWallet()) { + if (output.getAccountIndex().equals(0) && (subaddressIndex == null || output.getSubaddressIndex().equals(subaddressIndex))) { + isIncoming = true; + break; + } + } + } + if (isIncoming) incomingTxs.add(tx); + } + return incomingTxs; + } + + public BigInteger getBalanceForAddress(String address) { + return getBalanceForSubaddress(wallet.getAddressIndex(address).getIndex()); + } + + public BigInteger getBalanceForSubaddress(int subaddressIndex) { + MoneroSubaddress subaddress = getSubaddress(subaddressIndex); + return subaddress == null ? BigInteger.ZERO : subaddress.getBalance(); + } + + public BigInteger getAvailableBalanceForSubaddress(int subaddressIndex) { + MoneroSubaddress subaddress = getSubaddress(subaddressIndex); + return subaddress == null ? BigInteger.ZERO : subaddress.getUnlockedBalance(); + } + + public Stream getAddressEntriesForAvailableBalanceStream() { + Stream available = getFundedAvailableAddressEntries().stream(); + available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.ARBITRATOR).stream()); + available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.OFFER_FUNDING).stream().filter(entry -> !tradeManager.getOpenOfferManager().getOpenOfferById(entry.getOfferId()).isPresent())); + available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.TRADE_PAYOUT).stream().filter(entry -> tradeManager.getTrade(entry.getOfferId()) == null || tradeManager.getTrade(entry.getOfferId()).isPayoutUnlocked())); + return available.filter(addressEntry -> getBalanceForSubaddress(addressEntry.getSubaddressIndex()).compareTo(BigInteger.ZERO) > 0); + } + + public void addWalletListener(MoneroWalletListenerI listener) { + synchronized (walletListeners) { + walletListeners.add(listener); + } + } + + public void removeWalletListener(MoneroWalletListenerI listener) { + synchronized (walletListeners) { + if (!walletListeners.contains(listener)) throw new RuntimeException("Listener is not registered with wallet"); + walletListeners.remove(listener); + } + } + + // TODO (woodser): update balance and other listening + public void addBalanceListener(XmrBalanceListener listener) { + if (!balanceListeners.contains(listener)) balanceListeners.add(listener); + } + + public void removeBalanceListener(XmrBalanceListener listener) { + balanceListeners.remove(listener); + } + + public void updateBalanceListeners() { + BigInteger availableBalance = getAvailableBalance(); + for (XmrBalanceListener balanceListener : balanceListeners) { + BigInteger balance; + if (balanceListener.getSubaddressIndex() != null && balanceListener.getSubaddressIndex() != 0) balance = getBalanceForSubaddress(balanceListener.getSubaddressIndex()); + else balance = availableBalance; + ThreadUtils.submitToPool(() -> { + try { + balanceListener.onBalanceChanged(balance); + } catch (Exception e) { + log.warn("Failed to notify balance listener of change"); + e.printStackTrace(); + } + }); + } + } + + public void saveAddressEntryList() { + xmrAddressEntryList.requestPersistence(); + } + + public List getTransactions(boolean includeFailed) { + if (cachedTxs == null) { + log.warn("Transactions not cached, fetching from wallet"); + cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true)); // fetches from pool + } + if (includeFailed) return cachedTxs; + return cachedTxs.stream().filter(tx -> !tx.isFailed()).collect(Collectors.toList()); + } + + public BigInteger getBalance() { + return cachedBalance; + } + + public BigInteger getAvailableBalance() { + return cachedAvailableBalance; + } + + public List getSubaddresses() { + return cachedSubaddresses; + } + + public List getOutputs(MoneroOutputQuery query) { + List filteredOutputs = new ArrayList(); + for (MoneroOutputWallet output : cachedOutputs) { + if (query == null || query.meetsCriteria(output)) filteredOutputs.add(output); + } + return filteredOutputs; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Util + /////////////////////////////////////////////////////////////////////////////////////////// + + public static MoneroNetworkType getMoneroNetworkType() { + switch (Config.baseCurrencyNetwork()) { + case XMR_LOCAL: + return MoneroNetworkType.TESTNET; + case XMR_STAGENET: + return MoneroNetworkType.STAGENET; + case XMR_MAINNET: + return MoneroNetworkType.MAINNET; + default: + throw new RuntimeException("Unhandled base currency network: " + Config.baseCurrencyNetwork()); + } + } + + public static void printTxs(String tracePrefix, MoneroTxWallet... txs) { + StringBuilder sb = new StringBuilder(); + for (MoneroTxWallet tx : txs) sb.append('\n' + tx.toString()); + log.info("\n" + tracePrefix + ":" + sb.toString()); + } + // ------------------------------ PRIVATE HELPERS ------------------------- private void initialize() { // listen for connection changes xmrConnectionService.addConnectionListener(connection -> { - ThreadUtils.execute(() -> onConnectionChanged(connection), THREAD_ID); + + // force restart main wallet if connection changed before synced + if (!wasWalletSynced) { + if (!Boolean.TRUE.equals(connection.isConnected())) return; + ThreadUtils.submitToPool(() -> { + log.warn("Force restarting main wallet because connection changed before inital sync"); + forceRestartMainWallet(); + }); + return; + } else { + + // apply connection changes + ThreadUtils.execute(() -> onConnectionChanged(connection), THREAD_ID); + } }); // initialize main wallet when daemon synced @@ -897,7 +1231,7 @@ public class XmrWalletService { private void initMainWalletIfConnected() { ThreadUtils.execute(() -> { synchronized (walletLock) { - if (xmrConnectionService.downloadPercentageProperty().get() == 1 && wallet == null && !isShutDownStarted) { + if (wallet == null && xmrConnectionService.downloadPercentageProperty().get() == 1 && !isShutDownStarted) { maybeInitMainWallet(true); if (walletInitListener != null) xmrConnectionService.downloadPercentageProperty().removeListener(walletInitListener); } @@ -916,7 +1250,6 @@ public class XmrWalletService { } private void maybeInitMainWallet(boolean sync, int numAttempts) { - synchronized (walletLock) { if (isShutDownStarted) return; @@ -934,6 +1267,7 @@ public class XmrWalletService { long date = localDateTime.toEpochSecond(ZoneOffset.UTC); user.setWalletCreationDate(date); } + isClosingWallet = false; } // sync wallet and register listener @@ -947,9 +1281,9 @@ public class XmrWalletService { // sync main wallet log.info("Syncing main wallet"); long time = System.currentTimeMillis(); - syncWalletWithProgress(); // blocking + syncWithProgress(); // blocking log.info("Done syncing main wallet in " + (System.currentTimeMillis() - time) + " ms"); - cacheWalletState(); + doPollWallet(true); // log wallet balances if (getMoneroNetworkType() != MoneroNetworkType.MAINNET) { @@ -972,7 +1306,8 @@ public class XmrWalletService { // save but skip backup on initialization saveMainWallet(false); } catch (Exception e) { - log.warn("Error syncing main wallet: {}", e.getMessage()); + if (isClosingWallet || isShutDownStarted || HavenoUtils.havenoSetup.getWalletInitialized().get()) return; // ignore if wallet closing, shut down started, or app already initialized + log.warn("Error initially syncing main wallet: {}", e.getMessage()); if (numAttempts <= 1) { log.warn("Failed to sync main wallet. Opening app without syncing", numAttempts); HavenoUtils.havenoSetup.getWalletInitialized().set(true); @@ -991,58 +1326,84 @@ public class XmrWalletService { } } - // register internal listener to notify external listeners - wallet.addListener(new XmrWalletListener()); // TODO: initial snapshot calls getTxs() which updates balance after returning but will not announce change + // start polling main wallet + startPolling(); } } } - private void syncWalletWithProgress() { + private void syncWithProgress() { // show sync progress - updateSyncProgress(); + updateSyncProgress(wallet.getHeight()); + + // test connection changing on startup before wallet synced + if (runReconnectTestOnStartup) { + UserThread.runAfter(() -> { + log.warn("Testing connection change on startup before wallet synced"); + xmrConnectionService.setConnection("http://node.community.rino.io:18081"); // TODO: needs to be online + }, 1); + runReconnectTestOnStartup = false; // only run once + } // get sync notifications from native wallet if (wallet instanceof MoneroWalletFull) { + if (runReconnectTestOnStartup) GenUtils.waitFor(1000); // delay sync to test wallet.sync(new MoneroWalletListener() { @Override public void onSyncProgress(long height, long startHeight, long endHeight, double percentDone, String message) { - updateSyncProgress(); + updateSyncProgress(height); } }); wasWalletSynced = true; return; } - // poll monero-wallet-rpc for progress + // poll wallet for progress wallet.startSyncing(xmrConnectionService.getRefreshPeriodMs()); - CountDownLatch latch = new CountDownLatch(1); - syncLooper = new TaskLooper(() -> { - if (wallet.getHeight() < xmrConnectionService.getTargetHeight()) updateSyncProgress(); + syncWithProgressLatch = new CountDownLatch(1); + syncWithProgressLooper = new TaskLooper(() -> { + if (wallet == null) return; + long height = 0; + try { + height = wallet.getHeight(); // can get read timeout while syncing + } catch (Exception e) { + e.printStackTrace(); + return; + } + if (height < xmrConnectionService.getTargetHeight()) updateSyncProgress(height); else { - syncLooper.stop(); + syncWithProgressLooper.stop(); try { syncWallet(); // ensure finished syncing } catch (Exception e) { e.printStackTrace(); } wasWalletSynced = true; - updateSyncProgress(); - latch.countDown(); + updateSyncProgress(height); + syncWithProgressLatch.countDown(); } }); - syncLooper.start(1000); - HavenoUtils.awaitLatch(latch); + syncWithProgressLooper.start(1000); + HavenoUtils.awaitLatch(syncWithProgressLatch); + if (!wasWalletSynced) throw new IllegalStateException("Failed to sync wallet with progress"); } - private void updateSyncProgress() { - long height = wallet.getHeight(); - UserThread.await(() -> { + private void stopSyncWithProgress() { + if (syncWithProgressLooper != null) { + syncWithProgressLooper.stop(); + syncWithProgressLooper = null; + syncWithProgressLatch.countDown(); + } + } + + private void updateSyncProgress(long height) { + UserThread.execute(() -> { walletHeight.set(height); // new wallet reports height 1 before synced if (height == 1) { - downloadListener.progress(.0001, xmrConnectionService.getTargetHeight(), null); // >0% shows progress bar + downloadListener.progress(.0001, xmrConnectionService.getTargetHeight() - height, null); // >0% shows progress bar return; } @@ -1050,7 +1411,7 @@ public class XmrWalletService { long targetHeight = xmrConnectionService.getTargetHeight(); long blocksLeft = targetHeight - walletHeight.get(); if (syncStartHeight == null) syncStartHeight = walletHeight.get(); - double percent = targetHeight == syncStartHeight ? 1.0 : ((double) Math.max(1, walletHeight.get() - syncStartHeight) / (double) (targetHeight - syncStartHeight)) * 100d; // grant at least 1 block to show progress + double percent = Math.min(1.0, targetHeight == syncStartHeight ? 1.0 : ((double) Math.max(1, (double) walletHeight.get() - syncStartHeight) / (double) (targetHeight - syncStartHeight))); // grant at least 1 block to show progress downloadListener.progress(percent, blocksLeft, null); }); } @@ -1075,7 +1436,7 @@ public class XmrWalletService { return walletFull; } catch (Exception e) { e.printStackTrace(); - if (walletFull != null) forceCloseWallet(walletFull, config.getPath()); + if (walletFull != null) forceCloseMainWallet(); throw new IllegalStateException("Could not create wallet '" + config.getPath() + "'"); } } @@ -1207,36 +1568,32 @@ public class XmrWalletService { private void onConnectionChanged(MoneroRpcConnection connection) { synchronized (walletLock) { - if (isShutDownStarted) return; - if (wallet != null && HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) return; + if (wallet == null || isShutDownStarted) return; + if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) return; String oldProxyUri = wallet == null || wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getProxyUri(); String newProxyUri = connection == null ? null : connection.getProxyUri(); log.info("Setting daemon connection for main wallet: uri={}, proxyUri={}", connection == null ? null : connection.getUri(), newProxyUri); - if (wallet == null) maybeInitMainWallet(false); - else if (wallet instanceof MoneroWalletRpc && !StringUtils.equals(oldProxyUri, newProxyUri)) { - log.info("Restarting main wallet because proxy URI has changed, old={}, new={}", oldProxyUri, newProxyUri); - closeMainWallet(true); - maybeInitMainWallet(false); + if (wallet instanceof MoneroWalletRpc) { + if (StringUtils.equals(oldProxyUri, newProxyUri)) { + wallet.setDaemonConnection(connection); + } else { + log.info("Restarting main wallet because proxy URI has changed, old={}, new={}", oldProxyUri, newProxyUri); + closeMainWallet(true); + maybeInitMainWallet(false); + } } else { wallet.setDaemonConnection(connection); wallet.setProxyUri(connection.getProxyUri()); } // sync wallet on new thread - if (connection != null) { + if (connection != null && !isShutDownStarted) { + updateWalletRefreshPeriod(); wallet.getDaemonConnection().setPrintStackTrace(PRINT_STACK_TRACE); - ThreadUtils.submitToPool(() -> { - if (isShutDownStarted) return; - wallet.startSyncing(xmrConnectionService.getRefreshPeriodMs()); - try { - if (Boolean.TRUE.equals(connection.isConnected())) syncWallet(); - } catch (Exception e) { - log.warn("Failed to sync main wallet after setting daemon connection: " + e.getMessage()); - } - }); + wallet.startSyncing(xmrConnectionService.getRefreshPeriodMs()); } - log.info("Done setting main wallet daemon connection: " + (wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getUri())); + log.info("Done setting main wallet monerod=" + (wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getUri())); } } @@ -1270,9 +1627,11 @@ public class XmrWalletService { } private void closeMainWallet(boolean save) { + stopPolling(); synchronized (walletLock) { try { if (wallet != null) { + isClosingWallet = true; closeWallet(wallet, true); wallet = null; } @@ -1282,350 +1641,160 @@ public class XmrWalletService { } } - public synchronized XmrAddressEntry getNewAddressEntry() { - return getNewAddressEntryAux(null, XmrAddressEntry.Context.AVAILABLE); + private void forceCloseMainWallet() { + isClosingWallet = true; + stopPolling(); + stopSyncWithProgress(); + forceCloseWallet(wallet, getWalletPath(MONERO_WALLET_NAME)); + wallet = null; } - public synchronized XmrAddressEntry getNewAddressEntry(String offerId, XmrAddressEntry.Context context) { - - // try to use available and not yet used entries - try { - List unusedAddressEntries = getUnusedAddressEntries(); - if (!unusedAddressEntries.isEmpty()) return xmrAddressEntryList.swapAvailableToAddressEntryWithOfferId(unusedAddressEntries.get(0), context, offerId); - } catch (Exception e) { - log.warn("Error getting new address entry based on incoming transactions"); - e.printStackTrace(); + private void forceRestartMainWallet() { + log.warn("Force restarting main wallet"); + forceCloseMainWallet(); + synchronized (walletLock) { + maybeInitMainWallet(true); } - - // create new entry - return getNewAddressEntryAux(offerId, context); } - private XmrAddressEntry getNewAddressEntryAux(String offerId, XmrAddressEntry.Context context) { - MoneroSubaddress subaddress = wallet.createSubaddress(0); - cacheWalletState(); - XmrAddressEntry entry = new XmrAddressEntry(subaddress.getIndex(), subaddress.getAddress(), context, offerId, null); - log.info("Add new XmrAddressEntry {}", entry); - xmrAddressEntryList.addAddressEntry(entry); - return entry; + private void startPolling() { + synchronized (walletLock) { + if (isShutDownStarted || isPollInProgress()) return; + log.info("Starting to poll main wallet"); + updateWalletRefreshPeriod(); + pollLooper = new TaskLooper(() -> pollWallet()); + pollLooper.start(walletRefreshPeriodMs); + } } - public synchronized XmrAddressEntry getFreshAddressEntry() { - List unusedAddressEntries = getUnusedAddressEntries(); - if (unusedAddressEntries.isEmpty()) return getNewAddressEntry(); - else return unusedAddressEntries.get(0); + private void stopPolling() { + if (isPollInProgress()) { + pollLooper.stop(); + pollLooper = null; + } } - public synchronized XmrAddressEntry recoverAddressEntry(String offerId, String address, XmrAddressEntry.Context context) { - var available = findAddressEntry(address, XmrAddressEntry.Context.AVAILABLE); - if (!available.isPresent()) return null; - return xmrAddressEntryList.swapAvailableToAddressEntryWithOfferId(available.get(), context, offerId); + private boolean isPollInProgress() { + return pollLooper != null; } - public synchronized XmrAddressEntry getOrCreateAddressEntry(String offerId, XmrAddressEntry.Context context) { - Optional addressEntry = getAddressEntryListAsImmutableList().stream().filter(e -> offerId.equals(e.getOfferId())).filter(e -> context == e.getContext()).findAny(); - if (addressEntry.isPresent()) return addressEntry.get(); - else return getNewAddressEntry(offerId, context); + public void updateWalletRefreshPeriod() { + if (isShutDownStarted) return; + setWalletRefreshPeriod(getWalletRefreshPeriod()); } - public synchronized XmrAddressEntry getArbitratorAddressEntry() { - XmrAddressEntry.Context context = XmrAddressEntry.Context.ARBITRATOR; - Optional addressEntry = getAddressEntryListAsImmutableList().stream() - .filter(e -> context == e.getContext()) - .findAny(); - return addressEntry.isPresent() ? addressEntry.get() : getNewAddressEntryAux(null, context); + private long getWalletRefreshPeriod() { + return xmrConnectionService.getRefreshPeriodMs(); } - public synchronized Optional getAddressEntry(String offerId, XmrAddressEntry.Context context) { - List entries = getAddressEntryListAsImmutableList().stream().filter(e -> offerId.equals(e.getOfferId())).filter(e -> context == e.getContext()).collect(Collectors.toList()); - if (entries.size() > 1) throw new RuntimeException("Multiple address entries exist with offer ID " + offerId + " and context " + context + ". That should never happen."); - return entries.isEmpty() ? Optional.empty() : Optional.of(entries.get(0)); + private void setWalletRefreshPeriod(long walletRefreshPeriodMs) { + synchronized (walletLock) { + if (this.isShutDownStarted) return; + if (this.walletRefreshPeriodMs != null && this.walletRefreshPeriodMs == walletRefreshPeriodMs) return; + this.walletRefreshPeriodMs = walletRefreshPeriodMs; + if (getWallet() != null) { + log.info("Setting main wallet refresh rate for to {}", getWalletRefreshPeriod()); + getWallet().startSyncing(getWalletRefreshPeriod()); // TODO (monero-project): wallet rpc waits until last sync period finishes before starting new sync period + } + if (isPollInProgress()) { + stopPolling(); + startPolling(); + } + } } - public synchronized void swapAddressEntryToAvailable(String offerId, XmrAddressEntry.Context context) { - Optional addressEntryOptional = getAddressEntryListAsImmutableList().stream().filter(e -> offerId.equals(e.getOfferId())).filter(e -> context == e.getContext()).findAny(); - addressEntryOptional.ifPresent(e -> { - log.info("swap addressEntry with address {} and offerId {} from context {} to available", e.getAddressString(), e.getOfferId(), context); - xmrAddressEntryList.swapToAvailable(e); - saveAddressEntryList(); + private void pollWallet() { + if (pollInProgress) return; + doPollWallet(true); + } + + private void doPollWallet(boolean updateTxs) { + synchronized (pollLock) { + pollInProgress = true; + try { + + // log warning if wallet is too far behind daemon + MoneroDaemonInfo lastInfo = xmrConnectionService.getLastInfo(); + long walletHeight = wallet.getHeight(); + int maxBlocksBehindWarning = 10; + if (wasWalletSynced && lastInfo != null && walletHeight < lastInfo.getHeight() - maxBlocksBehindWarning && !Config.baseCurrencyNetwork().isTestnet()) { + log.warn("Main wallet is more than {} blocks behind monerod, wallet height={}, monerod height={},", maxBlocksBehindWarning, walletHeight, lastInfo.getHeight()); + } + + // fetch transactions from pool and cache + if (updateTxs) { + try { + cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true)); + } catch (Exception e) { // fetch from pool can fail + log.warn("Error polling main wallet's transactions from the pool: {}", e.getMessage()); + } + } + + // get basic wallet info + long height = wallet.getHeight(); + BigInteger balance = wallet.getBalance(); + BigInteger unlockedBalance = wallet.getUnlockedBalance(); + cachedSubaddresses = wallet.getSubaddresses(0); + cachedOutputs = wallet.getOutputs(); + + // cache and notify changes + if (cachedHeight == null) { + cachedHeight = height; + cachedBalance = balance; + cachedAvailableBalance = unlockedBalance; + } else { + + // notify listeners of new block + if (height != cachedHeight) { + cachedHeight = height; + onNewBlock(height); + } + + // notify listeners of balance change + if (!balance.equals(cachedBalance) || !unlockedBalance.equals(cachedAvailableBalance)) { + cachedBalance = balance; + cachedAvailableBalance = unlockedBalance; + onBalancesChanged(balance, unlockedBalance); + } + } + } catch (Exception e) { + if (isShutDownStarted) return; + boolean isConnectionRefused = e.getMessage() != null && e.getMessage().contains("Connection refused"); + if (isConnectionRefused && wallet != null) forceRestartMainWallet(); + else { + boolean isWalletConnected = isWalletConnectedToDaemon(); + if (!isWalletConnected) xmrConnectionService.checkConnection(); // check connection if wallet is not connected + if (wallet != null && isWalletConnected) { + log.warn("Error polling main wallet, errorMessage={}. Monerod={}", e.getMessage(), getConnectionService().getConnection()); + //e.printStackTrace(); + } + } + } finally { + pollInProgress = false; + } + } + } + + public boolean isWalletConnectedToDaemon() { + synchronized (walletLock) { + try { + if (wallet == null) return false; + return wallet.isConnectedToDaemon(); + } catch (Exception e) { + return false; + } + } + } + + private void onNewBlock(long height) { + UserThread.execute(() -> { + walletHeight.set(height); + for (MoneroWalletListenerI listener : walletListeners) ThreadUtils.submitToPool(() -> listener.onNewBlock(height)); }); } - public synchronized void resetAddressEntriesForOpenOffer(String offerId) { - log.info("resetAddressEntriesForOpenOffer offerId={}", offerId); - swapAddressEntryToAvailable(offerId, XmrAddressEntry.Context.OFFER_FUNDING); - - // swap trade payout to available if applicable - if (tradeManager == null) return; - Trade trade = tradeManager.getTrade(offerId); - if (trade == null || trade.isPayoutUnlocked()) swapAddressEntryToAvailable(offerId, XmrAddressEntry.Context.TRADE_PAYOUT); - } - - public synchronized void resetAddressEntriesForTrade(String offerId) { - swapAddressEntryToAvailable(offerId, XmrAddressEntry.Context.TRADE_PAYOUT); - } - - private Optional findAddressEntry(String address, XmrAddressEntry.Context context) { - return getAddressEntryListAsImmutableList().stream().filter(e -> address.equals(e.getAddressString())).filter(e -> context == e.getContext()).findAny(); - } - - public List getAddressEntries() { - return getAddressEntryListAsImmutableList().stream().collect(Collectors.toList()); - } - - public List getAvailableAddressEntries() { - return getAddressEntryListAsImmutableList().stream().filter(addressEntry -> XmrAddressEntry.Context.AVAILABLE == addressEntry.getContext()).collect(Collectors.toList()); - } - - public List getAddressEntriesForOpenOffer() { - return getAddressEntryListAsImmutableList().stream() - .filter(addressEntry -> XmrAddressEntry.Context.OFFER_FUNDING == addressEntry.getContext()) - .collect(Collectors.toList()); - } - - public List getAddressEntriesForTrade() { - return getAddressEntryListAsImmutableList().stream() - .filter(addressEntry -> XmrAddressEntry.Context.TRADE_PAYOUT == addressEntry.getContext()) - .collect(Collectors.toList()); - } - - public List getAddressEntries(XmrAddressEntry.Context context) { - return getAddressEntryListAsImmutableList().stream().filter(addressEntry -> context == addressEntry.getContext()).collect(Collectors.toList()); - } - - public List getFundedAvailableAddressEntries() { - synchronized (walletLock) { - return getAvailableAddressEntries().stream().filter(addressEntry -> getBalanceForSubaddress(addressEntry.getSubaddressIndex()).compareTo(BigInteger.ZERO) > 0).collect(Collectors.toList()); - } - } - - public List getAddressEntryListAsImmutableList() { - synchronized (walletLock) { - for (MoneroSubaddress subaddress : cachedSubaddresses) { - boolean exists = xmrAddressEntryList.getAddressEntriesAsListImmutable().stream().filter(addressEntry -> addressEntry.getAddressString().equals(subaddress.getAddress())).findAny().isPresent(); - if (!exists) { - XmrAddressEntry entry = new XmrAddressEntry(subaddress.getIndex(), subaddress.getAddress(), subaddress.getIndex() == 0 ? XmrAddressEntry.Context.BASE_ADDRESS : XmrAddressEntry.Context.AVAILABLE, null, null); - xmrAddressEntryList.addAddressEntry(entry); - } - } - return xmrAddressEntryList.getAddressEntriesAsListImmutable(); - } - } - - public List getUnusedAddressEntries() { - synchronized (walletLock) { - return getAvailableAddressEntries().stream() - .filter(e -> e.getContext() == XmrAddressEntry.Context.AVAILABLE && !subaddressHasIncomingTransfers(e.getSubaddressIndex())) - .collect(Collectors.toList()); - } - } - - public boolean subaddressHasIncomingTransfers(int subaddressIndex) { - return getNumOutputsForSubaddress(subaddressIndex) > 0; - } - - public int getNumOutputsForSubaddress(int subaddressIndex) { - int numUnspentOutputs = 0; - for (MoneroTxWallet tx : cachedTxs) { - //if (tx.getTransfers(new MoneroTransferQuery().setSubaddressIndex(subaddressIndex)).isEmpty()) continue; // TODO monero-project: transfers are occluded by transfers from/to same account, so this will return unused when used - numUnspentOutputs += tx.isConfirmed() ? tx.getOutputsWallet(new MoneroOutputQuery().setAccountIndex(0).setSubaddressIndex(subaddressIndex)).size() : 1; // TODO: monero-project does not provide outputs for unconfirmed txs - } - boolean positiveBalance = getSubaddress(subaddressIndex).getBalance().compareTo(BigInteger.ZERO) > 0; - if (positiveBalance && numUnspentOutputs == 0) return 1; // outputs do not appear until confirmed and internal transfers are occluded, so report 1 if positive balance - return numUnspentOutputs; - } - - private MoneroSubaddress getSubaddress(int subaddressIndex) { - for (MoneroSubaddress subaddress : cachedSubaddresses) { - if (subaddress.getIndex() == subaddressIndex) return subaddress; - } - return null; - } - - public int getNumTxsWithIncomingOutputs(int subaddressIndex) { - List txsWithIncomingOutputs = getTxsWithIncomingOutputs(subaddressIndex); - if (txsWithIncomingOutputs.isEmpty() && subaddressHasIncomingTransfers(subaddressIndex)) return 1; // outputs do not appear until confirmed and internal transfers are occluded, so report 1 if positive balance - return txsWithIncomingOutputs.size(); - } - - public List getTxsWithIncomingOutputs() { - return getTxsWithIncomingOutputs(null); - } - - public List getTxsWithIncomingOutputs(Integer subaddressIndex) { - List incomingTxs = new ArrayList<>(); - for (MoneroTxWallet tx : cachedTxs) { - boolean isIncoming = false; - if (tx.getIncomingTransfers() != null) { - for (MoneroIncomingTransfer transfer : tx.getIncomingTransfers()) { - if (transfer.getAccountIndex().equals(0) && (subaddressIndex == null || transfer.getSubaddressIndex().equals(subaddressIndex))) { - isIncoming = true; - break; - } - } - } - if (tx.getOutputs() != null && !isIncoming) { - for (MoneroOutputWallet output : tx.getOutputsWallet()) { - if (output.getAccountIndex().equals(0) && (subaddressIndex == null || output.getSubaddressIndex().equals(subaddressIndex))) { - isIncoming = true; - break; - } - } - } - if (isIncoming) incomingTxs.add(tx); - } - return incomingTxs; - } - - public BigInteger getBalanceForAddress(String address) { - return getBalanceForSubaddress(wallet.getAddressIndex(address).getIndex()); - } - - public BigInteger getBalanceForSubaddress(int subaddressIndex) { - return getSubaddress(subaddressIndex).getBalance(); - } - - public BigInteger getAvailableBalanceForSubaddress(int subaddressIndex) { - return getSubaddress(subaddressIndex).getUnlockedBalance(); - } - - public BigInteger getBalance() { - return cachedBalance; - } - - public BigInteger getAvailableBalance() { - return cachedAvailableBalance; - } - - public List getSubaddresses() { - return cachedSubaddresses; - } - - public Stream getAddressEntriesForAvailableBalanceStream() { - synchronized (walletLock) { - Stream available = getFundedAvailableAddressEntries().stream(); - available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.ARBITRATOR).stream()); - available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.OFFER_FUNDING).stream().filter(entry -> !tradeManager.getOpenOfferManager().getOpenOfferById(entry.getOfferId()).isPresent())); - available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.TRADE_PAYOUT).stream().filter(entry -> tradeManager.getTrade(entry.getOfferId()) == null || tradeManager.getTrade(entry.getOfferId()).isPayoutUnlocked())); - return available.filter(addressEntry -> getBalanceForSubaddress(addressEntry.getSubaddressIndex()).compareTo(BigInteger.ZERO) > 0); - } - } - - public void addWalletListener(MoneroWalletListenerI listener) { - synchronized (walletListeners) { - walletListeners.add(listener); - } - } - - public void removeWalletListener(MoneroWalletListenerI listener) { - synchronized (walletListeners) { - if (!walletListeners.contains(listener)) throw new RuntimeException("Listener is not registered with wallet"); - walletListeners.remove(listener); - } - } - - // TODO (woodser): update balance and other listening - public void addBalanceListener(XmrBalanceListener listener) { - if (!balanceListeners.contains(listener)) balanceListeners.add(listener); - } - - public void removeBalanceListener(XmrBalanceListener listener) { - balanceListeners.remove(listener); - } - - public void updateBalanceListeners() { - BigInteger availableBalance = getAvailableBalance(); - for (XmrBalanceListener balanceListener : balanceListeners) { - BigInteger balance; - if (balanceListener.getSubaddressIndex() != null && balanceListener.getSubaddressIndex() != 0) balance = getBalanceForSubaddress(balanceListener.getSubaddressIndex()); - else balance = availableBalance; - ThreadUtils.submitToPool(() -> { - try { - balanceListener.onBalanceChanged(balance); - } catch (Exception e) { - log.warn("Failed to notify balance listener of change"); - e.printStackTrace(); - } - }); - } - } - - public void saveAddressEntryList() { - xmrAddressEntryList.requestPersistence(); - } - - public List getTransactions(boolean includeDead) { - if (includeDead) return cachedTxs; - return cachedTxs.stream().filter(tx -> !tx.isFailed()).collect(Collectors.toList()); - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Util - /////////////////////////////////////////////////////////////////////////////////////////// - - public static MoneroNetworkType getMoneroNetworkType() { - switch (Config.baseCurrencyNetwork()) { - case XMR_LOCAL: - return MoneroNetworkType.TESTNET; - case XMR_STAGENET: - return MoneroNetworkType.STAGENET; - case XMR_MAINNET: - return MoneroNetworkType.MAINNET; - default: - throw new RuntimeException("Unhandled base currency network: " + Config.baseCurrencyNetwork()); - } - } - - public static void printTxs(String tracePrefix, MoneroTxWallet... txs) { - StringBuilder sb = new StringBuilder(); - for (MoneroTxWallet tx : txs) sb.append('\n' + tx.toString()); - log.info("\n" + tracePrefix + ":" + sb.toString()); - } - - // -------------------------------- HELPERS ------------------------------- - - private void cacheWalletState() { - cachedBalance = wallet.getBalance(0); - cachedAvailableBalance = wallet.getUnlockedBalance(0); - cachedSubaddresses = wallet.getSubaddresses(0); - cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true)); - } - - /** - * Relays wallet notifications to external listeners. - */ - private class XmrWalletListener extends MoneroWalletListener { - - @Override - public void onSyncProgress(long height, long startHeight, long endHeight, double percentDone, String message) { - ThreadUtils.submitToPool(() -> updateSyncProgress()); - for (MoneroWalletListenerI listener : walletListeners) ThreadUtils.submitToPool(() -> listener.onSyncProgress(height, startHeight, endHeight, percentDone, message)); - } - - @Override - public void onNewBlock(long height) { - cacheWalletState(); - UserThread.execute(() -> { - walletHeight.set(height); - for (MoneroWalletListenerI listener : walletListeners) ThreadUtils.submitToPool(() -> listener.onNewBlock(height)); - }); - } - - @Override - public void onBalancesChanged(BigInteger newBalance, BigInteger newUnlockedBalance) { - cacheWalletState(); - updateBalanceListeners(); - for (MoneroWalletListenerI listener : walletListeners) ThreadUtils.submitToPool(() -> listener.onBalancesChanged(newBalance, newUnlockedBalance)); - } - - @Override - public void onOutputReceived(MoneroOutputWallet output) { - for (MoneroWalletListenerI listener : walletListeners) ThreadUtils.submitToPool(() -> listener.onOutputReceived(output)); - } - - @Override - public void onOutputSpent(MoneroOutputWallet output) { - for (MoneroWalletListenerI listener : walletListeners) ThreadUtils.submitToPool(() -> listener.onOutputSpent(output)); - } + private void onBalancesChanged(BigInteger newBalance, BigInteger newUnlockedBalance) { + updateBalanceListeners(); + for (MoneroWalletListenerI listener : walletListeners) ThreadUtils.submitToPool(() -> listener.onBalancesChanged(newBalance, newUnlockedBalance)); } } diff --git a/desktop/src/main/java/haveno/desktop/main/debug/DebugView.java b/desktop/src/main/java/haveno/desktop/main/debug/DebugView.java index 1d447b69af..63cb4bfaad 100644 --- a/desktop/src/main/java/haveno/desktop/main/debug/DebugView.java +++ b/desktop/src/main/java/haveno/desktop/main/debug/DebugView.java @@ -33,7 +33,6 @@ import haveno.core.trade.protocol.tasks.ProcessPaymentReceivedMessage; import haveno.core.trade.protocol.tasks.ProcessPaymentSentMessage; import haveno.core.trade.protocol.tasks.RemoveOffer; import haveno.core.trade.protocol.tasks.SellerPreparePaymentReceivedMessage; -import haveno.core.trade.protocol.tasks.SellerPublishDepositTx; import haveno.core.trade.protocol.tasks.SellerPublishTradeStatistics; import haveno.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToBuyer; import haveno.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness; @@ -87,7 +86,6 @@ public class DebugView extends InitializableView { VerifyPeersAccountAgeWitness.class, //SellerSendsDepositTxAndDelayedPayoutTxMessage.class, - SellerPublishDepositTx.class, SellerPublishTradeStatistics.class, ProcessPaymentSentMessage.class, @@ -140,7 +138,6 @@ public class DebugView extends InitializableView { RemoveOffer.class, //SellerSendsDepositTxAndDelayedPayoutTxMessage.class, - SellerPublishDepositTx.class, SellerPublishTradeStatistics.class, ProcessPaymentSentMessage.class, diff --git a/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java b/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java index 7d6cd6a13f..8d9a83a839 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java @@ -36,6 +36,8 @@ package haveno.desktop.main.funds.deposit; import com.google.inject.Inject; import com.google.inject.name.Named; + +import haveno.common.ThreadUtils; import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.util.Tuple3; @@ -78,6 +80,7 @@ import javafx.fxml.FXML; import javafx.geometry.Insets; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; +import javafx.scene.control.Label; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; @@ -111,6 +114,7 @@ public class DepositView extends ActivatableView { private Button generateNewAddressButton; private TitledGroupBg titledGroupBg; private InputTextField amountTextField; + private static final String THREAD_ID = DepositView.class.getName(); private final XmrWalletService xmrWalletService; private final Preferences preferences; @@ -146,142 +150,155 @@ public class DepositView extends ActivatableView { confirmationsColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.confirmations"))); usageColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.usage"))); - // trigger creation of at least 1 address - try { - xmrWalletService.getFreshAddressEntry(); - } catch (Exception e) { - log.warn("Failed to get wallet txs to initialize DepositView"); - e.printStackTrace(); - } + // set loading placeholder + Label placeholderLabel = new Label("Loading..."); + tableView.setPlaceholder(placeholderLabel); - tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); - tableView.setPlaceholder(new AutoTooltipLabel(Res.get("funds.deposit.noAddresses"))); - tableViewSelectionListener = (observableValue, oldValue, newValue) -> { - if (newValue != null) { - fillForm(newValue.getAddressString()); - GUIUtil.requestFocus(amountTextField); + ThreadUtils.execute(() -> { + + // trigger creation of at least 1 address + try { + xmrWalletService.getFreshAddressEntry(); + } catch (Exception e) { + log.warn("Failed to create fresh address entry to initialize DepositView"); + e.printStackTrace(); } - }; - setAddressColumnCellFactory(); - setBalanceColumnCellFactory(); - setUsageColumnCellFactory(); - setConfidenceColumnCellFactory(); - - addressColumn.setComparator(Comparator.comparing(DepositListItem::getAddressString)); - balanceColumn.setComparator(Comparator.comparing(DepositListItem::getBalanceAsBI)); - confirmationsColumn.setComparator(Comparator.comparingLong(o -> o.getNumConfirmationsSinceFirstUsed())); - usageColumn.setComparator(Comparator.comparing(DepositListItem::getUsage)); - tableView.getSortOrder().add(usageColumn); - tableView.setItems(sortedList); - - titledGroupBg = addTitledGroupBg(gridPane, gridRow, 4, Res.get("funds.deposit.fundWallet")); - titledGroupBg.getStyleClass().add("last"); - - qrCodeImageView = new ImageView(); - qrCodeImageView.setFitHeight(150); - qrCodeImageView.setFitWidth(150); - qrCodeImageView.getStyleClass().add("qr-code"); - Tooltip.install(qrCodeImageView, new Tooltip(Res.get("shared.openLargeQRWindow"))); - qrCodeImageView.setOnMouseClicked(e -> UserThread.runAfter( - () -> new QRCodeWindow(getPaymentUri()).show(), - 200, TimeUnit.MILLISECONDS)); - GridPane.setRowIndex(qrCodeImageView, gridRow); - GridPane.setRowSpan(qrCodeImageView, 4); - GridPane.setColumnIndex(qrCodeImageView, 1); - GridPane.setMargin(qrCodeImageView, new Insets(Layout.FIRST_ROW_DISTANCE, 0, 0, 10)); - gridPane.getChildren().add(qrCodeImageView); - - addressTextField = addAddressTextField(gridPane, ++gridRow, Res.get("shared.address"), Layout.FIRST_ROW_DISTANCE); - addressTextField.setPaymentLabel(paymentLabelString); - - - amountTextField = addInputTextField(gridPane, ++gridRow, Res.get("funds.deposit.amount")); - amountTextField.setMaxWidth(380); - if (DevEnv.isDevMode()) - amountTextField.setText("10"); - - titledGroupBg.setVisible(false); - titledGroupBg.setManaged(false); - qrCodeImageView.setVisible(false); - qrCodeImageView.setManaged(false); - addressTextField.setVisible(false); - addressTextField.setManaged(false); - amountTextField.setManaged(false); - - Tuple3 buttonCheckBoxHBox = addButtonCheckBoxWithBox(gridPane, ++gridRow, - Res.get("funds.deposit.generateAddress"), - null, - 15); - buttonCheckBoxHBox.third.setSpacing(25); - generateNewAddressButton = buttonCheckBoxHBox.first; - - generateNewAddressButton.setOnAction(event -> { - boolean hasUnusedAddress = !xmrWalletService.getUnusedAddressEntries().isEmpty(); - if (hasUnusedAddress) { - new Popup().warning(Res.get("funds.deposit.selectUnused")).show(); - } else { - XmrAddressEntry newSavingsAddressEntry = xmrWalletService.getNewAddressEntry(); - updateList(); - UserThread.execute(() -> { - observableList.stream() - .filter(depositListItem -> depositListItem.getAddressString().equals(newSavingsAddressEntry.getAddressString())) - .findAny() - .ifPresent(depositListItem -> tableView.getSelectionModel().select(depositListItem)); + UserThread.execute(() -> { + tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + tableView.setPlaceholder(new AutoTooltipLabel(Res.get("funds.deposit.noAddresses"))); + tableViewSelectionListener = (observableValue, oldValue, newValue) -> { + if (newValue != null) { + fillForm(newValue.getAddressString()); + GUIUtil.requestFocus(amountTextField); + } + }; + + setAddressColumnCellFactory(); + setBalanceColumnCellFactory(); + setUsageColumnCellFactory(); + setConfidenceColumnCellFactory(); + + addressColumn.setComparator(Comparator.comparing(DepositListItem::getAddressString)); + balanceColumn.setComparator(Comparator.comparing(DepositListItem::getBalanceAsBI)); + confirmationsColumn.setComparator(Comparator.comparingLong(o -> o.getNumConfirmationsSinceFirstUsed())); + usageColumn.setComparator(Comparator.comparing(DepositListItem::getUsage)); + tableView.getSortOrder().add(usageColumn); + tableView.setItems(sortedList); + + titledGroupBg = addTitledGroupBg(gridPane, gridRow, 4, Res.get("funds.deposit.fundWallet")); + titledGroupBg.getStyleClass().add("last"); + + qrCodeImageView = new ImageView(); + qrCodeImageView.setFitHeight(150); + qrCodeImageView.setFitWidth(150); + qrCodeImageView.getStyleClass().add("qr-code"); + Tooltip.install(qrCodeImageView, new Tooltip(Res.get("shared.openLargeQRWindow"))); + qrCodeImageView.setOnMouseClicked(e -> UserThread.runAfter( + () -> new QRCodeWindow(getPaymentUri()).show(), + 200, TimeUnit.MILLISECONDS)); + GridPane.setRowIndex(qrCodeImageView, gridRow); + GridPane.setRowSpan(qrCodeImageView, 4); + GridPane.setColumnIndex(qrCodeImageView, 1); + GridPane.setMargin(qrCodeImageView, new Insets(Layout.FIRST_ROW_DISTANCE, 0, 0, 10)); + gridPane.getChildren().add(qrCodeImageView); + + addressTextField = addAddressTextField(gridPane, ++gridRow, Res.get("shared.address"), Layout.FIRST_ROW_DISTANCE); + addressTextField.setPaymentLabel(paymentLabelString); + amountTextField = addInputTextField(gridPane, ++gridRow, Res.get("funds.deposit.amount")); + amountTextField.setMaxWidth(380); + if (DevEnv.isDevMode()) + amountTextField.setText("10"); + + titledGroupBg.setVisible(false); + titledGroupBg.setManaged(false); + qrCodeImageView.setVisible(false); + qrCodeImageView.setManaged(false); + addressTextField.setVisible(false); + addressTextField.setManaged(false); + amountTextField.setManaged(false); + + Tuple3 buttonCheckBoxHBox = addButtonCheckBoxWithBox(gridPane, ++gridRow, + Res.get("funds.deposit.generateAddress"), + null, + 15); + buttonCheckBoxHBox.third.setSpacing(25); + generateNewAddressButton = buttonCheckBoxHBox.first; + + generateNewAddressButton.setOnAction(event -> { + boolean hasUnusedAddress = !xmrWalletService.getUnusedAddressEntries().isEmpty(); + if (hasUnusedAddress) { + new Popup().warning(Res.get("funds.deposit.selectUnused")).show(); + } else { + XmrAddressEntry newSavingsAddressEntry = xmrWalletService.getNewAddressEntry(); + updateList(); + UserThread.execute(() -> { + observableList.stream() + .filter(depositListItem -> depositListItem.getAddressString().equals(newSavingsAddressEntry.getAddressString())) + .findAny() + .ifPresent(depositListItem -> tableView.getSelectionModel().select(depositListItem)); + }); + } }); - } - }); - - balanceListener = new XmrBalanceListener() { - @Override - public void onBalanceChanged(BigInteger balance) { - updateList(); - } - }; - - walletListener = new MoneroWalletListener() { - @Override - public void onNewBlock(long height) { - updateList(); - } - }; - - GUIUtil.focusWhenAddedToScene(amountTextField); + + balanceListener = new XmrBalanceListener() { + @Override + public void onBalanceChanged(BigInteger balance) { + updateList(); + } + }; + + walletListener = new MoneroWalletListener() { + @Override + public void onNewBlock(long height) { + updateList(); + } + }; + + GUIUtil.focusWhenAddedToScene(amountTextField); + }); + }, THREAD_ID); } @Override protected void activate() { - tableView.getSelectionModel().selectedItemProperty().addListener(tableViewSelectionListener); - sortedList.comparatorProperty().bind(tableView.comparatorProperty()); - - // try to update deposits list - try { - updateList(); - } catch (Exception e) { - log.warn("Could not update deposits list"); - e.printStackTrace(); - } - - xmrWalletService.addBalanceListener(balanceListener); - xmrWalletService.addWalletListener(walletListener); - - amountTextFieldSubscription = EasyBind.subscribe(amountTextField.textProperty(), t -> { - addressTextField.setAmount(HavenoUtils.parseXmr(t)); - updateQRCode(); - }); - - if (tableView.getSelectionModel().getSelectedItem() == null && !sortedList.isEmpty()) - tableView.getSelectionModel().select(0); + ThreadUtils.execute(() -> { + UserThread.execute(() -> { + tableView.getSelectionModel().selectedItemProperty().addListener(tableViewSelectionListener); + sortedList.comparatorProperty().bind(tableView.comparatorProperty()); + + // try to update deposits list + try { + updateList(); + } catch (Exception e) { + log.warn("Could not update deposits list"); + e.printStackTrace(); + } + + xmrWalletService.addBalanceListener(balanceListener); + xmrWalletService.addWalletListener(walletListener); + + amountTextFieldSubscription = EasyBind.subscribe(amountTextField.textProperty(), t -> { + addressTextField.setAmount(HavenoUtils.parseXmr(t)); + updateQRCode(); + }); + + if (tableView.getSelectionModel().getSelectedItem() == null && !sortedList.isEmpty()) + tableView.getSelectionModel().select(0); + }); + }, THREAD_ID); } @Override protected void deactivate() { - tableView.getSelectionModel().selectedItemProperty().removeListener(tableViewSelectionListener); - sortedList.comparatorProperty().unbind(); - observableList.forEach(DepositListItem::cleanup); - xmrWalletService.removeBalanceListener(balanceListener); - xmrWalletService.removeWalletListener(walletListener); - amountTextFieldSubscription.unsubscribe(); + ThreadUtils.execute(() -> { + tableView.getSelectionModel().selectedItemProperty().removeListener(tableViewSelectionListener); + sortedList.comparatorProperty().unbind(); + observableList.forEach(DepositListItem::cleanup); + xmrWalletService.removeBalanceListener(balanceListener); + xmrWalletService.removeWalletListener(walletListener); + amountTextFieldSubscription.unsubscribe(); + }, THREAD_ID); } diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java index 15d8bc8ced..1c27e05f8c 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java @@ -156,7 +156,6 @@ public class BuyerStep2View extends TradeStepView { statusLabel.setText(Res.get("shared.preparingConfirmation")); break; case BUYER_SENT_PAYMENT_SENT_MSG: - case BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG: busyAnimation.play(); statusLabel.setText(Res.get("shared.sendingConfirmation")); timeoutTimer = UserThread.runAfter(() -> { @@ -168,6 +167,7 @@ public class BuyerStep2View extends TradeStepView { busyAnimation.stop(); statusLabel.setText(Res.get("shared.messageStoredInMailbox")); break; + case BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG: case SELLER_RECEIVED_PAYMENT_SENT_MSG: busyAnimation.stop(); statusLabel.setText(Res.get("shared.messageArrived")); @@ -442,7 +442,8 @@ public class BuyerStep2View extends TradeStepView { private boolean confirmPaymentSentPermitted() { if (!trade.confirmPermitted()) return false; - return trade.isDepositsUnlocked() && trade.getState().ordinal() < Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal(); + if (trade.getState() == Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG) return false; + return trade.isDepositsUnlocked() && trade.getState().ordinal() < Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG.ordinal(); } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java index 7d114b97bd..e267badcf4 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java @@ -123,20 +123,19 @@ public class SellerStep3View extends TradeStepView { case SELLER_SENT_PAYMENT_RECEIVED_MSG: busyAnimation.play(); statusLabel.setText(Res.get("shared.sendingConfirmation")); - timeoutTimer = UserThread.runAfter(() -> { busyAnimation.stop(); statusLabel.setText(Res.get("shared.sendingConfirmationAgain")); }, 30); break; - case SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG: - busyAnimation.stop(); - statusLabel.setText(Res.get("shared.messageArrived")); - break; case SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG: busyAnimation.stop(); statusLabel.setText(Res.get("shared.messageStoredInMailbox")); break; + case SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG: + busyAnimation.stop(); + statusLabel.setText(Res.get("shared.messageArrived")); + break; case SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG: // We get a popup and the trade closed, so we dont need to show anything here busyAnimation.stop(); @@ -290,7 +289,8 @@ public class SellerStep3View extends TradeStepView { private boolean confirmPaymentReceivedPermitted() { if (!trade.confirmPermitted()) return false; - return trade.getState().ordinal() >= Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal() && trade.getState().ordinal() < Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal(); + if (trade.getState() == Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG) return false; + return trade.getState().ordinal() >= Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal() && trade.getState().ordinal() <= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal(); } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/docs/deployment-guide.md b/docs/deployment-guide.md index ab5b8ef7be..ac389bbefb 100644 --- a/docs/deployment-guide.md +++ b/docs/deployment-guide.md @@ -116,6 +116,9 @@ First rebuild Haveno: `make skip-tests`. Run `make arbitrator-desktop` to run an arbitrator on Monero's mainnet or `make arbitrator-desktop-stagenet` to run an arbitrator on Monero's stagenet. +> **Note** +> Unregister the arbitrator before retiring the app, or clients will continue to try to connect for some time. + The Haveno GUI will open. If on mainnet, ignore the error about not receiving a filter object which is not added yet. Click on the `Account` tab and then press `ctrl + r`. A prompt will open asking to enter the key to register the arbitrator. Use a key generated in the previous steps and complete the registration. The arbitrator is now registered and ready to accept requests of dispute resolution. Arbitrators should remain online as much as possible in order to balance trades and avoid clients spending time trying to contact offline arbitrators. A VPS or dedicated machine running 24/7 is highly recommended.