diff --git a/core/src/main/java/haveno/core/api/CoreMoneroConnectionsService.java b/core/src/main/java/haveno/core/api/CoreMoneroConnectionsService.java index c1f05f3e83..e6750f539f 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 e8144b0555..de4b7ff258 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 cebbc39d02..cb1aaeb000 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 e90da7b2b1..38a1b9e6dc 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 03beba2ac6..247ca7635c 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 6306d51182..94d18309d9 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 a1dba0beca..c4065f6507 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 4562d4243a..408b6aefb6 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 5e3c51219d..f93a5efa60 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 ef11219571..c978c7c40a 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 9f33e9054f..4eab1740dc 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 f8834fe7e3..42bb11b23a 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 12e1df3bd1..860a7f0af1 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 e5ee75845e..fae55c45d5 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 1983b15eb0..7e685950a2 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 81be5fb2c0..f34ee969bc 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 550f3a4c37..add97ee29d 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 b4dae07d42..580424ab20 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 a78182b940..0d9261e230 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 46038c4aaf..f77977859b 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 7ab0a0d655..1796b7d12b 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 858944287a..c59bd919f3 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 88106baa16..872ff293ef 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 6468e12042..af71bfd678 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 06c2181600..dc0e951242 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 d480e75920..360b0f5af1 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 00c0947b72..d02817353e 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 9bc1aefccc..134fddf50c 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 34bb8f82fa..bec4cdac94 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 3ed8c2c3f6..c692259eb8 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 298974c6b9..0a30022738 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 5d764216cc..848df095db 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 {