From 1b753e4f29503a6a36ebf6c2bdfd90bd5c9e9148 Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 6 Apr 2023 16:51:12 -0400 Subject: [PATCH] stability fixes on tor optimize when multisig info imported fetch updates for tx progress indicators off main thread add synchronization locks refactor address entry management add totalTxFee to process model prevent same user from taking same offer at same time set refresh rate to 30s for tor --- .../api/CoreMoneroConnectionsService.java | 15 +- .../haveno/core/offer/OfferBookService.java | 66 +++++---- .../haveno/core/offer/OpenOfferManager.java | 7 +- .../tasks/SendOfferAvailabilityRequest.java | 2 +- .../tasks/MakerReserveOfferFunds.java | 8 +- .../tasks/MakerSendSignOfferRequest.java | 2 +- .../core/offer/takeoffer/TakeOfferModel.java | 2 +- .../core/payment/payload/PaymentMethod.java | 2 +- .../core/support/dispute/DisputeManager.java | 2 +- .../java/haveno/core/trade/HavenoUtils.java | 18 ++- .../main/java/haveno/core/trade/Trade.java | 132 +++++++++++------ .../java/haveno/core/trade/TradeManager.java | 140 +++++++++--------- .../core/trade/protocol/TradeProtocol.java | 2 +- .../ArbitratorProcessDepositRequest.java | 1 + .../tasks/ArbitratorProcessReserveTx.java | 1 + .../tasks/BuyerPreparePaymentSentMessage.java | 3 + .../tasks/MakerSendInitTradeRequest.java | 2 +- .../ProcessDepositsConfirmedMessage.java | 1 - .../tasks/ProcessPaymentReceivedMessage.java | 1 + .../tasks/ProcessPaymentSentMessage.java | 19 +-- .../tasks/TakerReserveTradeFunds.java | 2 +- .../core/xmr/model/XmrAddressEntryList.java | 22 ++- .../xmr/setup/MoneroWalletRpcManager.java | 2 +- .../core/xmr/wallet/MoneroKeyImagePoller.java | 60 ++++---- .../core/xmr/wallet/XmrWalletService.java | 135 +++++++++-------- .../daemon/grpc/GrpcDisputesService.java | 2 +- .../desktop/components/TxIdTextField.java | 32 ++-- .../indicator/TxConfidenceIndicator.java | 3 +- .../market/offerbook/OfferBookChartView.java | 64 ++++---- .../offerbook/OfferBookChartViewModel.java | 34 +++-- .../pendingtrades/steps/TradeStepView.java | 23 ++- .../steps/buyer/BuyerStep2View.java | 2 +- proto/src/main/proto/pb.proto | 45 +++--- 33 files changed, 498 insertions(+), 354 deletions(-) diff --git a/core/src/main/java/haveno/core/api/CoreMoneroConnectionsService.java b/core/src/main/java/haveno/core/api/CoreMoneroConnectionsService.java index c1f05f3e..e6750f53 100644 --- a/core/src/main/java/haveno/core/api/CoreMoneroConnectionsService.java +++ b/core/src/main/java/haveno/core/api/CoreMoneroConnectionsService.java @@ -44,7 +44,8 @@ public final class CoreMoneroConnectionsService { private static final int MIN_BROADCAST_CONNECTIONS = 0; // TODO: 0 for stagenet, 5+ for mainnet private static final long REFRESH_PERIOD_LOCAL_MS = 5000; // refresh period when connected to local node - private static final long REFRESH_PERIOD_REMOTE_MS = 20000; // refresh period when connected to remote node + private static final long REFRESH_PERIOD_HTTP_MS = 20000; // refresh period when connected to remote node over http + private static final long REFRESH_PERIOD_ONION_MS = 30000; // refresh period when connected to remote node over tor private static final long MIN_ERROR_LOG_PERIOD_MS = 300000; // minimum period between logging errors fetching daemon info private static Long lastErrorTimestamp; @@ -157,6 +158,10 @@ public final class CoreMoneroConnectionsService { } } + public boolean isConnected() { + return connectionManager.isConnected(); + } + public void addConnection(MoneroRpcConnection connection) { synchronized (lock) { accountService.checkAccountOpen(); @@ -256,10 +261,12 @@ public final class CoreMoneroConnectionsService { if (daemon == null) return REFRESH_PERIOD_LOCAL_MS; else { if (isConnectionLocal()) { - if (lastInfo != null && (lastInfo.isBusySyncing() || (lastInfo.getHeightWithoutBootstrap() != null && lastInfo.getHeightWithoutBootstrap() > 0 && lastInfo.getHeightWithoutBootstrap() < lastInfo.getHeight()))) return REFRESH_PERIOD_REMOTE_MS; // refresh slower if syncing or bootstrapped + if (lastInfo != null && (lastInfo.isBusySyncing() || (lastInfo.getHeightWithoutBootstrap() != null && lastInfo.getHeightWithoutBootstrap() > 0 && lastInfo.getHeightWithoutBootstrap() < lastInfo.getHeight()))) return REFRESH_PERIOD_HTTP_MS; // refresh slower if syncing or bootstrapped else return REFRESH_PERIOD_LOCAL_MS; // TODO: announce faster refresh after done syncing + } else if (getConnection().isOnion()) { + return REFRESH_PERIOD_ONION_MS; } else { - return REFRESH_PERIOD_REMOTE_MS; + return REFRESH_PERIOD_HTTP_MS; } } } @@ -327,7 +334,7 @@ public final class CoreMoneroConnectionsService { // reset connection manager connectionManager.reset(); - connectionManager.setTimeout(REFRESH_PERIOD_REMOTE_MS); + connectionManager.setTimeout(REFRESH_PERIOD_HTTP_MS); // load connections log.info("TOR proxy URI: " + getProxyUri()); diff --git a/core/src/main/java/haveno/core/offer/OfferBookService.java b/core/src/main/java/haveno/core/offer/OfferBookService.java index e8144b05..de4b7ff2 100644 --- a/core/src/main/java/haveno/core/offer/OfferBookService.java +++ b/core/src/main/java/haveno/core/offer/OfferBookService.java @@ -107,32 +107,40 @@ public class OfferBookService { p2PService.addHashSetChangedListener(new HashMapChangedListener() { @Override public void onAdded(Collection protectedStorageEntries) { - protectedStorageEntries.forEach(protectedStorageEntry -> offerBookChangedListeners.forEach(listener -> { - if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) { - maybeInitializeKeyImagePoller(); - OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload(); - keyImagePoller.addKeyImages(offerPayload.getReserveTxKeyImages()); - Offer offer = new Offer(offerPayload); - offer.setPriceFeedService(priceFeedService); - setReservedFundsSpent(offer); - listener.onAdded(offer); - } - })); + protectedStorageEntries.forEach(protectedStorageEntry -> { + synchronized (offerBookChangedListeners) { + offerBookChangedListeners.forEach(listener -> { + if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) { + maybeInitializeKeyImagePoller(); + OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload(); + keyImagePoller.addKeyImages(offerPayload.getReserveTxKeyImages()); + Offer offer = new Offer(offerPayload); + offer.setPriceFeedService(priceFeedService); + setReservedFundsSpent(offer); + listener.onAdded(offer); + } + }); + } + }); } @Override public void onRemoved(Collection protectedStorageEntries) { - protectedStorageEntries.forEach(protectedStorageEntry -> offerBookChangedListeners.forEach(listener -> { - if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) { - maybeInitializeKeyImagePoller(); - OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload(); - keyImagePoller.removeKeyImages(offerPayload.getReserveTxKeyImages()); - Offer offer = new Offer(offerPayload); - offer.setPriceFeedService(priceFeedService); - setReservedFundsSpent(offer); - listener.onRemoved(offer); - } - })); + protectedStorageEntries.forEach(protectedStorageEntry -> { + synchronized (offerBookChangedListeners) { + offerBookChangedListeners.forEach(listener -> { + if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) { + maybeInitializeKeyImagePoller(); + OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload(); + keyImagePoller.removeKeyImages(offerPayload.getReserveTxKeyImages()); + Offer offer = new Offer(offerPayload); + offer.setPriceFeedService(priceFeedService); + setReservedFundsSpent(offer); + listener.onRemoved(offer); + } + }); + } + }); } }); @@ -244,7 +252,9 @@ public class OfferBookService { } public void addOfferBookChangedListener(OfferBookChangedListener offerBookChangedListener) { - offerBookChangedListeners.add(offerBookChangedListener); + synchronized (offerBookChangedListeners) { + offerBookChangedListeners.add(offerBookChangedListener); + } } @@ -280,10 +290,12 @@ public class OfferBookService { private void updateAffectedOffers(String keyImage) { for (Offer offer : getOffers()) { if (offer.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) { - offerBookChangedListeners.forEach(listener -> { - listener.onRemoved(offer); - listener.onAdded(offer); - }); + synchronized (offerBookChangedListeners) { + offerBookChangedListeners.forEach(listener -> { + listener.onRemoved(offer); + listener.onAdded(offer); + }); + } } } } diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index cebbc39d..cb1aaeb0 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -984,6 +984,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe BigInteger sendAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.valueOf(0) : offer.getAmount(); BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getBuyerSecurityDeposit() : offer.getSellerSecurityDeposit(); Tuple2 txResult = xmrWalletService.verifyTradeTx( + offer.getId(), tradeFee, sendAmount, securityDeposit, @@ -1168,9 +1169,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe OfferAvailabilityResponse offerAvailabilityResponse = new OfferAvailabilityResponse(request.offerId, availabilityResult, makerSignature); - log.info("Send {} with offerId {} and uid {} to peer {}", + log.info("Send {} with offerId {}, uid {}, and result {} to peer {}", offerAvailabilityResponse.getClass().getSimpleName(), offerAvailabilityResponse.getOfferId(), - offerAvailabilityResponse.getUid(), peer); + offerAvailabilityResponse.getUid(), + availabilityResult, + peer); p2PService.sendEncryptedDirectMessage(peer, request.getPubKeyRing(), offerAvailabilityResponse, diff --git a/core/src/main/java/haveno/core/offer/availability/tasks/SendOfferAvailabilityRequest.java b/core/src/main/java/haveno/core/offer/availability/tasks/SendOfferAvailabilityRequest.java index e90da7b2..38a1b9e6 100644 --- a/core/src/main/java/haveno/core/offer/availability/tasks/SendOfferAvailabilityRequest.java +++ b/core/src/main/java/haveno/core/offer/availability/tasks/SendOfferAvailabilityRequest.java @@ -55,7 +55,7 @@ public class SendOfferAvailabilityRequest extends Task { XmrWalletService walletService = model.getXmrWalletService(); String paymentAccountId = model.getPaymentAccountId(); String paymentMethodId = user.getPaymentAccount(paymentAccountId).getPaymentAccountPayload().getPaymentMethodId(); - String payoutAddress = walletService.getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); // reserve new payout address + String payoutAddress = walletService.getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); // taker signs offer using offer id as nonce to avoid challenge protocol byte[] sig = HavenoUtils.sign(model.getP2PService().getKeyRing(), offer.getId()); diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java index 03beba2a..247ca763 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java @@ -53,9 +53,15 @@ public class MakerReserveOfferFunds extends Task { BigInteger makerFee = offer.getMakerFee(); BigInteger sendAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.valueOf(0) : offer.getAmount(); BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getBuyerSecurityDeposit() : offer.getSellerSecurityDeposit(); - String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); + String returnAddress = model.getXmrWalletService().getNewAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(makerFee, sendAmount, securityDeposit, returnAddress); + // check for error in case creating reserve tx exceeded timeout + // TODO: better way? + if (!model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).isPresent()) { + throw new RuntimeException("An error has occurred posting offer " + offer.getId() + " causing its subaddress entry to be deleted"); + } + // collect reserved key images List reservedKeyImages = new ArrayList(); for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex()); diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java index 6306d511..94d18309 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java @@ -60,7 +60,7 @@ public class MakerSendSignOfferRequest extends Task { runInterceptHook(); // create request for arbitrator to sign offer - String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); + String returnAddress = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(); SignOfferRequest request = new SignOfferRequest( model.getOffer().getId(), P2PService.getMyNodeAddress(), diff --git a/core/src/main/java/haveno/core/offer/takeoffer/TakeOfferModel.java b/core/src/main/java/haveno/core/offer/takeoffer/TakeOfferModel.java index a1dba0be..c4065f65 100644 --- a/core/src/main/java/haveno/core/offer/takeoffer/TakeOfferModel.java +++ b/core/src/main/java/haveno/core/offer/takeoffer/TakeOfferModel.java @@ -96,7 +96,7 @@ public class TakeOfferModel implements Model { this.clearModel(); this.offer = offer; this.paymentAccount = paymentAccount; - this.addressEntry = xmrWalletService.getOrCreateAddressEntry(offer.getId(), OFFER_FUNDING); // TODO (woodser): replace with xmr or remove + this.addressEntry = xmrWalletService.getOrCreateAddressEntry(offer.getId(), OFFER_FUNDING); validateModelInputs(); this.useSavingsWallet = useSavingsWallet; diff --git a/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java b/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java index 4562d424..408b6aef 100644 --- a/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java +++ b/core/src/main/java/haveno/core/payment/payload/PaymentMethod.java @@ -99,7 +99,7 @@ public final class PaymentMethod implements PersistablePayload, Comparable> extends Sup if (!trade.isPayoutPublished()) { // create unsigned dispute payout tx - log.info("Arbitrator creating unsigned dispute payout tx for trade {}", trade.getId()); + log.info("Creating unsigned dispute payout tx for trade {}", trade.getId()); try { // trade wallet must be synced diff --git a/core/src/main/java/haveno/core/trade/HavenoUtils.java b/core/src/main/java/haveno/core/trade/HavenoUtils.java index 5e3c5121..f93a5efa 100644 --- a/core/src/main/java/haveno/core/trade/HavenoUtils.java +++ b/core/src/main/java/haveno/core/trade/HavenoUtils.java @@ -471,16 +471,24 @@ public class HavenoUtils { } public static void executeTasks(Collection tasks, int maxConcurrency) { + executeTasks(tasks, maxConcurrency, null); + } + + public static void executeTasks(Collection tasks, int maxConcurrency, Long timeoutSeconds) { if (tasks.isEmpty()) return; ExecutorService pool = Executors.newFixedThreadPool(maxConcurrency); List> futures = new ArrayList>(); for (Runnable task : tasks) futures.add(pool.submit(task)); pool.shutdown(); - try { - if (!pool.awaitTermination(60, TimeUnit.SECONDS)) pool.shutdownNow(); - } catch (InterruptedException e) { - pool.shutdownNow(); - throw new RuntimeException(e); + + // interrupt after timeout + if (timeoutSeconds != null) { + try { + if (!pool.awaitTermination(timeoutSeconds, TimeUnit.SECONDS)) pool.shutdownNow(); + } catch (InterruptedException e) { + pool.shutdownNow(); + throw new RuntimeException(e); + } } // throw exception from any tasks diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index ef112195..c978c7c4 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -317,6 +317,7 @@ public abstract class Trade implements Tradable, Model { @Getter private final Offer offer; private final long takerFee; + private final long totalTxFee; // Added in 1.5.1 @Getter @@ -362,8 +363,6 @@ public abstract class Trade implements Tradable, Model { // Transient // Immutable @Getter - transient final private BigInteger totalTxFee; - @Getter transient final private XmrWalletService xmrWalletService; transient final private ObjectProperty stateProperty = new SimpleObjectProperty<>(state); @@ -385,6 +384,7 @@ public abstract class Trade implements Tradable, Model { // Mutable @Getter transient private boolean isInitialized; + @Getter transient private boolean isShutDown; // Added in v1.2.0 @@ -465,7 +465,7 @@ public abstract class Trade implements Tradable, Model { this.offer = offer; this.amount = tradeAmount.longValueExact(); this.takerFee = takerFee.longValueExact(); - this.totalTxFee = BigInteger.valueOf(0); // TODO: sum tx fees + this.totalTxFee = 0l; // TODO: sum tx fees this.price = tradePrice; this.xmrWalletService = xmrWalletService; this.processModel = processModel; @@ -585,6 +585,18 @@ public abstract class Trade implements Tradable, Model { return; } + // reset payment sent state if no ack receive + if (getState().ordinal() >= Trade.State.BUYER_CONFIRMED_IN_UI_PAYMENT_SENT.ordinal() && getState().ordinal() < Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG.ordinal()) { + log.warn("Resetting state of {} {} from {} to {} because no ack was received", getClass().getSimpleName(), getId(), getState(), Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN); + setState(Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN); + } + + // reset payment received state if no ack receive + if (getState().ordinal() >= Trade.State.SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT.ordinal() && getState().ordinal() < Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG.ordinal()) { + log.warn("Resetting state of {} {} from {} to {} because no ack was received", getClass().getSimpleName(), getId(), getState(), Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG); + setState(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG); + } + // handle trade state events tradePhaseSubscription = EasyBind.subscribe(phaseProperty, newValue -> { if (isDepositsPublished() && !isPayoutUnlocked()) updateWalletRefreshPeriod(); @@ -621,10 +633,6 @@ public abstract class Trade implements Tradable, Model { if (!isInitialized) return; log.info("Payout unlocked for {} {}, deleting multisig wallet", getClass().getSimpleName(), getId()); deleteWallet(); - if (txPollLooper != null) { - txPollLooper.stop(); - txPollLooper = null; - } if (idlePayoutSyncer != null) { xmrWalletService.removeWalletListener(idlePayoutSyncer); idlePayoutSyncer = null; @@ -702,6 +710,7 @@ public abstract class Trade implements Tradable, Model { synchronized (walletLock) { if (wallet != null) return wallet; if (!walletExists()) return null; + if (isShutDown) throw new RuntimeException("Cannot open wallet for " + getClass().getSimpleName() + " " + getId() + " because trade is shut down"); if (!isShutDown) wallet = xmrWalletService.openWallet(getWalletName()); return wallet; } @@ -746,7 +755,6 @@ public abstract class Trade implements Tradable, Model { } catch (Exception e) { if (!isShutDown) { log.warn("Error syncing trade wallet for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage()); - e.printStackTrace(); } } } @@ -784,6 +792,7 @@ public abstract class Trade implements Tradable, Model { private void closeWallet() { synchronized (walletLock) { if (wallet == null) throw new RuntimeException("Trade wallet to close was not previously opened for trade " + getId()); + stopPolling(); xmrWalletService.closeWallet(wallet, true); wallet = null; } @@ -977,10 +986,21 @@ public abstract class Trade implements Tradable, Model { if (sign) { // sign tx - MoneroMultisigSignResult result = wallet.signMultisigTxHex(payoutTxHex); - if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing payout tx"); - payoutTxHex = result.getSignedMultisigTxHex(); - describedTxSet = wallet.describeMultisigTxSet(payoutTxHex); // update described set + try { + MoneroMultisigSignResult result = wallet.signMultisigTxHex(payoutTxHex); + if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing payout tx"); + payoutTxHex = result.getSignedMultisigTxHex(); + } catch (Exception e) { + if (getPayoutTxHex() != null) { + log.info("Reusing previous payout tx for {} {} because signing failed with error \"{}\"", getClass().getSimpleName(), getId(), e.getMessage()); // in case previous message with signed tx failed to send + payoutTxHex = getPayoutTxHex(); + } else { + throw e; + } + } + + // describe result + describedTxSet = wallet.describeMultisigTxSet(payoutTxHex); payoutTx = describedTxSet.getTxs().get(0); // verify fee is within tolerance by recreating payout tx @@ -1049,6 +1069,7 @@ public abstract class Trade implements Tradable, Model { private MoneroTx getDepositTx(TradePeer trader) { String depositId = trader.getDepositTxHash(); + if (depositId == null) return null; try { if (trader.getDepositTx() == null || !trader.getDepositTx().isConfirmed()) { trader.setDepositTx(getTxFromWalletOrDaemon(depositId)); @@ -1106,21 +1127,18 @@ public abstract class Trade implements Tradable, Model { } public void shutDown() { - synchronized (walletLock) { + synchronized (this) { log.info("Shutting down {} {}", getClass().getSimpleName(), getId()); isInitialized = false; isShutDown = true; if (wallet != null) closeWallet(); - if (txPollLooper != null) { - txPollLooper.stop(); - txPollLooper = null; - } if (tradePhaseSubscription != null) tradePhaseSubscription.unsubscribe(); if (payoutStateSubscription != null) payoutStateSubscription.unsubscribe(); if (idlePayoutSyncer != null) { xmrWalletService.removeWalletListener(idlePayoutSyncer); idlePayoutSyncer = null; } + log.info("Done shutting down {} {}", getClass().getSimpleName(), getId()); } } @@ -1558,6 +1576,10 @@ public abstract class Trade implements Tradable, Model { return BigInteger.valueOf(takerFee); } + public BigInteger getTotalTxFee() { + return BigInteger.valueOf(totalTxFee); + } + public BigInteger getBuyerSecurityDeposit() { if (getBuyer().getDepositTxHash() == null) return null; return getBuyer().getSecurityDeposit(); @@ -1621,34 +1643,38 @@ public abstract class Trade implements Tradable, Model { } private void setDaemonConnection(MoneroRpcConnection connection) { - MoneroWallet wallet = getWallet(); - if (wallet == null) return; - log.info("Setting daemon connection for trade wallet {}: {}", getId() , connection == null ? null : connection.getUri()); - wallet.setDaemonConnection(connection); - - // sync and reprocess messages on new thread - if (connection != null && !Boolean.FALSE.equals(connection.isConnected())) { - HavenoUtils.submitTask(() -> { - updateSyncing(); - - // reprocess pending payout messages - this.getProtocol().maybeReprocessPaymentReceivedMessage(false); - HavenoUtils.arbitrationManager.maybeReprocessDisputeClosedMessage(this, false); - }); + synchronized (walletLock) { + if (isShutDown) return; + MoneroWallet wallet = getWallet(); + if (wallet == null) return; + log.info("Setting daemon connection for trade wallet {}: {}", getId() , connection == null ? null : connection.getUri()); + wallet.setDaemonConnection(connection); + updateWalletRefreshPeriod(); + + // sync and reprocess messages on new thread + if (connection != null && !Boolean.FALSE.equals(connection.isConnected())) { + HavenoUtils.submitTask(() -> { + updateSyncing(); + + // reprocess pending payout messages + this.getProtocol().maybeReprocessPaymentReceivedMessage(false); + HavenoUtils.arbitrationManager.maybeReprocessDisputeClosedMessage(this, false); + }); + } } } private void updateSyncing() { if (isShutDown) return; if (!isIdling()) { - trySyncWallet(); updateWalletRefreshPeriod(); + trySyncWallet(); } else { long startSyncingInMs = ThreadLocalRandom.current().nextLong(0, getWalletRefreshPeriod()); // random time to start syncing UserThread.runAfter(() -> { if (!isShutDown) { - trySyncWallet(); updateWalletRefreshPeriod(); + trySyncWallet(); } }, startSyncingInMs / 1000l); } @@ -1659,27 +1685,35 @@ public abstract class Trade implements Tradable, Model { } private void setWalletRefreshPeriod(long walletRefreshPeriod) { - if (this.isShutDown) return; - if (this.walletRefreshPeriod != null && this.walletRefreshPeriod == walletRefreshPeriod) return; - this.walletRefreshPeriod = walletRefreshPeriod; synchronized (walletLock) { + if (this.isShutDown) return; + if (this.walletRefreshPeriod != null && this.walletRefreshPeriod == walletRefreshPeriod) return; + this.walletRefreshPeriod = walletRefreshPeriod; if (getWallet() != null) { log.info("Setting wallet refresh rate for {} {} to {}", getClass().getSimpleName(), getId(), walletRefreshPeriod); getWallet().startSyncing(getWalletRefreshPeriod()); // TODO (monero-project): wallet rpc waits until last sync period finishes before starting new sync period } - if (txPollLooper != null) { - txPollLooper.stop(); - txPollLooper = null; - } + stopPolling(); } startPolling(); } private void startPolling() { - if (txPollLooper != null) return; - log.info("Listening for payout tx for {} {}", getClass().getSimpleName(), getId()); - txPollLooper = new TaskLooper(() -> { pollWallet(); }); - txPollLooper.start(walletRefreshPeriod); + synchronized (walletLock) { + if (txPollLooper != null) return; + log.info("Starting to poll wallet for {} {}", getClass().getSimpleName(), getId()); + txPollLooper = new TaskLooper(() -> { pollWallet(); }); + txPollLooper.start(walletRefreshPeriod); + } + } + + private void stopPolling() { + synchronized (walletLock) { + if (txPollLooper != null) { + txPollLooper.stop(); + txPollLooper = null; + } + } } private void pollWallet() { @@ -1698,6 +1732,7 @@ public abstract class Trade implements Tradable, Model { .setHashes(Arrays.asList(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash())) .setIncludeOutputs(true)); } catch (Exception e) { + if (!isShutDown) log.info("Could not fetch deposit txs from wallet for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage()); // expected at first return; } @@ -1750,7 +1785,10 @@ public abstract class Trade implements Tradable, Model { } } } catch (Exception e) { - if (!isShutDown && getWallet() != null && isWalletConnected()) log.warn("Error polling trade wallet {}: {}", getId(), e.getMessage()); + if (!isShutDown && getWallet() != null && isWalletConnected()) { + log.warn("Error polling trade wallet {}: {}", getId(), e.getMessage()); + e.printStackTrace(); + } } } @@ -1844,6 +1882,7 @@ public abstract class Trade implements Tradable, Model { protobuf.Trade.Builder builder = protobuf.Trade.newBuilder() .setOffer(offer.toProtoMessage()) .setTakerFee(takerFee) + .setTotalTxFee(totalTxFee) .setTakeOfferDate(takeOfferDate) .setProcessModel(processModel.toProtoMessage()) .setAmount(amount) @@ -1868,7 +1907,7 @@ public abstract class Trade implements Tradable, Model { Optional.ofNullable(mediationResultState).ifPresent(e -> builder.setMediationResultState(MediationResultState.toProtoMessage(mediationResultState))); Optional.ofNullable(refundResultState).ifPresent(e -> builder.setRefundResultState(RefundResultState.toProtoMessage(refundResultState))); Optional.ofNullable(payoutTxHex).ifPresent(e -> builder.setPayoutTxHex(payoutTxHex)); - Optional.ofNullable(payoutTxKey).ifPresent(e -> builder.setPayoutTxHex(payoutTxKey)); + Optional.ofNullable(payoutTxKey).ifPresent(e -> builder.setPayoutTxKey(payoutTxKey)); Optional.ofNullable(counterCurrencyExtraData).ifPresent(e -> builder.setCounterCurrencyExtraData(counterCurrencyExtraData)); Optional.ofNullable(assetTxProofResult).ifPresent(e -> builder.setAssetTxProofResult(assetTxProofResult.name())); return builder.build(); @@ -1913,6 +1952,7 @@ public abstract class Trade implements Tradable, Model { return "Trade{" + "\n offer=" + offer + ",\n takerFee=" + takerFee + + ",\n totalTxFee=" + totalTxFee + ",\n takeOfferDate=" + takeOfferDate + ",\n processModel=" + processModel + ",\n payoutTxId='" + payoutTxId + '\'' + diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index 9f33e905..4eab1740 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -353,16 +353,16 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } public TradeProtocol getTradeProtocol(Trade trade) { - String uid = trade.getUid(); - if (tradeProtocolByTradeId.containsKey(uid)) { - return tradeProtocolByTradeId.get(uid); - } else { - TradeProtocol tradeProtocol = TradeProtocolFactory.getNewTradeProtocol(trade); - TradeProtocol prev = tradeProtocolByTradeId.put(uid, tradeProtocol); - if (prev != null) { - log.error("We had already an entry with uid {}", trade.getUid()); - } + synchronized (tradeProtocolByTradeId) { + return tradeProtocolByTradeId.get(trade.getUid()); + } + } + public TradeProtocol createTradeProtocol(Trade trade) { + synchronized (tradeProtocolByTradeId) { + TradeProtocol tradeProtocol = TradeProtocolFactory.getNewTradeProtocol(trade); + TradeProtocol prev = tradeProtocolByTradeId.put(trade.getUid(), tradeProtocol); + if (prev != null) log.error("We had already an entry with uid {}", trade.getUid()); return tradeProtocol; } } @@ -377,6 +377,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi List trades = getAllTrades(); // open trades in parallel since each may open a multisig wallet + log.info("Initializing trades"); int threadPoolSize = 10; Set tasks = new HashSet(); for (Trade trade : trades) { @@ -387,8 +388,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } }); }; - log.info("Initializing persisted trades"); HavenoUtils.executeTasks(tasks, threadPoolSize); + log.info("Done initializing trades"); // reset any available address entries if (isShutDown) return; @@ -419,7 +420,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi private void initPersistedTrade(Trade trade) { if (isShutDown) return; - initTradeAndProtocol(trade, getTradeProtocol(trade)); + initTradeAndProtocol(trade, createTradeProtocol(trade)); requestPersistence(); scheduleDeletionIfUnfunded(trade); } @@ -463,7 +464,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } } if (offer == null) { - log.warn("Ignoring InitTradeRequest from {} with tradeId {} because no offer is on the books", sender, request.getTradeId()); + log.warn("Ignoring InitTradeRequest from {} with tradeId {} because offer is not on the books", sender, request.getTradeId()); return; } @@ -489,38 +490,39 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } } else { - // verify request is from taker - if (!sender.equals(request.getTakerNodeAddress())) { - log.warn("Ignoring InitTradeRequest from {} with tradeId {} because request must be from taker when trade is not initialized", sender, request.getTradeId()); - return; - } + // verify request is from taker + if (!sender.equals(request.getTakerNodeAddress())) { + log.warn("Ignoring InitTradeRequest from {} with tradeId {} because request must be from taker when trade is not initialized", sender, request.getTradeId()); + return; + } - // get expected taker fee - BigInteger takerFee = HavenoUtils.getTakerFee(BigInteger.valueOf(offer.getOfferPayload().getAmount())); + // get expected taker fee + BigInteger takerFee = HavenoUtils.getTakerFee(BigInteger.valueOf(offer.getOfferPayload().getAmount())); - // create arbitrator trade - trade = new ArbitratorTrade(offer, - BigInteger.valueOf(offer.getOfferPayload().getAmount()), - takerFee, - offer.getOfferPayload().getPrice(), - xmrWalletService, - getNewProcessModel(offer), - UUID.randomUUID().toString(), - request.getMakerNodeAddress(), - request.getTakerNodeAddress(), - request.getArbitratorNodeAddress()); + // create arbitrator trade + trade = new ArbitratorTrade(offer, + BigInteger.valueOf(offer.getOfferPayload().getAmount()), + takerFee, + offer.getOfferPayload().getPrice(), + xmrWalletService, + getNewProcessModel(offer), + UUID.randomUUID().toString(), + request.getMakerNodeAddress(), + request.getTakerNodeAddress(), + request.getArbitratorNodeAddress()); - // set reserve tx hash if available - Optional signedOfferOptional = openOfferManager.getSignedOfferById(request.getTradeId()); - if (signedOfferOptional.isPresent()) { - SignedOffer signedOffer = signedOfferOptional.get(); - trade.getMaker().setReserveTxHash(signedOffer.getReserveTxHash()); - } + // set reserve tx hash if available + Optional signedOfferOptional = openOfferManager.getSignedOfferById(request.getTradeId()); + if (signedOfferOptional.isPresent()) { + SignedOffer signedOffer = signedOfferOptional.get(); + trade.getMaker().setReserveTxHash(signedOffer.getReserveTxHash()); + } - initTradeAndProtocol(trade, getTradeProtocol(trade)); - synchronized (tradableList) { - tradableList.add(trade); - } + // initialize trade protocol + initTradeAndProtocol(trade, createTradeProtocol(trade)); + synchronized (tradableList) { + tradableList.add(trade); + } } ((ArbitratorProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> { @@ -596,7 +598,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi trade.getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing()); trade.getMaker().setPubKeyRing(trade.getOffer().getPubKeyRing()); - initTradeAndProtocol(trade, getTradeProtocol(trade)); + initTradeAndProtocol(trade, createTradeProtocol(trade)); trade.getSelf().setPaymentAccountId(offer.getOfferPayload().getMakerPaymentAccountId()); trade.getSelf().setReserveTxHash(openOffer.getReserveTxHash()); // TODO (woodser): initialize in initTradeAndProtocol? trade.getSelf().setReserveTxHex(openOffer.getReserveTxHex()); @@ -782,11 +784,12 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi trade.getSelf().setPubKeyRing(model.getPubKeyRing()); trade.getSelf().setPaymentAccountId(paymentAccountId); - TradeProtocol tradeProtocol = TradeProtocolFactory.getNewTradeProtocol(trade); - TradeProtocol prev = tradeProtocolByTradeId.put(trade.getUid(), tradeProtocol); - if (prev != null) { - log.error("We had already an entry with uid {}", trade.getUid()); - } + // ensure trade is not already open + Optional tradeOptional = getOpenTrade(offer.getId()); + if (tradeOptional.isPresent()) throw new RuntimeException("Cannot create trade protocol because trade with ID " + trade.getId() + " is already open"); + + // initialize trade protocol + TradeProtocol tradeProtocol = createTradeProtocol(trade); synchronized (tradableList) { tradableList.add(trade); } @@ -804,11 +807,14 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi if (takeOfferRequestErrorMessageHandler != null) takeOfferRequestErrorMessageHandler.handleErrorMessage(errorMessage); }); requestPersistence(); + } else { + log.warn("Cannot take offer {} because it's not available, state={}", offer.getId(), offer.getState()); } }, errorMessage -> { log.warn("Taker error during check offer availability: " + errorMessage); errorMessageHandler.handleErrorMessage(errorMessage); + if (takeOfferRequestErrorMessageHandler != null) takeOfferRequestErrorMessageHandler.handleErrorMessage(errorMessage); }); requestPersistence(); @@ -958,32 +964,32 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi .collect(Collectors.toSet())); tradesIdSet.addAll(closedTradableManager.getTradesStreamWithFundsLockedIn() .map(trade -> { - MoneroTx makerDepositTx = trade.getMakerDepositTx(); - if (makerDepositTx != null) { - if (!makerDepositTx.isConfirmed()) { - tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithUnconfirmedDepositTx", trade.getShortId()))); // TODO (woodser): rename to closedTradeWithLockedDepositTx + MoneroTx makerDepositTx = trade.getMakerDepositTx(); + if (makerDepositTx != null) { + if (!makerDepositTx.isConfirmed()) { + tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithUnconfirmedDepositTx", trade.getShortId()))); // TODO (woodser): rename to closedTradeWithLockedDepositTx + } else { + log.warn("We found a closed trade with locked up funds. " + + "That should never happen. {} ID={}, state={}, payoutState={}, disputeState={}", trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState()); + } } else { - log.warn("We found a closed trade with locked up funds. " + - "That should never happen. {} ID={}, state={}, payoutState={}, disputeState={}", trade.getId(), trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState()); + log.warn("Closed trade with locked up funds missing maker deposit tx. {} ID={}, state={}, payoutState={}, disputeState={}", trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState()); + tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithNoDepositTx", trade.getShortId()))); } - } else { - log.warn("Closed trade with locked up funds missing maker deposit tx. {} ID={}, state={}, payoutState={}, disputeState={}", trade.getId(), trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState()); - tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithNoDepositTx", trade.getShortId()))); - } - MoneroTx takerDepositTx = trade.getTakerDepositTx(); - if (takerDepositTx != null) { - if (!takerDepositTx.isConfirmed()) { - tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithUnconfirmedDepositTx", trade.getShortId()))); + MoneroTx takerDepositTx = trade.getTakerDepositTx(); + if (takerDepositTx != null) { + if (!takerDepositTx.isConfirmed()) { + tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithUnconfirmedDepositTx", trade.getShortId()))); + } else { + log.warn("We found a closed trade with locked up funds. " + + "That should never happen. trade ID={} ID={}, state={}, payoutState={}, disputeState={}", trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState()); + } } else { - log.warn("We found a closed trade with locked up funds. " + - "That should never happen. trade ID={} ID={}, state={}, payoutState={}, disputeState={}", trade.getId(), trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState()); + log.warn("Closed trade with locked up funds missing taker deposit tx. {} ID={}, state={}, payoutState={}, disputeState={}", trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState()); + tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithNoDepositTx", trade.getShortId()))); } - } else { - log.warn("Closed trade with locked up funds missing taker deposit tx. {} ID={}, state={}, payoutState={}, disputeState={}", trade.getId(), trade.getClass().getSimpleName(), trade.getId(), trade.getState(), trade.getPayoutState(), trade.getDisputeState()); - tradeTxException.set(new TradeTxException(Res.get("error.closedTradeWithNoDepositTx", trade.getShortId()))); - } - return trade.getId(); + return trade.getId(); }) .collect(Collectors.toSet())); diff --git a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java index f8834fe7..42bb11b2 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -162,7 +162,6 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D // TODO (woodser): this method only necessary because isPubKeyValid not called with sender argument, so it's validated before private void handleMailboxCollectionSkipValidation(Collection collection) { - log.warn("TradeProtocol.handleMailboxCollectionSkipValidation"); collection.stream() .map(DecryptedMessageWithPubKey::getNetworkEnvelope) .filter(this::isMyMessage) @@ -817,6 +816,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D protected void latchTrade() { if (tradeLatch != null) throw new RuntimeException("Trade latch is not null. That should never happen."); + if (trade.isShutDown()) throw new RuntimeException("Cannot latch trade " + trade.getId() + " for protocol because it's shut down"); tradeLatch = new CountDownLatch(1); } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java index 12e1df3b..860a7f0a 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java @@ -85,6 +85,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask { // verify deposit tx try { trade.getXmrWalletService().verifyTradeTx( + offer.getId(), tradeFee, sendAmount, securityDeposit, diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessReserveTx.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessReserveTx.java index e5ee7584..fae55c45 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessReserveTx.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessReserveTx.java @@ -60,6 +60,7 @@ public class ArbitratorProcessReserveTx extends TradeTask { Tuple2 txResult; try { txResult = trade.getXmrWalletService().verifyTradeTx( + offer.getId(), tradeFee, sendAmount, securityDeposit, diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java index 1983b15e..7e685950 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java @@ -62,6 +62,9 @@ public class BuyerPreparePaymentSentMessage extends TradeTask { // create payout tx if we have seller's updated multisig hex if (trade.getSeller().getUpdatedMultisigHex() != null) { + // import multisig hex + trade.importMultisigHex(); + // create payout tx log.info("Buyer creating unsigned payout tx"); MoneroTxWallet payoutTx = trade.createPayoutTx(); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequest.java index 81be5fb2..f34ee969 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequest.java @@ -70,7 +70,7 @@ public class MakerSendInitTradeRequest extends TradeTask { trade.getSelf().getReserveTxHash(), trade.getSelf().getReserveTxHex(), trade.getSelf().getReserveTxKey(), - model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(), + model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(), null); // send request to arbitrator 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 550f3a4c..add97ee2 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 @@ -55,7 +55,6 @@ public class ProcessDepositsConfirmedMessage extends TradeTask { // update multisig hex sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex()); - trade.importMultisigHex(); // decrypt seller payment account payload if key given if (request.getSellerPaymentAccountKey() != null && trade.getTradePeer().getPaymentAccountPayload() == null) { 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 b4dae07d..580424ab 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 @@ -126,6 +126,7 @@ public class ProcessPaymentReceivedMessage extends TradeTask { trade.verifyPayoutTx(message.getSignedPayoutTxHex(), false, true); } else { try { + if (trade.getProcessModel().getPaymentSentMessage() == null) throw new RuntimeException("Process model does not have payment sent message for " + trade.getClass().getSimpleName() + " " + trade.getId()); if (StringUtils.equals(trade.getPayoutTxHex(), trade.getProcessModel().getPaymentSentMessage().getPayoutTxHex())) { // unsigned log.info("{} {} verifying, signing, and publishing seller's payout tx", trade.getClass().getSimpleName(), trade.getId()); trade.verifyPayoutTx(message.getUnsignedPayoutTxHex(), true, true); 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 a78182b9..0d9261e2 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 @@ -44,22 +44,17 @@ public class ProcessPaymentSentMessage extends TradeTask { // verify signature of payment sent message HavenoUtils.verifyPaymentSentMessage(trade, message); - // set state - processModel.setPaymentSentMessage(message); - trade.setPayoutTxHex(message.getPayoutTxHex()); - trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex()); - trade.getSeller().setAccountAgeWitness(message.getSellerAccountAgeWitness()); - - // import multisig hex - trade.importMultisigHex(); + // update latest peer address + trade.getBuyer().setNodeAddress(processModel.getTempTradePeerNodeAddress()); // if seller, decrypt buyer's payment account payload if (trade.isSeller()) trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey()); - // update latest peer address - trade.getBuyer().setNodeAddress(processModel.getTempTradePeerNodeAddress()); - - // set state + // update state + processModel.setPaymentSentMessage(message); + trade.setPayoutTxHex(message.getPayoutTxHex()); + trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex()); + trade.getSeller().setAccountAgeWitness(message.getSellerAccountAgeWitness()); String counterCurrencyTxId = message.getCounterCurrencyTxId(); if (counterCurrencyTxId != null && counterCurrencyTxId.length() < 100) trade.setCounterCurrencyTxId(counterCurrencyTxId); String counterCurrencyExtraData = message.getCounterCurrencyExtraData(); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java index 46038c4a..f7797785 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java @@ -43,7 +43,7 @@ public class TakerReserveTradeFunds extends TradeTask { BigInteger takerFee = trade.getTakerFee(); BigInteger sendAmount = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getOffer().getAmount() : BigInteger.valueOf(0); BigInteger securityDeposit = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getOffer().getSellerSecurityDeposit() : trade.getOffer().getBuyerSecurityDeposit(); - String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); + String returnAddress = model.getXmrWalletService().getAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(); MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(takerFee, sendAmount, securityDeposit, returnAddress); // collect reserved key images diff --git a/core/src/main/java/haveno/core/xmr/model/XmrAddressEntryList.java b/core/src/main/java/haveno/core/xmr/model/XmrAddressEntryList.java index 7ab0a0d6..1796b7d1 100644 --- a/core/src/main/java/haveno/core/xmr/model/XmrAddressEntryList.java +++ b/core/src/main/java/haveno/core/xmr/model/XmrAddressEntryList.java @@ -93,7 +93,7 @@ public final class XmrAddressEntryList implements PersistableEnvelope, Persisted return ImmutableList.copyOf(entrySet); } - public void addAddressEntry(XmrAddressEntry addressEntry) { + public boolean addAddressEntry(XmrAddressEntry addressEntry) { boolean entryWithSameOfferIdAndContextAlreadyExist = entrySet.stream().anyMatch(e -> { if (addressEntry.getOfferId() != null) { return addressEntry.getOfferId().equals(e.getOfferId()) && addressEntry.getContext() == e.getContext(); @@ -101,14 +101,12 @@ public final class XmrAddressEntryList implements PersistableEnvelope, Persisted return false; }); if (entryWithSameOfferIdAndContextAlreadyExist) { - log.error("We have an address entry with the same offer ID and context. We do not add the new one. " + - "addressEntry={}, entrySet={}", addressEntry, entrySet); - return; + throw new IllegalArgumentException("We have an address entry with the same offer ID and context. We do not add the new one. addressEntry=" + addressEntry); } boolean setChangedByAdd = entrySet.add(addressEntry); - if (setChangedByAdd) - requestPersistence(); + if (setChangedByAdd) requestPersistence(); + return setChangedByAdd; } public void swapToAvailable(XmrAddressEntry addressEntry) { @@ -123,9 +121,19 @@ public final class XmrAddressEntryList implements PersistableEnvelope, Persisted public XmrAddressEntry swapAvailableToAddressEntryWithOfferId(XmrAddressEntry addressEntry, XmrAddressEntry.Context context, String offerId) { + // remove old entry boolean setChangedByRemove = entrySet.remove(addressEntry); + + // add new entry final XmrAddressEntry newAddressEntry = new XmrAddressEntry(addressEntry.getSubaddressIndex(), addressEntry.getAddressString(), context, offerId, null); - boolean setChangedByAdd = entrySet.add(newAddressEntry); + boolean setChangedByAdd = false; + try { + setChangedByAdd = addAddressEntry(newAddressEntry); + } catch (Exception e) { + entrySet.add(addressEntry); // undo change if error + throw e; + } + if (setChangedByRemove || setChangedByAdd) requestPersistence(); diff --git a/core/src/main/java/haveno/core/xmr/setup/MoneroWalletRpcManager.java b/core/src/main/java/haveno/core/xmr/setup/MoneroWalletRpcManager.java index 85894428..c59bd919 100644 --- a/core/src/main/java/haveno/core/xmr/setup/MoneroWalletRpcManager.java +++ b/core/src/main/java/haveno/core/xmr/setup/MoneroWalletRpcManager.java @@ -130,7 +130,7 @@ public class MoneroWalletRpcManager { // stop process String pid = walletRpc.getProcess() == null ? null : String.valueOf(walletRpc.getProcess().pid()); - log.info("Stopping MoneroWalletRpc port: {} pid: {}", port, pid); + log.info("Stopping MoneroWalletRpc path={}, port={}, pid={}", walletRpc.getPath(), port, pid); walletRpc.stopProcess(); } diff --git a/core/src/main/java/haveno/core/xmr/wallet/MoneroKeyImagePoller.java b/core/src/main/java/haveno/core/xmr/wallet/MoneroKeyImagePoller.java index 88106baa..872ff293 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/MoneroKeyImagePoller.java +++ b/core/src/main/java/haveno/core/xmr/wallet/MoneroKeyImagePoller.java @@ -190,7 +190,9 @@ public class MoneroKeyImagePoller { Set containedKeyImages = new HashSet(keyImages); containedKeyImages.retainAll(this.keyImages); this.keyImages.removeAll(containedKeyImages); - for (String lastKeyImage : new HashSet<>(lastStatuses.keySet())) lastStatuses.remove(lastKeyImage); + synchronized (lastStatuses) { + for (String lastKeyImage : new HashSet<>(lastStatuses.keySet())) lastStatuses.remove(lastKeyImage); + } refreshPolling(); } } @@ -202,39 +204,43 @@ public class MoneroKeyImagePoller { * @return true if the key is spent, false if unspent, null if unknown */ public Boolean isSpent(String keyImage) { - if (!lastStatuses.containsKey(keyImage)) return null; - return lastStatuses.get(keyImage) != MoneroKeyImageSpentStatus.NOT_SPENT; + synchronized (lastStatuses) { + if (!lastStatuses.containsKey(keyImage)) return null; + return lastStatuses.get(keyImage) != MoneroKeyImageSpentStatus.NOT_SPENT; + } } public void poll() { - synchronized (keyImages) { - if (daemon == null) { - log.warn("Cannot poll key images because daemon is null"); - return; - } - try { + if (daemon == null) { + log.warn("Cannot poll key images because daemon is null"); + return; + } + try { - // fetch spent statuses - List spentStatuses = keyImages.isEmpty() ? new ArrayList() : daemon.getKeyImageSpentStatuses(keyImages); + // fetch spent statuses + List spentStatuses = keyImages.isEmpty() ? new ArrayList() : daemon.getKeyImageSpentStatuses(keyImages); - // collect changed statuses - Map changedStatuses = new HashMap(); - for (int i = 0; i < keyImages.size(); i++) { - if (lastStatuses.get(keyImages.get(i)) != spentStatuses.get(i)) { - lastStatuses.put(keyImages.get(i), spentStatuses.get(i)); - changedStatuses.put(keyImages.get(i), spentStatuses.get(i)); + // collect changed statuses + Map changedStatuses = new HashMap(); + synchronized (lastStatuses) { + synchronized (keyImages) { + for (int i = 0; i < keyImages.size(); i++) { + if (lastStatuses.get(keyImages.get(i)) != spentStatuses.get(i)) { + lastStatuses.put(keyImages.get(i), spentStatuses.get(i)); + changedStatuses.put(keyImages.get(i), spentStatuses.get(i)); + } } } - - // announce changes - if (!changedStatuses.isEmpty()) { - for (MoneroKeyImageListener listener : new ArrayList(listeners)) { - listener.onSpentStatusChanged(changedStatuses); - } - } - } catch (Exception e) { - log.warn("Error polling key images: " + e.getMessage()); } + + // announce changes + if (!changedStatuses.isEmpty()) { + for (MoneroKeyImageListener listener : new ArrayList(listeners)) { + listener.onSpentStatusChanged(changedStatuses); + } + } + } catch (Exception e) { + log.warn("Error polling key images: " + e.getMessage()); } } @@ -245,7 +251,7 @@ public class MoneroKeyImagePoller { private synchronized void setIsPolling(boolean enabled) { if (enabled) { if (!isPolling) { - isPolling = true; // TODO monero-java: looper.isPolling() + isPolling = true; // TODO: use looper.isStarted(), synchronize looper.start(refreshPeriodMs); } } else { 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 6468e120..af71bfd6 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -300,11 +300,14 @@ public class XmrWalletService { * @return a transaction to reserve a trade */ public MoneroTxWallet createReserveTx(BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String returnAddress) { - log.info("Creating reserve tx with fee={}, sendAmount={}, securityDeposit={}", tradeFee, sendAmount, securityDeposit); - return createTradeTx(tradeFee, sendAmount, securityDeposit, returnAddress, true); + log.info("Creating reserve tx with return address={}", returnAddress); + long time = System.currentTimeMillis(); + MoneroTxWallet reserveTx = createTradeTx(tradeFee, sendAmount, securityDeposit, returnAddress, true); + log.info("Done creating reserve tx in {} ms", System.currentTimeMillis() - time); + return reserveTx; } - /** + /**s * Create the multisig deposit tx and freeze its inputs. * * @param trade the trade to create a deposit tx from @@ -326,8 +329,11 @@ public class XmrWalletService { thawOutputs(trade.getSelf().getReserveTxKeyImages()); } - log.info("Creating deposit tx with fee={}, sendAmount={}, securityDeposit={}", tradeFee, sendAmount, securityDeposit); - return createTradeTx(tradeFee, sendAmount, securityDeposit, multisigAddress, false); + log.info("Creating deposit tx for trade {} {} with multisig address={}", trade.getClass().getSimpleName(), trade.getId(), multisigAddress); + long time = System.currentTimeMillis(); + MoneroTxWallet tradeTx = createTradeTx(tradeFee, sendAmount, securityDeposit, multisigAddress, false); + log.info("Done creating deposit tx for trade {} {} in {} ms", trade.getClass().getSimpleName(), trade.getId(), System.currentTimeMillis() - time); + return tradeTx; } } @@ -378,7 +384,7 @@ public class XmrWalletService { * @param keyImages expected key images of inputs, ignored if null * @return tuple with the verified tx and its actual security deposit */ - public Tuple2 verifyTradeTx(BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String address, String txHash, String txHex, String txKey, List keyImages, boolean isReserveTx) { + public Tuple2 verifyTradeTx(String offerId, BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String address, String txHash, String txHex, String txKey, List keyImages, boolean isReserveTx) { MoneroDaemonRpc daemon = getDaemon(); MoneroWallet wallet = getWallet(); MoneroTx tx = null; @@ -393,7 +399,10 @@ public class XmrWalletService { // submit tx to pool MoneroSubmitTxResult result = daemon.submitTxHex(txHex, true); // TODO (woodser): invert doNotRelay flag to relay for library consistency? if (!result.isGood()) throw new RuntimeException("Failed to submit tx to daemon: " + JsonUtils.serialize(result)); - tx = getTx(txHash); + + // get pool tx which has weight and size + for (MoneroTx poolTx : daemon.getTxPool()) if (poolTx.getHash().equals(txHash)) tx = poolTx; + if (tx == null) throw new RuntimeException("Tx is not in pool after being submitted"); // verify key images if (keyImages != null) { @@ -426,7 +435,9 @@ public class XmrWalletService { BigInteger actualSendAmount = returnCheck.getReceivedAmount().subtract(isReserveTx ? actualTradeFee : actualSecurityDeposit); // verify trade fee - if (!tradeFee.equals(actualTradeFee)) throw new RuntimeException("Trade fee is incorrect amount, expected " + tradeFee + " but was " + actualTradeFee); + if (!tradeFee.equals(actualTradeFee)) { + throw new RuntimeException("Trade fee is incorrect amount, expected=" + tradeFee + ", actual=" + actualTradeFee + ", return address check=" + JsonUtils.serialize(returnCheck) + ", fee address check=" + JsonUtils.serialize(feeCheck)); + } // verify sufficient security deposit BigInteger minSecurityDeposit = new BigDecimal(securityDeposit).multiply(new BigDecimal(1.0 - SECURITY_DEPOSIT_TOLERANCE)).toBigInteger(); @@ -436,6 +447,9 @@ public class XmrWalletService { BigInteger minDepositAndFee = sendAmount.add(securityDeposit).subtract(new BigDecimal(tx.getFee()).multiply(new BigDecimal(1.0 - DUST_TOLERANCE)).toBigInteger()); BigInteger actualDepositAndFee = actualSendAmount.add(actualSecurityDeposit).add(tx.getFee()); if (actualDepositAndFee.compareTo(minDepositAndFee) < 0) throw new RuntimeException("Deposit amount + fee is not enough, needed " + minDepositAndFee + " but was " + actualDepositAndFee); + } catch (Exception e) { + log.warn("Error verifying trade tx with offer id=" + offerId + (tx == null ? "" : ", tx=" + tx) + ": " + e.getMessage()); + throw e; } finally { try { daemon.flushTxPool(txHash); // flush tx from pool @@ -524,7 +538,7 @@ public class XmrWalletService { wallet = null; walletListeners.clear(); } catch (Exception e) { - log.warn("Error closing monero-wallet-rpc subprocess. Was Haveno stopped manually with ctrl+c?"); + log.warn("Error closing main monero-wallet-rpc subprocess. Was Haveno stopped manually with ctrl+c?"); } } @@ -546,10 +560,10 @@ public class XmrWalletService { private void maybeInitMainWallet() { if (wallet != null) throw new RuntimeException("Main wallet is already initialized"); - MoneroDaemonRpc daemon = connectionsService.getDaemon(); - log.info("Initializing main wallet with " + (daemon == null ? "daemon: null" : "monerod uri=" + daemon.getRpcConnection().getUri() + ", height=" + connectionsService.getLastInfo().getHeight())); // open or create wallet + MoneroDaemonRpc daemon = connectionsService.getDaemon(); + log.info("Initializing main wallet with " + (daemon == null ? "daemon: null" : "monerod uri=" + daemon.getRpcConnection().getUri())); MoneroWalletConfig walletConfig = new MoneroWalletConfig().setPath(MONERO_WALLET_NAME).setPassword(getWalletPassword()); if (MoneroUtils.walletExists(xmrWalletFile.getPath())) { wallet = openWalletRpc(walletConfig, rpcBindPort); @@ -593,7 +607,6 @@ public class XmrWalletService { // must be connected to daemon MoneroRpcConnection connection = connectionsService.getConnection(); if (connection == null || !Boolean.TRUE.equals(connection.isConnected())) throw new RuntimeException("Must be connected to daemon before creating wallet"); - config.setServer(connection); // start monero-wallet-rpc instance MoneroWalletRpc walletRpc = startWalletRpcInstance(port); @@ -607,7 +620,7 @@ public class XmrWalletService { // create wallet log.info("Creating wallet " + config.getPath() + " connected to daemon " + connection.getUri()); long time = System.currentTimeMillis(); - walletRpc.createWallet(config); + walletRpc.createWallet(config.setServer(connection)); log.info("Done creating wallet " + config.getPath() + " in " + (System.currentTimeMillis() - time) + " ms"); return walletRpc; } catch (Exception e) { @@ -689,7 +702,13 @@ public class XmrWalletService { wallet.setDaemonConnection(connection); if (connection != null && !Boolean.FALSE.equals(connection.isConnected())) { wallet.startSyncing(connectionsService.getDefaultRefreshPeriodMs()); - new Thread(() -> wallet.sync()).start(); + new Thread(() -> { + try { + wallet.sync(); + } catch (Exception e) { + log.warn("Failed to sync main wallet after setting daemon connection: " + e.getMessage()); + } + }).start(); } } } @@ -738,59 +757,57 @@ public class XmrWalletService { // ----------------------------- LEGACY APP ------------------------------- - public XmrAddressEntry getNewAddressEntry() { - return getOrCreateAddressEntry(XmrAddressEntry.Context.AVAILABLE, Optional.empty()); + public synchronized XmrAddressEntry getNewAddressEntry() { + return getNewAddressEntry(XmrAddressEntry.Context.AVAILABLE); } - public XmrAddressEntry getFreshAddressEntry() { - List unusedAddressEntries = getUnusedAddressEntries(); - if (unusedAddressEntries.isEmpty()) return getNewAddressEntry(); - else return unusedAddressEntries.get(0); - } + public synchronized XmrAddressEntry getNewAddressEntry(String offerId, XmrAddressEntry.Context context) { - public 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 XmrAddressEntry getNewAddressEntry(String offerId, XmrAddressEntry.Context context) { + // try to use available and not yet used entries + List incomingTxs = getIncomingTxs(null); // pre-fetch all incoming txs to avoid query per subaddress + Optional emptyAvailableAddressEntry = getAddressEntryListAsImmutableList().stream().filter(e -> XmrAddressEntry.Context.AVAILABLE == e.getContext()).filter(e -> isSubaddressUnused(e.getSubaddressIndex(), incomingTxs)).findAny(); + if (emptyAvailableAddressEntry.isPresent()) return xmrAddressEntryList.swapAvailableToAddressEntryWithOfferId(emptyAvailableAddressEntry.get(), context, offerId); + + // create new subaddress and entry MoneroSubaddress subaddress = wallet.createSubaddress(0); XmrAddressEntry entry = new XmrAddressEntry(subaddress.getIndex(), subaddress.getAddress(), context, offerId, null); xmrAddressEntryList.addAddressEntry(entry); return entry; } - public 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 { - // We try to use available and not yet used entries - List incomingTxs = getIncomingTxs(null); // pre-fetch all incoming txs to avoid query per subaddress - Optional emptyAvailableAddressEntry = getAddressEntryListAsImmutableList().stream().filter(e -> XmrAddressEntry.Context.AVAILABLE == e.getContext()) - .filter(e -> isSubaddressUnused(e.getSubaddressIndex(), incomingTxs)).findAny(); - if (emptyAvailableAddressEntry.isPresent()) { - return xmrAddressEntryList.swapAvailableToAddressEntryWithOfferId(emptyAvailableAddressEntry.get(), context, offerId); - } else { - return getNewAddressEntry(offerId, context); - } - } + public synchronized XmrAddressEntry getFreshAddressEntry() { + List unusedAddressEntries = getUnusedAddressEntries(); + if (unusedAddressEntries.isEmpty()) return getNewAddressEntry(); + else return unusedAddressEntries.get(0); } - public XmrAddressEntry getArbitratorAddressEntry() { + 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 getOrCreateAddressEntry(context, addressEntry); + return addressEntry.isPresent() ? addressEntry.get() : getNewAddressEntry(context); } - public Optional getAddressEntry(String offerId, XmrAddressEntry.Context context) { - return getAddressEntryListAsImmutableList().stream().filter(e -> offerId.equals(e.getOfferId())).filter(e -> context == e.getContext()).findAny(); + 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 void swapTradeEntryToAvailableEntry(String offerId, XmrAddressEntry.Context context) { + public synchronized void swapTradeEntryToAvailableEntry(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); @@ -799,13 +816,14 @@ public class XmrWalletService { }); } - public void resetAddressEntriesForOpenOffer(String offerId) { + public synchronized void resetAddressEntriesForOpenOffer(String offerId) { log.info("resetAddressEntriesForOpenOffer offerId={}", offerId); swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.OFFER_FUNDING); swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.RESERVED_FOR_TRADE); + swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.TRADE_PAYOUT); } - public void resetAddressEntriesForPendingTrade(String offerId) { + public synchronized void resetAddressEntriesForPendingTrade(String offerId) { swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.MULTI_SIG); // We swap also TRADE_PAYOUT to be sure all is cleaned up. There might be cases // where a user cannot send the funds @@ -821,17 +839,12 @@ public class XmrWalletService { swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.TRADE_PAYOUT); } - private XmrAddressEntry getOrCreateAddressEntry(XmrAddressEntry.Context context, - Optional addressEntry) { - if (addressEntry.isPresent()) { - return addressEntry.get(); - } else { - MoneroSubaddress subaddress = wallet.createSubaddress(0); - XmrAddressEntry entry = new XmrAddressEntry(subaddress.getIndex(), subaddress.getAddress(), context, null, null); - log.info("getOrCreateAddressEntry: add new XmrAddressEntry {}", entry); - xmrAddressEntryList.addAddressEntry(entry); - return entry; - } + private XmrAddressEntry getNewAddressEntry(XmrAddressEntry.Context context) { + MoneroSubaddress subaddress = wallet.createSubaddress(0); + XmrAddressEntry entry = new XmrAddressEntry(subaddress.getIndex(), subaddress.getAddress(), context, null, null); + log.info("getOrCreateAddressEntry: add new XmrAddressEntry {}", entry); + xmrAddressEntryList.addAddressEntry(entry); + return entry; } private Optional findAddressEntry(String address, XmrAddressEntry.Context context) { diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcDisputesService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcDisputesService.java index 06c21816..dc0e9512 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcDisputesService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcDisputesService.java @@ -139,7 +139,7 @@ public class GrpcDisputesService extends DisputesImplBase { new HashMap<>() {{ put(getGetDisputeMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS)); put(getGetDisputesMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); - put(getResolveDisputeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); + put(getResolveDisputeMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS)); put(getOpenDisputeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); put(getSendDisputeChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS)); }} diff --git a/desktop/src/main/java/haveno/desktop/components/TxIdTextField.java b/desktop/src/main/java/haveno/desktop/components/TxIdTextField.java index d480e759..360b0f5a 100644 --- a/desktop/src/main/java/haveno/desktop/components/TxIdTextField.java +++ b/desktop/src/main/java/haveno/desktop/components/TxIdTextField.java @@ -20,6 +20,7 @@ package haveno.desktop.components; import com.jfoenix.controls.JFXTextField; import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; +import haveno.common.UserThread; import haveno.common.util.Utilities; import haveno.core.locale.Res; import haveno.core.user.BlockChainExplorer; @@ -135,12 +136,14 @@ public class TxIdTextField extends AnchorPane { }; xmrWalletService.addWalletListener(txUpdater); - updateConfidence(txId, true, null); - textField.setText(txId); textField.setOnMouseClicked(mouseEvent -> openBlockExplorer(txId)); blockExplorerIcon.setOnMouseClicked(mouseEvent -> openBlockExplorer(txId)); copyIcon.setOnMouseClicked(e -> Utilities.copyToClipboard(txId)); + txConfidenceIndicator.setVisible(true); + + // update off main thread + new Thread(() -> updateConfidence(txId, true, null)).start(); } public void cleanup() { @@ -165,7 +168,7 @@ public class TxIdTextField extends AnchorPane { } } - private void updateConfidence(String txId, boolean useCache, Long height) { + private synchronized void updateConfidence(String txId, boolean useCache, Long height) { MoneroTx tx = null; try { tx = useCache ? xmrWalletService.getTxWithCache(txId) : xmrWalletService.getTx(txId); @@ -173,14 +176,19 @@ public class TxIdTextField extends AnchorPane { } catch (Exception e) { // do nothing } - GUIUtil.updateConfidence(tx, progressIndicatorTooltip, txConfidenceIndicator); - if (txConfidenceIndicator.getProgress() != 0) { - txConfidenceIndicator.setVisible(true); - AnchorPane.setRightAnchor(txConfidenceIndicator, 0.0); - } - if (txConfidenceIndicator.getProgress() >= 1.0 && txUpdater != null) { - xmrWalletService.removeWalletListener(txUpdater); // unregister listener - txUpdater = null; - } + updateConfidence(tx); + } + + private void updateConfidence(MoneroTx tx) { + UserThread.execute(() -> { + GUIUtil.updateConfidence(tx, progressIndicatorTooltip, txConfidenceIndicator); + if (txConfidenceIndicator.getProgress() != 0) { + AnchorPane.setRightAnchor(txConfidenceIndicator, 0.0); + } + if (txConfidenceIndicator.getProgress() >= 1.0 && txUpdater != null) { + xmrWalletService.removeWalletListener(txUpdater); // unregister listener + txUpdater = null; + } + }); } } diff --git a/desktop/src/main/java/haveno/desktop/components/indicator/TxConfidenceIndicator.java b/desktop/src/main/java/haveno/desktop/components/indicator/TxConfidenceIndicator.java index 00c0947b..d0281735 100644 --- a/desktop/src/main/java/haveno/desktop/components/indicator/TxConfidenceIndicator.java +++ b/desktop/src/main/java/haveno/desktop/components/indicator/TxConfidenceIndicator.java @@ -42,6 +42,7 @@ package haveno.desktop.components.indicator; +import haveno.common.UserThread; import haveno.desktop.components.indicator.skin.StaticProgressIndicatorSkin; import javafx.beans.property.DoubleProperty; import javafx.beans.property.DoublePropertyBase; @@ -220,7 +221,7 @@ public class TxConfidenceIndicator extends Control { */ public final void setProgress(double value) { - progressProperty().set(value); + UserThread.execute(() -> progressProperty().set(value)); } public final DoubleProperty progressProperty() { diff --git a/desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartView.java b/desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartView.java index 9bc1aefc..134fddf5 100644 --- a/desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartView.java +++ b/desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartView.java @@ -373,44 +373,52 @@ public class OfferBookChartView extends ActivatableViewAndModel minMaxFilterLeft(List> data) { - double maxValue = data.stream() - .mapToDouble(o -> o.getXValue().doubleValue()) - .max() - .orElse(Double.MIN_VALUE); - // Hide offers less than a div-factor of dataLimitFactor lower than the highest offer. - double minValue = data.stream() - .mapToDouble(o -> o.getXValue().doubleValue()) - .filter(o -> o > maxValue / dataLimitFactor) - .min() - .orElse(Double.MAX_VALUE); - return List.of(minValue, maxValue); + synchronized (data) { + double maxValue = data.stream() + .mapToDouble(o -> o.getXValue().doubleValue()) + .max() + .orElse(Double.MIN_VALUE); + // Hide offers less than a div-factor of dataLimitFactor lower than the highest offer. + double minValue = data.stream() + .mapToDouble(o -> o.getXValue().doubleValue()) + .filter(o -> o > maxValue / dataLimitFactor) + .min() + .orElse(Double.MAX_VALUE); + return List.of(minValue, maxValue); + } } private List minMaxFilterRight(List> data) { - double minValue = data.stream() - .mapToDouble(o -> o.getXValue().doubleValue()) - .min() - .orElse(Double.MAX_VALUE); + synchronized (data) { + double minValue = data.stream() + .mapToDouble(o -> o.getXValue().doubleValue()) + .min() + .orElse(Double.MAX_VALUE); - // Hide offers a dataLimitFactor factor higher than the lowest offer - double maxValue = data.stream() - .mapToDouble(o -> o.getXValue().doubleValue()) - .filter(o -> o < minValue * dataLimitFactor) - .max() - .orElse(Double.MIN_VALUE); - return List.of(minValue, maxValue); + // Hide offers a dataLimitFactor factor higher than the lowest offer + double maxValue = data.stream() + .mapToDouble(o -> o.getXValue().doubleValue()) + .filter(o -> o < minValue * dataLimitFactor) + .max() + .orElse(Double.MIN_VALUE); + return List.of(minValue, maxValue); + } } private List> filterLeft(List> data, double maxValue) { - return data.stream() - .filter(o -> o.getXValue().doubleValue() > maxValue / dataLimitFactor) - .collect(Collectors.toList()); + synchronized (data) { + return data.stream() + .filter(o -> o.getXValue().doubleValue() > maxValue / dataLimitFactor) + .collect(Collectors.toList()); + } } private List> filterRight(List> data, double minValue) { - return data.stream() - .filter(o -> o.getXValue().doubleValue() < minValue * dataLimitFactor) - .collect(Collectors.toList()); + synchronized (data) { + return data.stream() + .filter(o -> o.getXValue().doubleValue() < minValue * dataLimitFactor) + .collect(Collectors.toList()); + } } private Tuple4, VBox, Button, Label> getOfferTable(OfferDirection direction) { diff --git a/desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartViewModel.java b/desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartViewModel.java index 34bb8f82..bec4cdac 100644 --- a/desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/market/offerbook/OfferBookChartViewModel.java @@ -389,24 +389,26 @@ class OfferBookChartViewModel extends ActivatableViewModel { OfferDirection direction, List> data, ObservableList offerTableList) { - data.clear(); - double accumulatedAmount = 0; - List offerTableListTemp = new ArrayList<>(); - for (Offer offer : sortedList) { - Price price = offer.getPrice(); - if (price != null) { - double amount = (double) offer.getAmount().longValueExact() / LongMath.pow(10, HavenoUtils.XMR_SMALLEST_UNIT_EXPONENT); - accumulatedAmount += amount; - offerTableListTemp.add(new OfferListItem(offer, accumulatedAmount)); - - double priceAsDouble = (double) price.getValue() / LongMath.pow(10, price.smallestUnitExponent()); - if (direction.equals(OfferDirection.BUY)) - data.add(0, new XYChart.Data<>(priceAsDouble, accumulatedAmount)); - else - data.add(new XYChart.Data<>(priceAsDouble, accumulatedAmount)); + synchronized (data) { + data.clear(); + double accumulatedAmount = 0; + List offerTableListTemp = new ArrayList<>(); + for (Offer offer : sortedList) { + Price price = offer.getPrice(); + if (price != null) { + double amount = (double) offer.getAmount().longValueExact() / LongMath.pow(10, HavenoUtils.XMR_SMALLEST_UNIT_EXPONENT); + accumulatedAmount += amount; + offerTableListTemp.add(new OfferListItem(offer, accumulatedAmount)); + + double priceAsDouble = (double) price.getValue() / LongMath.pow(10, price.smallestUnitExponent()); + if (direction.equals(OfferDirection.BUY)) + data.add(0, new XYChart.Data<>(priceAsDouble, accumulatedAmount)); + else + data.add(new XYChart.Data<>(priceAsDouble, accumulatedAmount)); + } } + offerTableList.setAll(offerTableListTemp); } - offerTableList.setAll(offerTableListTemp); } private boolean isEditEntry(String id) { diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java index 3ed8c2c3..c692259e 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java @@ -66,6 +66,8 @@ import org.slf4j.LoggerFactory; import java.time.Duration; import java.time.Instant; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import static com.google.common.base.Preconditions.checkNotNull; @@ -111,6 +113,8 @@ public abstract class TradeStepView extends AnchorPane { trade = model.dataModel.getTrade(); checkNotNull(trade, "Trade must not be null at TradeStepView"); + startCachingTxs(); + ScrollPane scrollPane = new ScrollPane(); scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); @@ -166,16 +170,25 @@ public abstract class TradeStepView extends AnchorPane { // }; } + private void startCachingTxs() { + List txIds = new ArrayList(); + if (!model.dataModel.makerTxId.isEmpty().get()) txIds.add(model.dataModel.makerTxId.get()); + if (!model.dataModel.takerTxId.isEmpty().get()) txIds.add(model.dataModel.takerTxId.get()); + new Thread(() -> trade.getXmrWalletService().getTxsWithCache(txIds)).start(); + } + public void activate() { if (selfTxIdTextField != null) { if (selfTxIdSubscription != null) selfTxIdSubscription.unsubscribe(); selfTxIdSubscription = EasyBind.subscribe(model.dataModel.isMaker() ? model.dataModel.makerTxId : model.dataModel.takerTxId, id -> { - if (!id.isEmpty()) + if (!id.isEmpty()) { + startCachingTxs(); selfTxIdTextField.setup(id); - else + } else { selfTxIdTextField.cleanup(); + } }); } if (peerTxIdTextField != null) { @@ -183,10 +196,12 @@ public abstract class TradeStepView extends AnchorPane { peerTxIdSubscription.unsubscribe(); peerTxIdSubscription = EasyBind.subscribe(model.dataModel.isMaker() ? model.dataModel.takerTxId : model.dataModel.makerTxId, id -> { - if (!id.isEmpty()) + if (!id.isEmpty()) { + startCachingTxs(); peerTxIdTextField.setup(id); - else + } else { peerTxIdTextField.cleanup(); + } }); } trade.errorMessageProperty().addListener(errorMessageListener); 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 298974c6..0a300227 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 @@ -223,7 +223,7 @@ public class BuyerStep2View extends TradeStepView { addTradeInfoBlock(); PaymentAccountPayload paymentAccountPayload = model.dataModel.getSellersPaymentAccountPayload(); - String paymentMethodId = paymentAccountPayload != null ? paymentAccountPayload.getPaymentMethodId() : ""; + String paymentMethodId = paymentAccountPayload != null ? paymentAccountPayload.getPaymentMethodId() : ""; TitledGroupBg accountTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 4, Res.get("portfolio.pending.step2_buyer.startPaymentUsing", Res.get(paymentMethodId)), Layout.COMPACT_GROUP_DISTANCE); diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 5d764216..848df095 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -1477,28 +1477,29 @@ message Trade { string payout_tx_key = 5; int64 amount = 6; int64 taker_fee = 8; - int64 take_offer_date = 9; - int64 price = 10; - State state = 11; - PayoutState payout_state = 12; - DisputeState dispute_state = 13; - TradePeriodState period_state = 14; - Contract contract = 15; - string contract_as_json = 16; - bytes contract_hash = 17; - NodeAddress arbitrator_node_address = 18; - NodeAddress mediator_node_address = 19; - string error_message = 20; - string counter_currency_tx_id = 21; - repeated ChatMessage chat_message = 22; - MediationResultState mediation_result_state = 23; - int64 lock_time = 24; - int64 start_time = 25; - NodeAddress refund_agent_node_address = 26; - RefundResultState refund_result_state = 27; - string counter_currency_extra_data = 28; - string asset_tx_proof_result = 29; // name of AssetTxProofResult enum - string uid = 30; + int64 total_tx_fee = 9; + int64 take_offer_date = 10; + int64 price = 11; + State state = 12; + PayoutState payout_state = 13; + DisputeState dispute_state = 14; + TradePeriodState period_state = 15; + Contract contract = 16; + string contract_as_json = 17; + bytes contract_hash = 18; + NodeAddress arbitrator_node_address = 19; + NodeAddress mediator_node_address = 20; + string error_message = 21; + string counter_currency_tx_id = 22; + repeated ChatMessage chat_message = 23; + MediationResultState mediation_result_state = 24; + int64 lock_time = 25; + int64 start_time = 26; + NodeAddress refund_agent_node_address = 27; + RefundResultState refund_result_state = 28; + string counter_currency_extra_data = 29; + string asset_tx_proof_result = 30; // name of AssetTxProofResult enum + string uid = 31; } message BuyerAsMakerTrade {