From e63141279c88b9cdd04aee66e9620ab6a10fd17f Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 29 Apr 2024 07:02:05 -0400 Subject: [PATCH] refactoring based on congestion testing retry creating and processing trade txs on failure do not use connection manager polling to reduce requests use global daemon lock for wallet sync operations sync wallets on poll if behind use local util to get payment uri to avoid blocking all peers share multisig hex on deposits confirmed import multisig hex when needed --- .../main/java/haveno/core/api/CoreApi.java | 20 +- .../haveno/core/api/XmrConnectionService.java | 98 +++-- .../haveno/core/offer/OfferBookService.java | 4 +- .../haveno/core/offer/OpenOfferManager.java | 43 +- .../offer/placeoffer/PlaceOfferProtocol.java | 2 +- .../tasks/MakerReserveOfferFunds.java | 58 ++- .../core/support/dispute/DisputeManager.java | 10 +- .../arbitration/ArbitrationManager.java | 23 +- .../java/haveno/core/trade/HavenoUtils.java | 17 + .../main/java/haveno/core/trade/Trade.java | 379 +++++++++++------- .../java/haveno/core/trade/TradeManager.java | 58 ++- .../trade/protocol/ArbitratorProtocol.java | 6 +- .../trade/protocol/BuyerAsMakerProtocol.java | 4 +- .../trade/protocol/BuyerAsTakerProtocol.java | 4 +- .../core/trade/protocol/BuyerProtocol.java | 3 +- .../core/trade/protocol/ProcessModel.java | 1 + .../trade/protocol/SellerAsMakerProtocol.java | 4 +- .../trade/protocol/SellerAsTakerProtocol.java | 4 +- .../core/trade/protocol/TradeProtocol.java | 28 +- .../tasks/BuyerPreparePaymentSentMessage.java | 4 +- .../tasks/MaybeSendSignContractRequest.java | 98 +++-- .../ProcessDepositsConfirmedMessage.java | 22 +- .../tasks/ProcessPaymentReceivedMessage.java | 3 +- .../tasks/ProcessPaymentSentMessage.java | 9 - .../SellerPreparePaymentReceivedMessage.java | 2 +- ...tResponse.java => SendDepositRequest.java} | 12 +- .../tasks/TakerReserveTradeFunds.java | 62 ++- .../core/xmr/wallet/XmrWalletService.java | 287 ++++++------- .../daemon/grpc/GrpcXmrConnectionService.java | 50 +-- .../main/funds/deposit/DepositView.java | 3 +- .../main/funds/withdrawal/WithdrawalView.java | 2 +- .../main/offer/MutableOfferViewModel.java | 8 +- .../pendingtrades/PendingTradesViewModel.java | 22 + .../steps/buyer/BuyerStep2View.java | 2 +- .../java/haveno/desktop/util/GUIUtil.java | 3 +- proto/src/main/proto/grpc.proto | 12 +- 36 files changed, 799 insertions(+), 568 deletions(-) rename core/src/main/java/haveno/core/trade/protocol/tasks/{ProcessSignContractResponse.java => SendDepositRequest.java} (88%) diff --git a/core/src/main/java/haveno/core/api/CoreApi.java b/core/src/main/java/haveno/core/api/CoreApi.java index 433ce1d5..b4a293a3 100644 --- a/core/src/main/java/haveno/core/api/CoreApi.java +++ b/core/src/main/java/haveno/core/api/CoreApi.java @@ -199,15 +199,15 @@ public class CoreApi { // Monero Connections /////////////////////////////////////////////////////////////////////////////////////////// - public void addMoneroConnection(MoneroRpcConnection connection) { + public void addXmrConnection(MoneroRpcConnection connection) { xmrConnectionService.addConnection(connection); } - public void removeMoneroConnection(String connectionUri) { + public void removeXmrConnection(String connectionUri) { xmrConnectionService.removeConnection(connectionUri); } - public MoneroRpcConnection getMoneroConnection() { + public MoneroRpcConnection getXmrConnection() { return xmrConnectionService.getConnection(); } @@ -215,15 +215,15 @@ public class CoreApi { return xmrConnectionService.getConnections(); } - public void setMoneroConnection(String connectionUri) { + public void setXmrConnection(String connectionUri) { xmrConnectionService.setConnection(connectionUri); } - public void setMoneroConnection(MoneroRpcConnection connection) { + public void setXmrConnection(MoneroRpcConnection connection) { xmrConnectionService.setConnection(connection); } - public MoneroRpcConnection checkMoneroConnection() { + public MoneroRpcConnection checkXmrConnection() { return xmrConnectionService.checkConnection(); } @@ -231,19 +231,19 @@ public class CoreApi { return xmrConnectionService.checkConnections(); } - public void startCheckingMoneroConnection(Long refreshPeriod) { + public void startCheckingXmrConnection(Long refreshPeriod) { xmrConnectionService.startCheckingConnection(refreshPeriod); } - public void stopCheckingMoneroConnection() { + public void stopCheckingXmrConnection() { xmrConnectionService.stopCheckingConnection(); } - public MoneroRpcConnection getBestAvailableMoneroConnection() { + public MoneroRpcConnection getBestAvailableXmrConnection() { return xmrConnectionService.getBestAvailableConnection(); } - public void setMoneroConnectionAutoSwitch(boolean autoSwitch) { + public void setXmrConnectionAutoSwitch(boolean autoSwitch) { xmrConnectionService.setAutoSwitch(autoSwitch); } diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index 0a4afdb2..7fa02bda 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -90,6 +90,7 @@ public final class XmrConnectionService { private boolean isInitialized; private boolean pollInProgress; private MoneroDaemonRpc daemon; + private Boolean isConnected = false; @Getter private MoneroDaemonInfo lastInfo; private Long syncStartHeight = null; @@ -148,7 +149,6 @@ public final class XmrConnectionService { isInitialized = false; synchronized (lock) { if (daemonPollLooper != null) daemonPollLooper.stop(); - connectionManager.stopPolling(); daemon = null; } } @@ -171,7 +171,7 @@ public final class XmrConnectionService { } public Boolean isConnected() { - return connectionManager.isConnected(); + return isConnected; } public void addConnection(MoneroRpcConnection connection) { @@ -196,6 +196,12 @@ public final class XmrConnectionService { return connectionManager.getConnections(); } + public void switchToBestConnection() { + if (isFixedConnection() || !connectionManager.getAutoSwitch()) return; + MoneroRpcConnection bestConnection = getBestAvailableConnection(); + if (bestConnection != null) setConnection(bestConnection); + } + public void setConnection(String connectionUri) { accountService.checkAccountOpen(); connectionManager.setConnection(connectionUri); // listener will update connection list @@ -226,8 +232,8 @@ public final class XmrConnectionService { public void stopCheckingConnection() { accountService.checkAccountOpen(); - connectionManager.stopPolling(); connectionList.setRefreshPeriod(-1L); + updatePolling(); } public MoneroRpcConnection getBestAvailableConnection() { @@ -472,8 +478,6 @@ public final class XmrConnectionService { if (!isFixedConnection() && (connectionManager.getConnection() == null || connectionManager.getAutoSwitch())) { MoneroRpcConnection bestConnection = getBestAvailableConnection(); if (bestConnection != null) setConnection(bestConnection); - } else { - checkConnection(); } } else if (!isInitialized) { @@ -485,19 +489,11 @@ public final class XmrConnectionService { // start local node if applicable maybeStartLocalNode(); - - // update connection - checkConnection(); } // register connection listener connectionManager.addListener(this::onConnectionChanged); - // start polling after delay - UserThread.runAfter(() -> { - if (!isShutDownStarted) connectionManager.startPolling(getRefreshPeriodMs() * 2); - }, getDefaultRefreshPeriodMs() * 2 / 1000); - isInitialized = true; } @@ -524,7 +520,6 @@ public final class XmrConnectionService { private void onConnectionChanged(MoneroRpcConnection currentConnection) { if (isShutDownStarted) return; - log.info("XmrConnectionService.onConnectionChanged() uri={}, connected={}", currentConnection == null ? null : currentConnection.getUri(), currentConnection == null ? "false" : currentConnection.isConnected()); if (currentConnection == null) { log.warn("Setting daemon connection to null"); Thread.dumpStack(); @@ -532,9 +527,11 @@ public final class XmrConnectionService { synchronized (lock) { if (currentConnection == null) { daemon = null; + isConnected = false; connectionList.setCurrentConnectionUri(null); } else { daemon = new MoneroDaemonRpc(currentConnection); + isConnected = currentConnection.isConnected(); connectionList.removeConnection(currentConnection.getUri()); connectionList.addConnection(currentConnection); connectionList.setCurrentConnectionUri(currentConnection.getUri()); @@ -546,9 +543,13 @@ public final class XmrConnectionService { numUpdates.set(numUpdates.get() + 1); }); } - updatePolling(); + + // update polling + doPollDaemon(); + UserThread.runAfter(() -> updatePolling(), getRefreshPeriodMs() / 1000); // notify listeners in parallel + log.info("XmrConnectionService.onConnectionChanged() uri={}, connected={}", currentConnection == null ? null : currentConnection.getUri(), currentConnection == null ? "false" : isConnected); synchronized (listenerLock) { for (MoneroConnectionManagerListener listener : listeners) { ThreadUtils.submitToPool(() -> listener.onConnectionChanged(currentConnection)); @@ -557,18 +558,14 @@ public final class XmrConnectionService { } private void updatePolling() { - new Thread(() -> { - synchronized (lock) { - stopPolling(); - if (connectionList.getRefreshPeriod() >= 0) startPolling(); // 0 means default refresh poll - } - }).start(); + stopPolling(); + if (connectionList.getRefreshPeriod() >= 0) startPolling(); // 0 means default refresh poll } private void startPolling() { synchronized (lock) { if (daemonPollLooper != null) daemonPollLooper.stop(); - daemonPollLooper = new TaskLooper(() -> pollDaemonInfo()); + daemonPollLooper = new TaskLooper(() -> pollDaemon()); daemonPollLooper.start(getRefreshPeriodMs()); } } @@ -582,17 +579,34 @@ public final class XmrConnectionService { } } - private void pollDaemonInfo() { + private void pollDaemon() { if (pollInProgress) return; + doPollDaemon(); + } + + private void doPollDaemon() { synchronized (pollLock) { pollInProgress = true; if (isShutDownStarted) return; try { // poll daemon - log.debug("Polling daemon info"); - if (daemon == null) throw new RuntimeException("No daemon connection"); - lastInfo = daemon.getInfo(); + if (daemon == null) switchToBestConnection(); + if (daemon == null) throw new RuntimeException("No connection to Monero daemon"); + try { + lastInfo = daemon.getInfo(); + } catch (Exception e) { + try { + log.warn("Failed to fetch daemon info, trying to switch to best connection: " + e.getMessage()); + switchToBestConnection(); + lastInfo = daemon.getInfo(); + } catch (Exception e2) { + throw e2; // caught internally + } + } + + // connected to daemon + isConnected = true; // update properties on user thread UserThread.execute(() -> { @@ -632,19 +646,15 @@ public final class XmrConnectionService { lastErrorTimestamp = null; } - // update and notify connected state - if (!Boolean.TRUE.equals(connectionManager.isConnected())) { - connectionManager.checkConnection(); - } - // clear error message - if (Boolean.TRUE.equals(connectionManager.isConnected()) && HavenoUtils.havenoSetup != null) { - HavenoUtils.havenoSetup.getWalletServiceErrorMsg().set(null); - } + if (HavenoUtils.havenoSetup != null) HavenoUtils.havenoSetup.getWalletServiceErrorMsg().set(null); } catch (Exception e) { - // skip if shut down or connected - if (isShutDownStarted || Boolean.TRUE.equals(isConnected())) return; + // not connected to daemon + isConnected = false; + + // skip if shut down + if (isShutDownStarted) return; // log error message periodically if ((lastErrorTimestamp == null || System.currentTimeMillis() - lastErrorTimestamp > MIN_ERROR_LOG_PERIOD_MS)) { @@ -653,20 +663,8 @@ public final class XmrConnectionService { if (DevEnv.isDevMode()) e.printStackTrace(); } - new Thread(() -> { - if (isShutDownStarted) return; - if (!isFixedConnection() && connectionManager.getAutoSwitch()) { - MoneroRpcConnection bestConnection = getBestAvailableConnection(); - if (bestConnection != null) connectionManager.setConnection(bestConnection); - } else { - connectionManager.checkConnection(); - } - - // set error message - if (!Boolean.TRUE.equals(connectionManager.isConnected()) && HavenoUtils.havenoSetup != null) { - HavenoUtils.havenoSetup.getWalletServiceErrorMsg().set(e.getMessage()); - } - }).start(); + // set error message + if (HavenoUtils.havenoSetup != null) HavenoUtils.havenoSetup.getWalletServiceErrorMsg().set(e.getMessage()); } finally { pollInProgress = false; } diff --git a/core/src/main/java/haveno/core/offer/OfferBookService.java b/core/src/main/java/haveno/core/offer/OfferBookService.java index 5453da86..0bb08a46 100644 --- a/core/src/main/java/haveno/core/offer/OfferBookService.java +++ b/core/src/main/java/haveno/core/offer/OfferBookService.java @@ -36,7 +36,6 @@ package haveno.core.offer; import com.google.inject.Inject; import com.google.inject.name.Named; -import common.utils.GenUtils; import haveno.common.UserThread; import haveno.common.config.Config; import haveno.common.file.JsonFileManager; @@ -46,6 +45,7 @@ import haveno.core.api.XmrConnectionService; import haveno.core.filter.FilterManager; import haveno.core.locale.Res; import haveno.core.provider.price.PriceFeedService; +import haveno.core.trade.HavenoUtils; import haveno.core.util.JsonUtil; import haveno.core.xmr.wallet.XmrKeyImageListener; import haveno.core.xmr.wallet.XmrKeyImagePoller; @@ -287,7 +287,7 @@ public class OfferBookService { // first poll after 20s // TODO: remove? new Thread(() -> { - GenUtils.waitFor(20000); + HavenoUtils.waitFor(20000); keyImagePoller.poll(); }).start(); } diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 83b7f3be..eefe8a78 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -36,7 +36,6 @@ package haveno.core.offer; import static com.google.common.base.Preconditions.checkNotNull; import com.google.inject.Inject; -import common.utils.GenUtils; import haveno.common.ThreadUtils; import haveno.common.Timer; import haveno.common.UserThread; @@ -71,6 +70,7 @@ import haveno.core.trade.ClosedTradableManager; import haveno.core.trade.HavenoUtils; import haveno.core.trade.TradableList; import haveno.core.trade.handlers.TransactionResultHandler; +import haveno.core.trade.protocol.TradeProtocol; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.user.Preferences; import haveno.core.user.User; @@ -278,7 +278,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // first poll in 5s // TODO: remove? new Thread(() -> { - GenUtils.waitFor(5000); + HavenoUtils.waitFor(5000); signedOfferKeyImagePoller.poll(); }).start(); } @@ -349,7 +349,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // For typical number of offers we are tolerant with delay to give enough time to broadcast. // If number of offers is very high we limit to 3 sec. to not delay other shutdown routines. long delayMs = Math.min(3000, size * 200 + 500); - GenUtils.waitFor(delayMs); + HavenoUtils.waitFor(delayMs); }, THREAD_ID); } else { broadcaster.flush(); @@ -705,9 +705,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // remove open offer which thaws its key images private void onCancelled(@NotNull OpenOffer openOffer) { Offer offer = openOffer.getOffer(); - if (offer.getOfferPayload().getReserveTxKeyImages() != null) { - xmrWalletService.thawOutputs(offer.getOfferPayload().getReserveTxKeyImages()); - } + xmrWalletService.thawOutputs(offer.getOfferPayload().getReserveTxKeyImages()); offer.setState(Offer.State.REMOVED); openOffer.setState(OpenOffer.State.CANCELED); removeOpenOffer(openOffer); @@ -1029,6 +1027,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // handle sufficient available balance to split output boolean sufficientAvailableBalance = xmrWalletService.getWallet().getUnlockedBalance(0).compareTo(offerReserveAmount) >= 0; if (sufficientAvailableBalance) { + log.info("Splitting and scheduling outputs for offer {} at subaddress {}", openOffer.getShortId()); splitAndSchedule(openOffer); } else if (openOffer.getScheduledTxHashes() == null) { scheduleWithEarliestTxs(openOffers, openOffer); @@ -1038,16 +1037,28 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe private MoneroTxWallet splitAndSchedule(OpenOffer openOffer) { BigInteger reserveAmount = openOffer.getOffer().getReserveAmount(); xmrWalletService.swapAddressEntryToAvailable(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); // change funding subaddress in case funded with unsuitable output(s) - XmrAddressEntry entry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); - log.info("Creating split output tx to fund offer {} at subaddress {}", openOffer.getId(), entry.getSubaddressIndex()); - long startTime = System.currentTimeMillis(); - MoneroTxWallet splitOutputTx = xmrWalletService.getWallet().createTx(new MoneroTxConfig() - .setAccountIndex(0) - .setAddress(entry.getAddressString()) - .setAmount(reserveAmount) - .setRelay(true) - .setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY)); - log.info("Done creating split output tx to fund offer {} in {} ms", openOffer.getId(), System.currentTimeMillis() - startTime); + MoneroTxWallet splitOutputTx = null; + synchronized (XmrWalletService.WALLET_LOCK) { + XmrAddressEntry entry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); + log.info("Creating split output tx to fund offer {} at subaddress {}", openOffer.getShortId(), entry.getSubaddressIndex()); + long startTime = System.currentTimeMillis(); + for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { + try { + splitOutputTx = xmrWalletService.createTx(new MoneroTxConfig() + .setAccountIndex(0) + .setAddress(entry.getAddressString()) + .setAmount(reserveAmount) + .setRelay(true) + .setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY)); + break; + } catch (Exception e) { + log.warn("Error creating split output tx to fund offer {} at subaddress {}, attempt={}/{}, error={}", openOffer.getShortId(), entry.getSubaddressIndex(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); + if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying + } + } + log.info("Done creating split output tx to fund offer {} in {} ms", openOffer.getId(), System.currentTimeMillis() - startTime); + } // schedule txs openOffer.setSplitOutputTxHash(splitOutputTx.getHash()); diff --git a/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java b/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java index 46ead41e..0fbd7efe 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java @@ -133,7 +133,7 @@ public class PlaceOfferProtocol { stopTimeoutTimer(); timeoutTimer = UserThread.runAfter(() -> { handleError(Res.get("createOffer.timeoutAtPublishing")); - }, TradeProtocol.TRADE_TIMEOUT_SECONDS); + }, TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS); } private void stopTimeoutTimer() { 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 b8f9b7c1..9be0425e 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 @@ -17,12 +17,22 @@ package haveno.core.offer.placeoffer.tasks; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; + import haveno.common.taskrunner.Task; import haveno.common.taskrunner.TaskRunner; import haveno.core.offer.Offer; +import haveno.core.offer.OfferDirection; +import haveno.core.offer.OpenOffer; import haveno.core.offer.placeoffer.PlaceOfferModel; +import haveno.core.trade.HavenoUtils; +import haveno.core.trade.protocol.TradeProtocol; import haveno.core.xmr.model.XmrAddressEntry; +import haveno.core.xmr.wallet.XmrWalletService; import lombok.extern.slf4j.Slf4j; +import monero.daemon.model.MoneroOutput; import monero.wallet.model.MoneroTxWallet; @Slf4j @@ -35,7 +45,8 @@ public class MakerReserveOfferFunds extends Task { @Override protected void run() { - Offer offer = model.getOpenOffer().getOffer(); + OpenOffer openOffer = model.getOpenOffer(); + Offer offer = openOffer.getOffer(); try { runInterceptHook(); @@ -44,16 +55,51 @@ public class MakerReserveOfferFunds extends Task { model.getXmrWalletService().getConnectionService().verifyConnection(); // create reserve tx - MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(model.getOpenOffer()); - model.setReserveTx(reserveTx); + MoneroTxWallet reserveTx = null; + synchronized (XmrWalletService.WALLET_LOCK) { - // 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 relevant info + BigInteger penaltyFee = HavenoUtils.multiply(offer.getAmount(), offer.getPenaltyFeePct()); + BigInteger makerFee = offer.getMaxMakerFee(); + BigInteger sendAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.ZERO : offer.getAmount(); + BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit(); + String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); + XmrAddressEntry fundingEntry = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null); + Integer preferredSubaddressIndex = fundingEntry == null ? null : fundingEntry.getSubaddressIndex(); + + // attempt creating reserve tx + synchronized (HavenoUtils.getWalletFunctionLock()) { + for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { + try { + reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex); + } catch (Exception e) { + log.warn("Error creating reserve tx, attempt={}/{}, offerId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, openOffer.getShortId(), e.getMessage()); + if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying + } + + // 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"); + } + if (reserveTx != null) break; + } + } + + // collect reserved key images + List reservedKeyImages = new ArrayList(); + for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex()); + + // update offer state + openOffer.setReserveTxHash(reserveTx.getHash()); + openOffer.setReserveTxHex(reserveTx.getFullHex()); + openOffer.setReserveTxKey(reserveTx.getKey()); + offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages); } // reset protocol timeout model.getProtocol().startTimeoutTimer(); + model.setReserveTx(reserveTx); complete(); } catch (Throwable t) { offer.setErrorMessage("An error occurred.\n" + diff --git a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java index bc7b8e0d..9b7e10d5 100644 --- a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java @@ -524,7 +524,6 @@ public abstract class DisputeManager> extends Sup // update multisig hex if (message.getUpdatedMultisigHex() != null) sender.setUpdatedMultisigHex(message.getUpdatedMultisigHex()); - if (trade.walletExists()) trade.importMultisigHex(); // add chat message with price info if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0); @@ -896,17 +895,12 @@ public abstract class DisputeManager> extends Sup } // create dispute payout tx - MoneroTxWallet payoutTx = null; - try { - payoutTx = trade.getWallet().createTx(txConfig); - } catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException("Loser payout is too small to cover the mining fee"); - } + MoneroTxWallet payoutTx = trade.createDisputePayoutTx(txConfig); // update trade state if (updateState) { trade.getProcessModel().setUnsignedPayoutTx(payoutTx); + trade.getSelf().setUpdatedMultisigHex(trade.getWallet().exportMultisigHex()); trade.setPayoutTx(payoutTx); trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); if (trade.getBuyer().getUpdatedMultisigHex() != null && trade.getBuyer().getUnsignedPayoutTxHex() == null) trade.getBuyer().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); diff --git a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java index 280d68d3..773a2e64 100644 --- a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java +++ b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java @@ -36,7 +36,6 @@ package haveno.core.support.dispute.arbitration; import com.google.inject.Inject; import com.google.inject.Singleton; -import common.utils.GenUtils; import haveno.common.ThreadUtils; import haveno.common.Timer; import haveno.common.UserThread; @@ -68,6 +67,7 @@ import haveno.core.trade.Contract; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; +import haveno.core.trade.protocol.TradeProtocol; import haveno.core.xmr.wallet.TradeWalletService; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.AckMessageSourceType; @@ -290,7 +290,7 @@ public final class ArbitrationManager extends DisputeManager txHashes = multisigWallet.submitMultisigTxHex(disputeTxSet.getMultisigTxHex()); - disputeTxSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed + for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { + try { + List txHashes = multisigWallet.submitMultisigTxHex(disputeTxSet.getMultisigTxHex()); + disputeTxSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed + break; + } catch (Exception e) { + log.warn("Failed to submit dispute payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage()); + if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying + } + } // update state trade.setPayoutTx(disputeTxSet.getTxs().get(0)); // TODO (woodser): is trade.payoutTx() mutually exclusive from dispute payout tx? diff --git a/core/src/main/java/haveno/core/trade/HavenoUtils.java b/core/src/main/java/haveno/core/trade/HavenoUtils.java index 8fcbd371..2a0c73f7 100644 --- a/core/src/main/java/haveno/core/trade/HavenoUtils.java +++ b/core/src/main/java/haveno/core/trade/HavenoUtils.java @@ -19,6 +19,8 @@ package haveno.core.trade; import com.google.common.base.CaseFormat; import com.google.common.base.Charsets; + +import common.utils.GenUtils; import haveno.common.config.Config; import haveno.common.crypto.CryptoException; import haveno.common.crypto.Hash; @@ -70,6 +72,17 @@ public class HavenoUtils { public static final double TAKER_FEE_PCT = 0.0075; // 0.75% public static final double PENALTY_FEE_PCT = 0.02; // 2% + // synchronize requests to the daemon + private static boolean SYNC_DAEMON_REQUESTS = true; // sync long requests to daemon (e.g. refresh, update pool) + private static boolean SYNC_WALLET_REQUESTS = false; // additionally sync wallet functions to daemon (e.g. create tx, import multisig hex) + private static Object DAEMON_LOCK = new Object(); + public static Object getDaemonLock() { + return SYNC_DAEMON_REQUESTS ? DAEMON_LOCK : new Object(); + } + public static Object getWalletFunctionLock() { + return SYNC_WALLET_REQUESTS ? getDaemonLock() : new Object(); + } + // non-configurable public static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = DecimalFormatSymbols.getInstance(Locale.US); // use the US locale as a base for all DecimalFormats (commas should be omitted from number strings) public static int XMR_SMALLEST_UNIT_EXPONENT = 12; @@ -108,6 +121,10 @@ public class HavenoUtils { return new Date().before(releaseDatePlusDays); } + public static void waitFor(long waitMs) { + GenUtils.waitFor(waitMs); + } + // ----------------------- CONVERSION UTILS ------------------------------- public static BigInteger coinToAtomicUnits(Coin coin) { diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index dafdae47..88d63eb5 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -37,7 +37,6 @@ package haveno.core.trade; import com.google.common.base.Preconditions; import com.google.protobuf.ByteString; import com.google.protobuf.Message; -import common.utils.GenUtils; import haveno.common.ThreadUtils; import haveno.common.UserThread; import haveno.common.crypto.Encryption; @@ -120,7 +119,6 @@ import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Optional; -import java.util.concurrent.CountDownLatch; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Collectors; @@ -479,7 +477,6 @@ public abstract class Trade implements Tradable, Model { private long payoutTxFee; private Long payoutHeight; private IdlePayoutSyncer idlePayoutSyncer; - @Getter @Setter private boolean isCompleted; @@ -638,18 +635,14 @@ public abstract class Trade implements Tradable, Model { // handle trade state events tradeStateSubscription = EasyBind.subscribe(stateProperty, newValue -> { if (!isInitialized || isShutDownStarted) return; - ThreadUtils.execute(() -> { - if (newValue == Trade.State.MULTISIG_COMPLETED) { - updatePollPeriod(); - startPolling(); - } - }, getId()); + // no processing }); // handle trade phase events tradePhaseSubscription = EasyBind.subscribe(phaseProperty, newValue -> { if (!isInitialized || isShutDownStarted) return; ThreadUtils.execute(() -> { + if (newValue == Trade.Phase.DEPOSIT_REQUESTED) startPolling(); if (isDepositsPublished() && !isPayoutUnlocked()) updatePollPeriod(); if (isPaymentReceived()) { UserThread.execute(() -> { @@ -674,9 +667,9 @@ public abstract class Trade implements Tradable, Model { // sync main wallet to update pending balance new Thread(() -> { - GenUtils.waitFor(1000); + HavenoUtils.waitFor(1000); if (isShutDownStarted) return; - if (Boolean.TRUE.equals(xmrConnectionService.isConnected())) xmrWalletService.syncWallet(xmrWalletService.getWallet()); + if (xmrConnectionService.isConnected()) xmrWalletService.syncWallet(); }).start(); // complete disputed trade @@ -731,16 +724,17 @@ public abstract class Trade implements Tradable, Model { setPayoutStateUnlocked(); return; } else { - throw new IllegalStateException("Missing trade wallet for " + getClass().getSimpleName() + " " + getId()); + log.warn("Missing trade wallet for {} {}, state={}, marked completed={}", getClass().getSimpleName(), getShortId(), getState(), isCompleted()); + return; } } - // initialize syncing and polling - tryInitPolling(); + // start polling if deposit requested + if (isDepositRequested()) tryInitPolling(); } public void requestPersistence() { - processModel.getTradeManager().requestPersistence(); + if (processModel.getTradeManager() != null) processModel.getTradeManager().requestPersistence(); } public TradeProtocol getProtocol() { @@ -793,21 +787,8 @@ public abstract class Trade implements Tradable, Model { return MONERO_TRADE_WALLET_PREFIX + getShortId() + "_" + getShortUid(); } - public void checkAndVerifyDaemonConnection() { - - // check connection which might update - xmrConnectionService.checkConnection(); - xmrConnectionService.verifyConnection(); - - // check wallet connection on same thread as connection change - CountDownLatch latch = new CountDownLatch(1); - ThreadUtils.submitToPool((() -> { - ThreadUtils.execute(() -> { - if (!isWalletConnectedToDaemon()) throw new RuntimeException("Trade wallet is not connected to a Monero node"); // wallet connection is updated on trade thread - latch.countDown(); - }, getConnectionChangedThreadId()); - })); - HavenoUtils.awaitLatch(latch); // TODO: better way? + public void verifyDaemonConnection() { + if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) throw new RuntimeException("Connection service is not connected to a Monero node"); } public boolean isWalletConnectedToDaemon() { @@ -848,7 +829,7 @@ public abstract class Trade implements Tradable, Model { // reset wallet poll period after duration new Thread(() -> { - GenUtils.waitFor(pollNormalDuration); + HavenoUtils.waitFor(pollNormalDuration); Long pollNormalStartTimeMsCopy = pollNormalStartTimeMs; // copy to avoid race condition if (pollNormalStartTimeMsCopy == null) return; if (!isShutDown && System.currentTimeMillis() >= pollNormalStartTimeMsCopy + pollNormalDuration) { @@ -860,21 +841,38 @@ public abstract class Trade implements Tradable, Model { public void importMultisigHex() { synchronized (walletLock) { - - // ensure wallet sees deposits confirmed - if (!isDepositsConfirmed()) syncAndPollWallet(); - - // import multisig hexes - List multisigHexes = new ArrayList(); - for (TradePeer node : getAllTradeParties()) if (node.getUpdatedMultisigHex() != null) multisigHexes.add(node.getUpdatedMultisigHex()); - if (!multisigHexes.isEmpty()) { - log.info("Importing multisig hex for {} {}", getClass().getSimpleName(), getId()); - long startTime = System.currentTimeMillis(); - getWallet().importMultisigHex(multisigHexes.toArray(new String[0])); - log.info("Done importing multisig hex for {} {} in {} ms", getClass().getSimpleName(), getId(), System.currentTimeMillis() - startTime); + synchronized (HavenoUtils.getWalletFunctionLock()) { + for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { + try { + doImportMultisigHex(); + break; + } catch (Exception e) { + log.warn("Failed to import multisig hex, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage()); + if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying + } + } } + } + } + + private void doImportMultisigHex() { + + // ensure wallet sees deposits confirmed + if (!isDepositsConfirmed()) syncAndPollWallet(); + + // collect multisig hex from peers + List multisigHexes = new ArrayList(); + for (TradePeer peer : getOtherPeers()) if (peer.getUpdatedMultisigHex() != null) multisigHexes.add(peer.getUpdatedMultisigHex()); + + // import multisig hex + log.info("Importing multisig hexes for {} {}, count={}", getClass().getSimpleName(), getShortId(), multisigHexes.size()); + long startTime = System.currentTimeMillis(); + if (!multisigHexes.isEmpty()) { + wallet.importMultisigHex(multisigHexes.toArray(new String[0])); requestSaveWallet(); } + log.info("Done importing multisig hexes for {} {} in {} ms, count={}", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime, multisigHexes.size()); } public void changeWalletPassword(String oldPassword, String newPassword) { @@ -891,10 +889,10 @@ public abstract class Trade implements Tradable, Model { public void saveWallet() { synchronized (walletLock) { if (!walletExists()) { - log.warn("Cannot save wallet for {} {} because it does not exist", getClass().getSimpleName(), getId()); + log.warn("Cannot save wallet for {} {} because it does not exist", getClass().getSimpleName(), getShortId()); return; } - if (wallet == null) throw new RuntimeException("Trade wallet is not open for trade " + getId()); + if (wallet == null) throw new RuntimeException("Trade wallet is not open for trade " + getShortId()); xmrWalletService.saveWallet(wallet); maybeBackupWallet(); } @@ -953,7 +951,13 @@ public abstract class Trade implements Tradable, Model { // check for balance if (wallet.getBalance().compareTo(BigInteger.ZERO) > 0) { - throw new IllegalStateException("Refusing to delete wallet for " + getClass().getSimpleName() + " " + getId() + " because it has a balance"); + synchronized (HavenoUtils.getDaemonLock()) { + log.warn("Rescanning spent outputs for {} {}", getClass().getSimpleName(), getId()); + wallet.rescanSpent(); + if (wallet.getBalance().compareTo(BigInteger.ZERO) > 0) { + throw new IllegalStateException("Refusing to delete wallet for " + getClass().getSimpleName() + " " + getId() + " because it has a balance of " + wallet.getBalance()); + } + } } // force close wallet without warning @@ -1021,17 +1025,44 @@ public abstract class Trade implements Tradable, Model { return contract; } + public MoneroTxWallet createTx(MoneroTxConfig txConfig) { + synchronized (walletLock) { + synchronized (HavenoUtils.getWalletFunctionLock()) { + return wallet.createTx(txConfig); + } + } + } + /** * Create the payout tx. * - * @return MoneroTxWallet the payout tx when the trade is successfully completed + * @return the payout tx when the trade is successfully completed */ public MoneroTxWallet createPayoutTx() { // check connection to monero daemon - checkAndVerifyDaemonConnection(); + verifyDaemonConnection(); - // check multisig import + // create payout tx + synchronized (walletLock) { + synchronized (HavenoUtils.getWalletFunctionLock()) { + for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { + try { + return doCreatePayoutTx(); + } catch (Exception e) { + log.warn("Failed to create payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage()); + if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying + } + } + throw new RuntimeException("Failed to create payout tx for " + getClass().getSimpleName() + " " + getId()); + } + } + } + + private MoneroTxWallet doCreatePayoutTx() { + + // check if multisig import needed MoneroWallet multisigWallet = getWallet(); if (multisigWallet.isMultisigImportNeeded()) throw new RuntimeException("Cannot create payout tx because multisig import is needed"); @@ -1047,7 +1078,7 @@ public abstract class Trade implements Tradable, Model { BigInteger sellerPayoutAmount = sellerDepositAmount.subtract(tradeAmount); // create payout tx - MoneroTxWallet payoutTx = multisigWallet.createTx(new MoneroTxConfig() + MoneroTxWallet payoutTx = createTx(new MoneroTxConfig() .setAccountIndex(0) .addDestination(buyerPayoutAddress, buyerPayoutAmount) .addDestination(sellerPayoutAddress, sellerPayoutAmount) @@ -1066,6 +1097,24 @@ public abstract class Trade implements Tradable, Model { return payoutTx; } + public MoneroTxWallet createDisputePayoutTx(MoneroTxConfig txConfig) { + synchronized (walletLock) { + synchronized (HavenoUtils.getWalletFunctionLock()) { + for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { + try { + return createTx(txConfig); + } catch (Exception e) { + if (e.getMessage().contains("not possible")) throw new RuntimeException("Loser payout is too small to cover the mining fee"); + log.warn("Failed to create payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage()); + if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying + } + } + throw new RuntimeException("Failed to create payout tx for " + getClass().getSimpleName() + " " + getId()); + } + } + } + /** * Process a payout tx. * @@ -1074,82 +1123,93 @@ public abstract class Trade implements Tradable, Model { * @param publish publishes the signed payout tx if true */ public void processPayoutTx(String payoutTxHex, boolean sign, boolean publish) { - log.info("Processing payout tx for {} {}", getClass().getSimpleName(), getId()); + synchronized (walletLock) { + log.info("Processing payout tx for {} {}", getClass().getSimpleName(), getId()); - // gather relevant info - MoneroWallet wallet = getWallet(); - Contract contract = getContract(); - BigInteger sellerDepositAmount = wallet.getTx(getSeller().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): redundancy of processModel.getPreparedDepositTxId() vs this.getDepositTxId() necessary or avoidable? - BigInteger buyerDepositAmount = wallet.getTx(getBuyer().getDepositTxHash()).getIncomingAmount(); - BigInteger tradeAmount = getAmount(); - - // describe payout tx - MoneroTxSet describedTxSet = wallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex)); - if (describedTxSet.getTxs() == null || describedTxSet.getTxs().size() != 1) throw new IllegalArgumentException("Bad payout tx"); // TODO (woodser): test nack - MoneroTxWallet payoutTx = describedTxSet.getTxs().get(0); - - // verify payout tx has exactly 2 destinations - if (payoutTx.getOutgoingTransfer() == null || payoutTx.getOutgoingTransfer().getDestinations() == null || payoutTx.getOutgoingTransfer().getDestinations().size() != 2) throw new IllegalArgumentException("Payout tx does not have exactly two destinations"); - - // get buyer and seller destinations (order not preserved) - boolean buyerFirst = payoutTx.getOutgoingTransfer().getDestinations().get(0).getAddress().equals(contract.getBuyerPayoutAddressString()); - MoneroDestination buyerPayoutDestination = payoutTx.getOutgoingTransfer().getDestinations().get(buyerFirst ? 0 : 1); - MoneroDestination sellerPayoutDestination = payoutTx.getOutgoingTransfer().getDestinations().get(buyerFirst ? 1 : 0); - - // verify payout addresses - if (!buyerPayoutDestination.getAddress().equals(contract.getBuyerPayoutAddressString())) throw new IllegalArgumentException("Buyer payout address does not match contract"); - if (!sellerPayoutDestination.getAddress().equals(contract.getSellerPayoutAddressString())) throw new IllegalArgumentException("Seller payout address does not match contract"); - - // verify change address is multisig's primary address - if (!payoutTx.getChangeAmount().equals(BigInteger.ZERO)) log.warn("Dust left in multisig wallet for {} {}: {}", getClass().getSimpleName(), getId(), payoutTx.getChangeAmount()); - if (!payoutTx.getChangeAmount().equals(BigInteger.ZERO) && !payoutTx.getChangeAddress().equals(wallet.getPrimaryAddress())) throw new IllegalArgumentException("Change address is not multisig wallet's primary address"); - - // verify sum of outputs = destination amounts + change amount - if (!payoutTx.getOutputSum().equals(buyerPayoutDestination.getAmount().add(sellerPayoutDestination.getAmount()).add(payoutTx.getChangeAmount()))) throw new IllegalArgumentException("Sum of outputs != destination amounts + change amount"); - - // verify buyer destination amount is deposit amount + this amount - 1/2 tx costs - BigInteger txCost = payoutTx.getFee().add(payoutTx.getChangeAmount()); - BigInteger txCostSplit = txCost.divide(BigInteger.valueOf(2)); - BigInteger expectedBuyerPayout = buyerDepositAmount.add(tradeAmount).subtract(txCostSplit); - if (!buyerPayoutDestination.getAmount().equals(expectedBuyerPayout)) throw new IllegalArgumentException("Buyer destination amount is not deposit amount + trade amount - 1/2 tx costs, " + buyerPayoutDestination.getAmount() + " vs " + expectedBuyerPayout); - - // verify seller destination amount is deposit amount - this amount - 1/2 tx costs - BigInteger expectedSellerPayout = sellerDepositAmount.subtract(tradeAmount).subtract(txCostSplit); - if (!sellerPayoutDestination.getAmount().equals(expectedSellerPayout)) throw new IllegalArgumentException("Seller destination amount is not deposit amount - trade amount - 1/2 tx costs, " + sellerPayoutDestination.getAmount() + " vs " + expectedSellerPayout); - - // check wallet connection - if (sign || publish) checkAndVerifyDaemonConnection(); - - // handle tx signing - if (sign) { - - // sign tx - MoneroMultisigSignResult result = wallet.signMultisigTxHex(payoutTxHex); - if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing payout tx"); - payoutTxHex = result.getSignedMultisigTxHex(); - - // describe result - describedTxSet = wallet.describeMultisigTxSet(payoutTxHex); - payoutTx = describedTxSet.getTxs().get(0); - - // verify fee is within tolerance by recreating payout tx - // TODO (monero-project): creating tx will require exchanging updated multisig hex if message needs reprocessed. provide weight with describe_transfer so fee can be estimated? - MoneroTxWallet feeEstimateTx = createPayoutTx(); - BigInteger feeEstimate = feeEstimateTx.getFee(); - double feeDiff = payoutTx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal? - if (feeDiff > XmrWalletService.MINER_FEE_TOLERANCE) throw new IllegalArgumentException("Miner fee is not within " + (XmrWalletService.MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + payoutTx.getFee()); - log.info("Payout tx fee {} is within tolerance, diff %={}", payoutTx.getFee(), feeDiff); - } - - // update trade state - setPayoutTx(payoutTx); - setPayoutTxHex(payoutTxHex); - - // submit payout tx - if (publish) { - wallet.submitMultisigTxHex(payoutTxHex); - pollWallet(); + // gather relevant info + MoneroWallet wallet = getWallet(); + Contract contract = getContract(); + BigInteger sellerDepositAmount = wallet.getTx(getSeller().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): redundancy of processModel.getPreparedDepositTxId() vs this.getDepositTxId() necessary or avoidable? + BigInteger buyerDepositAmount = wallet.getTx(getBuyer().getDepositTxHash()).getIncomingAmount(); + BigInteger tradeAmount = getAmount(); + + // describe payout tx + MoneroTxSet describedTxSet = wallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex)); + if (describedTxSet.getTxs() == null || describedTxSet.getTxs().size() != 1) throw new IllegalArgumentException("Bad payout tx"); // TODO (woodser): test nack + MoneroTxWallet payoutTx = describedTxSet.getTxs().get(0); + + // verify payout tx has exactly 2 destinations + if (payoutTx.getOutgoingTransfer() == null || payoutTx.getOutgoingTransfer().getDestinations() == null || payoutTx.getOutgoingTransfer().getDestinations().size() != 2) throw new IllegalArgumentException("Payout tx does not have exactly two destinations"); + + // get buyer and seller destinations (order not preserved) + boolean buyerFirst = payoutTx.getOutgoingTransfer().getDestinations().get(0).getAddress().equals(contract.getBuyerPayoutAddressString()); + MoneroDestination buyerPayoutDestination = payoutTx.getOutgoingTransfer().getDestinations().get(buyerFirst ? 0 : 1); + MoneroDestination sellerPayoutDestination = payoutTx.getOutgoingTransfer().getDestinations().get(buyerFirst ? 1 : 0); + + // verify payout addresses + if (!buyerPayoutDestination.getAddress().equals(contract.getBuyerPayoutAddressString())) throw new IllegalArgumentException("Buyer payout address does not match contract"); + if (!sellerPayoutDestination.getAddress().equals(contract.getSellerPayoutAddressString())) throw new IllegalArgumentException("Seller payout address does not match contract"); + + // verify change address is multisig's primary address + if (!payoutTx.getChangeAmount().equals(BigInteger.ZERO)) log.warn("Dust left in multisig wallet for {} {}: {}", getClass().getSimpleName(), getId(), payoutTx.getChangeAmount()); + if (!payoutTx.getChangeAmount().equals(BigInteger.ZERO) && !payoutTx.getChangeAddress().equals(wallet.getPrimaryAddress())) throw new IllegalArgumentException("Change address is not multisig wallet's primary address"); + + // verify sum of outputs = destination amounts + change amount + if (!payoutTx.getOutputSum().equals(buyerPayoutDestination.getAmount().add(sellerPayoutDestination.getAmount()).add(payoutTx.getChangeAmount()))) throw new IllegalArgumentException("Sum of outputs != destination amounts + change amount"); + + // verify buyer destination amount is deposit amount + this amount - 1/2 tx costs + BigInteger txCost = payoutTx.getFee().add(payoutTx.getChangeAmount()); + BigInteger txCostSplit = txCost.divide(BigInteger.valueOf(2)); + BigInteger expectedBuyerPayout = buyerDepositAmount.add(tradeAmount).subtract(txCostSplit); + if (!buyerPayoutDestination.getAmount().equals(expectedBuyerPayout)) throw new IllegalArgumentException("Buyer destination amount is not deposit amount + trade amount - 1/2 tx costs, " + buyerPayoutDestination.getAmount() + " vs " + expectedBuyerPayout); + + // verify seller destination amount is deposit amount - this amount - 1/2 tx costs + BigInteger expectedSellerPayout = sellerDepositAmount.subtract(tradeAmount).subtract(txCostSplit); + if (!sellerPayoutDestination.getAmount().equals(expectedSellerPayout)) throw new IllegalArgumentException("Seller destination amount is not deposit amount - trade amount - 1/2 tx costs, " + sellerPayoutDestination.getAmount() + " vs " + expectedSellerPayout); + + // check connection + if (sign || publish) verifyDaemonConnection(); + + // handle tx signing + if (sign) { + + // sign tx + MoneroMultisigSignResult result = wallet.signMultisigTxHex(payoutTxHex); + if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing payout tx"); + payoutTxHex = result.getSignedMultisigTxHex(); + + // describe result + describedTxSet = wallet.describeMultisigTxSet(payoutTxHex); + payoutTx = describedTxSet.getTxs().get(0); + + // verify fee is within tolerance by recreating payout tx + // TODO (monero-project): creating tx will require exchanging updated multisig hex if message needs reprocessed. provide weight with describe_transfer so fee can be estimated? + MoneroTxWallet feeEstimateTx = createPayoutTx(); + BigInteger feeEstimate = feeEstimateTx.getFee(); + double feeDiff = payoutTx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal? + if (feeDiff > XmrWalletService.MINER_FEE_TOLERANCE) throw new IllegalArgumentException("Miner fee is not within " + (XmrWalletService.MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + payoutTx.getFee()); + log.info("Payout tx fee {} is within tolerance, diff %={}", payoutTx.getFee(), feeDiff); + } + + // update trade state + setPayoutTx(payoutTx); + setPayoutTxHex(payoutTxHex); + + // submit payout tx + if (publish) { + for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { + try { + wallet.submitMultisigTxHex(payoutTxHex); + break; + } catch (Exception e) { + log.warn("Failed to submit payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage()); + if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying + } + } + } } + pollWallet(); } /** @@ -1173,6 +1233,7 @@ public abstract class Trade implements Tradable, Model { // set payment account payload getTradePeer().setPaymentAccountPayload(paymentAccountPayload); + processModel.getPaymentAccountDecryptedProperty().set(true); } catch (Exception e) { throw new RuntimeException(e); } @@ -1220,6 +1281,7 @@ public abstract class Trade implements Tradable, Model { public void clearAndShutDown() { ThreadUtils.execute(() -> { clearProcessData(); + onShutDownStarted(); ThreadUtils.submitToPool(() -> shutDown()); // run off trade thread }, getId()); } @@ -1237,7 +1299,7 @@ public abstract class Trade implements Tradable, Model { // TODO: clear other process data setPayoutTxHex(null); - for (TradePeer peer : getAllTradeParties()) { + for (TradePeer peer : getAllPeers()) { peer.setUnsignedPayoutTxHex(null); peer.setUpdatedMultisigHex(null); peer.setDisputeClosedMessage(null); @@ -1294,7 +1356,7 @@ public abstract class Trade implements Tradable, Model { // repeatedly acquire lock to clear tasks for (int i = 0; i < 20; i++) { synchronized (this) { - GenUtils.waitFor(10); + HavenoUtils.waitFor(10); } } @@ -1390,6 +1452,7 @@ public abstract class Trade implements Tradable, Model { } this.state = state; + requestPersistence(); UserThread.await(() -> { stateProperty.set(state); phaseProperty.set(state.getPhase()); @@ -1421,6 +1484,7 @@ public abstract class Trade implements Tradable, Model { } this.payoutState = payoutState; + requestPersistence(); UserThread.await(() -> payoutStateProperty.set(payoutState)); } @@ -1572,13 +1636,13 @@ public abstract class Trade implements Tradable, Model { throw new RuntimeException("Trade is not maker, taker, or arbitrator"); } - private List getPeers() { - List peers = getAllTradeParties(); + private List getOtherPeers() { + List peers = getAllPeers(); if (!peers.remove(getSelf())) throw new IllegalStateException("Failed to remove self from list of peers"); return peers; } - private List getAllTradeParties() { + private List getAllPeers() { List peers = new ArrayList(); peers.add(getMaker()); peers.add(getTaker()); @@ -1765,7 +1829,7 @@ public abstract class Trade implements Tradable, Model { if (this instanceof BuyerTrade) { return getArbitrator().isDepositsConfirmedMessageAcked(); } else { - for (TradePeer peer : getPeers()) if (!peer.isDepositsConfirmedMessageAcked()) return false; + for (TradePeer peer : getOtherPeers()) if (!peer.isDepositsConfirmedMessageAcked()) return false; return true; } } @@ -1982,13 +2046,19 @@ public abstract class Trade implements Tradable, Model { } // sync and reprocess messages on new thread - if (isInitialized && connection != null && !Boolean.FALSE.equals(connection.isConnected())) { + if (isInitialized && connection != null && !Boolean.FALSE.equals(xmrConnectionService.isConnected())) { ThreadUtils.execute(() -> tryInitPolling(), getId()); } } } private void tryInitPolling() { if (isShutDownStarted) return; + + // set known deposit txs + List depositTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true).setInTxPool(false)); + setDepositTxs(depositTxs); + + // start polling if (!isIdling()) { tryInitPollingAux(); } else { @@ -2023,9 +2093,12 @@ public abstract class Trade implements Tradable, Model { private void syncWallet(boolean pollWallet) { if (getWallet() == null) throw new RuntimeException("Cannot sync trade wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId()); if (getWallet().getDaemonConnection() == null) throw new RuntimeException("Cannot sync trade wallet because it's not connected to a Monero daemon for " + getClass().getSimpleName() + ", " + getId()); - log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getId()); - xmrWalletService.syncWallet(getWallet()); - log.info("Done syncing wallet for {} {}", getClass().getSimpleName(), getId()); + if (isWalletBehind()) { + log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getShortId()); + long startTime = System.currentTimeMillis(); + syncWalletIfBehind(); + log.info("Done syncing wallet for {} {} in {} ms", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime); + } // apply tor after wallet synced depending on configuration if (!wasWalletSynced) { @@ -2063,6 +2136,7 @@ public abstract class Trade implements Tradable, Model { private void startPolling() { synchronized (walletLock) { if (isShutDownStarted || isPollInProgress()) return; + updatePollPeriod(); log.info("Starting to poll wallet for {} {}", getClass().getSimpleName(), getId()); pollLooper = new TaskLooper(() -> pollWallet()); pollLooper.start(pollPeriodMs); @@ -2110,7 +2184,15 @@ public abstract class Trade implements Tradable, Model { MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true); Boolean updatePool = !isDepositsConfirmed() && (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null); if (!updatePool) query.setInTxPool(false); // avoid updating from pool if possible - List txs = wallet.getTxs(query); + List txs; + if (!updatePool) txs = wallet.getTxs(query); + else { + synchronized (walletLock) { + synchronized (HavenoUtils.getDaemonLock()) { + txs = wallet.getTxs(query); + } + } + } setDepositTxs(txs); if (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null) return; // skip if either deposit tx not seen setStateDepositsSeen(); @@ -2142,7 +2224,7 @@ public abstract class Trade implements Tradable, Model { if (isPayoutExpected || isPayoutPublished()) syncWalletIfBehind(); // rescan spent outputs to detect unconfirmed payout tx - if (isPayoutExpected && !isPayoutPublished()) { + if (isPayoutExpected && wallet.getBalance().compareTo(BigInteger.ZERO) > 0) { try { wallet.rescanSpent(); } catch (Exception e) { @@ -2154,7 +2236,15 @@ public abstract class Trade implements Tradable, Model { MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true); boolean updatePool = isPayoutExpected && !isPayoutConfirmed(); if (!updatePool) query.setInTxPool(false); // avoid updating from pool if possible - List txs = wallet.getTxs(query); + List txs = null; + if (!updatePool) txs = wallet.getTxs(query); + else { + synchronized (walletLock) { + synchronized (HavenoUtils.getDaemonLock()) { + txs = wallet.getTxs(query); + } + } + } setDepositTxs(txs); // check if any outputs spent (observed on payout published) @@ -2191,7 +2281,15 @@ public abstract class Trade implements Tradable, Model { } private void syncWalletIfBehind() { - if (wallet.getHeight() < xmrConnectionService.getTargetHeight()) syncWallet(false); + if (isWalletBehind()) { + synchronized (walletLock) { + xmrWalletService.syncWallet(wallet); + } + } + } + + private boolean isWalletBehind() { + return wallet.getHeight() < xmrConnectionService.getTargetHeight(); } private void setDepositTxs(List txs) { @@ -2278,9 +2376,8 @@ public abstract class Trade implements Tradable, Model { processing = false; } catch (Exception e) { processing = false; - boolean isWalletConnected = isWalletConnectedToDaemon(); - if (!isWalletConnected) xmrConnectionService.checkConnection(); // check connection if wallet is not connected - if (isInitialized &&!isShutDownStarted && isWalletConnected) { + if (!isInitialized || isShutDownStarted) return; + if (isWalletConnectedToDaemon()) { e.printStackTrace(); log.warn("Error polling idle trade for {} {}: {}. Monerod={}", getClass().getSimpleName(), getId(), e.getMessage(), getXmrWalletService().getConnectionService().getConnection()); }; diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index 5ccd1ecd..4c5cd81b 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -38,7 +38,6 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.collect.ImmutableList; import com.google.inject.Inject; -import common.utils.GenUtils; import haveno.common.ClockWatcher; import haveno.common.ThreadUtils; import haveno.common.UserThread; @@ -512,7 +511,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi }).start(); // allow execution to start - GenUtils.waitFor(100); + HavenoUtils.waitFor(100); } private void initPersistedTrade(Trade trade) { @@ -1249,27 +1248,20 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } private void addTrade(Trade trade) { - UserThread.execute(() -> { - synchronized (tradableList) { - if (tradableList.add(trade)) { - requestPersistence(); - } + synchronized (tradableList) { + if (tradableList.add(trade)) { + requestPersistence(); } - }); + } } private void removeTrade(Trade trade) { log.info("TradeManager.removeTrade() " + trade.getId()); - synchronized (tradableList) { - if (!tradableList.contains(trade)) return; - } - + // remove trade - UserThread.execute(() -> { - synchronized (tradableList) { - tradableList.remove(trade); - } - }); + synchronized (tradableList) { + if (!tradableList.remove(trade)) return; + } // unregister and persist p2PService.removeDecryptedDirectMessageListener(getTradeProtocol(trade)); @@ -1277,30 +1269,26 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } private void maybeRemoveTradeOnError(Trade trade) { - synchronized (tradableList) { - if (trade.isDepositRequested() && !trade.isDepositFailed()) { - listenForCleanup(trade); - } else { - removeTradeOnError(trade); - } + if (trade.isDepositRequested() && !trade.isDepositFailed()) { + listenForCleanup(trade); + } else { + removeTradeOnError(trade); } } private void removeTradeOnError(Trade trade) { log.warn("TradeManager.removeTradeOnError() trade={}, tradeId={}, state={}", trade.getClass().getSimpleName(), trade.getShortId(), trade.getState()); - synchronized (tradableList) { - // unreserve taker key images - if (trade instanceof TakerTrade && trade.getSelf().getReserveTxKeyImages() != null) { - xmrWalletService.thawOutputs(trade.getSelf().getReserveTxKeyImages()); - trade.getSelf().setReserveTxKeyImages(null); - } + // unreserve taker key images + if (trade instanceof TakerTrade) { + xmrWalletService.thawOutputs(trade.getSelf().getReserveTxKeyImages()); + trade.getSelf().setReserveTxKeyImages(null); + } - // unreserve open offer - Optional openOffer = openOfferManager.getOpenOfferById(trade.getId()); - if (trade instanceof MakerTrade && openOffer.isPresent()) { - openOfferManager.unreserveOpenOffer(openOffer.get()); - } + // unreserve open offer + Optional openOffer = openOfferManager.getOpenOfferById(trade.getId()); + if (trade instanceof MakerTrade && openOffer.isPresent()) { + openOfferManager.unreserveOpenOffer(openOffer.get()); } // clear and shut down trade @@ -1358,7 +1346,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi new Thread(() -> { // wait minimum time - GenUtils.waitFor(Math.max(0, REMOVE_AFTER_MS - (System.currentTimeMillis() - startTime))); + HavenoUtils.waitFor(Math.max(0, REMOVE_AFTER_MS - (System.currentTimeMillis() - startTime))); // get trade's deposit txs from daemon MoneroTx makerDepositTx = trade.getMaker().getDepositTxHash() == null ? null : xmrWalletService.getDaemon().getTx(trade.getMaker().getDepositTxHash()); diff --git a/core/src/main/java/haveno/core/trade/protocol/ArbitratorProtocol.java b/core/src/main/java/haveno/core/trade/protocol/ArbitratorProtocol.java index 4a0f2b7f..ff3c09a9 100644 --- a/core/src/main/java/haveno/core/trade/protocol/ArbitratorProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/ArbitratorProtocol.java @@ -59,13 +59,13 @@ public class ArbitratorProtocol extends DisputeProtocol { ArbitratorSendInitTradeOrMultisigRequests.class) .using(new TradeTaskRunner(trade, () -> { - startTimeout(TRADE_TIMEOUT_SECONDS); + startTimeout(TRADE_STEP_TIMEOUT_SECONDS); handleTaskRunnerSuccess(peer, message); }, errorMessage -> { handleTaskRunnerFault(peer, message, errorMessage); })) - .withTimeout(TRADE_TIMEOUT_SECONDS)) + .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) .executeTasks(true); awaitTradeLatch(); } @@ -100,7 +100,7 @@ public class ArbitratorProtocol extends DisputeProtocol { errorMessage -> { handleTaskRunnerFault(sender, request, errorMessage); })) - .withTimeout(TRADE_TIMEOUT_SECONDS)) + .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) .executeTasks(true); awaitTradeLatch(); } diff --git a/core/src/main/java/haveno/core/trade/protocol/BuyerAsMakerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/BuyerAsMakerProtocol.java index 337d92e2..4ca7f37a 100644 --- a/core/src/main/java/haveno/core/trade/protocol/BuyerAsMakerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/BuyerAsMakerProtocol.java @@ -74,13 +74,13 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol MakerSendInitTradeRequest.class) .using(new TradeTaskRunner(trade, () -> { - startTimeout(TRADE_TIMEOUT_SECONDS); + startTimeout(TRADE_STEP_TIMEOUT_SECONDS); handleTaskRunnerSuccess(peer, message); }, errorMessage -> { handleTaskRunnerFault(peer, message, errorMessage); })) - .withTimeout(TRADE_TIMEOUT_SECONDS)) + .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) .executeTasks(true); awaitTradeLatch(); } diff --git a/core/src/main/java/haveno/core/trade/protocol/BuyerAsTakerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/BuyerAsTakerProtocol.java index e45fe25c..dc921b08 100644 --- a/core/src/main/java/haveno/core/trade/protocol/BuyerAsTakerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/BuyerAsTakerProtocol.java @@ -79,13 +79,13 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol TakerSendInitTradeRequestToArbitrator.class) .using(new TradeTaskRunner(trade, () -> { - startTimeout(TRADE_TIMEOUT_SECONDS); + startTimeout(TRADE_STEP_TIMEOUT_SECONDS); unlatchTrade(); }, errorMessage -> { handleError(errorMessage); })) - .withTimeout(TRADE_TIMEOUT_SECONDS)) + .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) .executeTasks(true); awaitTradeLatch(); } diff --git a/core/src/main/java/haveno/core/trade/protocol/BuyerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/BuyerProtocol.java index 602bb28e..55f253b5 100644 --- a/core/src/main/java/haveno/core/trade/protocol/BuyerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/BuyerProtocol.java @@ -46,6 +46,7 @@ import haveno.core.trade.protocol.tasks.BuyerPreparePaymentSentMessage; import haveno.core.trade.protocol.tasks.BuyerSendPaymentSentMessageToArbitrator; import haveno.core.trade.protocol.tasks.BuyerSendPaymentSentMessageToSeller; import haveno.core.trade.protocol.tasks.SendDepositsConfirmedMessageToArbitrator; +import haveno.core.trade.protocol.tasks.SendDepositsConfirmedMessageToSeller; import haveno.core.trade.protocol.tasks.TradeTask; import haveno.network.p2p.NodeAddress; import lombok.extern.slf4j.Slf4j; @@ -158,6 +159,6 @@ public class BuyerProtocol extends DisputeProtocol { @SuppressWarnings("unchecked") @Override public Class[] getDepositsConfirmedTasks() { - return new Class[] { SendDepositsConfirmedMessageToArbitrator.class }; + return new Class[] { SendDepositsConfirmedMessageToSeller.class, SendDepositsConfirmedMessageToArbitrator.class }; } } diff --git a/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java b/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java index 5bfb54a4..8e26e3b8 100644 --- a/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java +++ b/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java @@ -163,6 +163,7 @@ public class ProcessModel implements Model, PersistablePayload { private ObjectProperty paymentSentMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); @Setter private ObjectProperty paymentSentMessageStatePropertyArbitrator = new SimpleObjectProperty<>(MessageState.UNDEFINED); + private ObjectProperty paymentAccountDecryptedProperty = new SimpleObjectProperty<>(false); public ProcessModel(String offerId, String accountId, PubKeyRing pubKeyRing) { this(offerId, accountId, pubKeyRing, new TradePeer(), new TradePeer(), new TradePeer()); diff --git a/core/src/main/java/haveno/core/trade/protocol/SellerAsMakerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/SellerAsMakerProtocol.java index d0d3d46f..88c3e24b 100644 --- a/core/src/main/java/haveno/core/trade/protocol/SellerAsMakerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/SellerAsMakerProtocol.java @@ -79,13 +79,13 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc MakerSendInitTradeRequest.class) .using(new TradeTaskRunner(trade, () -> { - startTimeout(TRADE_TIMEOUT_SECONDS); + startTimeout(TRADE_STEP_TIMEOUT_SECONDS); handleTaskRunnerSuccess(peer, message); }, errorMessage -> { handleTaskRunnerFault(peer, message, errorMessage); })) - .withTimeout(TRADE_TIMEOUT_SECONDS)) + .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) .executeTasks(true); awaitTradeLatch(); } diff --git a/core/src/main/java/haveno/core/trade/protocol/SellerAsTakerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/SellerAsTakerProtocol.java index 0fd8f073..9dcd761b 100644 --- a/core/src/main/java/haveno/core/trade/protocol/SellerAsTakerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/SellerAsTakerProtocol.java @@ -80,13 +80,13 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc TakerSendInitTradeRequestToArbitrator.class) .using(new TradeTaskRunner(trade, () -> { - startTimeout(TRADE_TIMEOUT_SECONDS); + startTimeout(TRADE_STEP_TIMEOUT_SECONDS); unlatchTrade(); }, errorMessage -> { handleError(errorMessage); })) - .withTimeout(TRADE_TIMEOUT_SECONDS)) + .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) .executeTasks(true); awaitTradeLatch(); } 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 3b2ddd8e..ad5bf44f 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -37,6 +37,7 @@ package haveno.core.trade.protocol; import haveno.common.ThreadUtils; import haveno.common.Timer; import haveno.common.UserThread; +import haveno.common.config.Config; import haveno.common.crypto.PubKeyRing; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.proto.network.NetworkEnvelope; @@ -67,7 +68,7 @@ import haveno.core.trade.protocol.tasks.ProcessInitMultisigRequest; import haveno.core.trade.protocol.tasks.ProcessPaymentReceivedMessage; import haveno.core.trade.protocol.tasks.ProcessPaymentSentMessage; import haveno.core.trade.protocol.tasks.ProcessSignContractRequest; -import haveno.core.trade.protocol.tasks.ProcessSignContractResponse; +import haveno.core.trade.protocol.tasks.SendDepositRequest; import haveno.core.trade.protocol.tasks.RemoveOffer; import haveno.core.trade.protocol.tasks.SellerPublishTradeStatistics; import haveno.core.trade.protocol.tasks.MaybeResendDisputeClosedMessageWithPayout; @@ -93,8 +94,10 @@ import java.util.concurrent.CountDownLatch; @Slf4j public abstract class TradeProtocol implements DecryptedDirectMessageListener, DecryptedMailboxListener { - public static final int TRADE_TIMEOUT_SECONDS = 120; + public static final int TRADE_STEP_TIMEOUT_SECONDS = Config.baseCurrencyNetwork().isTestnet() ? 45 : 180; private static final String TIMEOUT_REACHED = "Timeout reached."; + public static final int MAX_ATTEMPTS = 3; + public static final long REPROCESS_DELAY_MS = 5000; protected final ProcessModel processModel; protected final Trade trade; @@ -104,6 +107,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D protected TradeResultHandler tradeResultHandler; protected ErrorMessageHandler errorMessageHandler; + private boolean depositsConfirmedTasksCalled; private int reprocessPaymentReceivedMessageCount; /////////////////////////////////////////////////////////////////////////////////////////// @@ -251,14 +255,14 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } // send deposits confirmed message if applicable - maybeSendDepositsConfirmedMessages(); EasyBind.subscribe(trade.stateProperty(), state -> maybeSendDepositsConfirmedMessages()); } public void maybeSendDepositsConfirmedMessages() { if (!trade.isInitialized() || trade.isShutDownStarted()) return; ThreadUtils.execute(() -> { - if (!trade.isDepositsConfirmed() || trade.isDepositsConfirmedAcked() || trade.isPayoutPublished()) return; + if (!trade.isDepositsConfirmed() || trade.isDepositsConfirmedAcked() || trade.isPayoutPublished() || depositsConfirmedTasksCalled) return; + depositsConfirmedTasksCalled = true; synchronized (trade) { if (!trade.isInitialized() || trade.isShutDownStarted()) return; // skip if shutting down latchTrade(); @@ -316,13 +320,13 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D MaybeSendSignContractRequest.class) .using(new TradeTaskRunner(trade, () -> { - startTimeout(TRADE_TIMEOUT_SECONDS); + startTimeout(TRADE_STEP_TIMEOUT_SECONDS); handleTaskRunnerSuccess(sender, request); }, errorMessage -> { handleTaskRunnerFault(sender, request, errorMessage); })) - .withTimeout(TRADE_TIMEOUT_SECONDS)) + .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) .executeTasks(true); awaitTradeLatch(); } @@ -354,13 +358,13 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D ProcessSignContractRequest.class) .using(new TradeTaskRunner(trade, () -> { - startTimeout(TRADE_TIMEOUT_SECONDS); + startTimeout(TRADE_STEP_TIMEOUT_SECONDS); handleTaskRunnerSuccess(sender, message); }, errorMessage -> { handleTaskRunnerFault(sender, message, errorMessage); })) - .withTimeout(TRADE_TIMEOUT_SECONDS)) // extend timeout + .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) // extend timeout .executeTasks(true); awaitTradeLatch(); } else { @@ -396,16 +400,16 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D .from(sender)) .setup(tasks( // TODO (woodser): validate request - ProcessSignContractResponse.class) + SendDepositRequest.class) .using(new TradeTaskRunner(trade, () -> { - startTimeout(TRADE_TIMEOUT_SECONDS); + startTimeout(TRADE_STEP_TIMEOUT_SECONDS); handleTaskRunnerSuccess(sender, message); }, errorMessage -> { handleTaskRunnerFault(sender, message, errorMessage); })) - .withTimeout(TRADE_TIMEOUT_SECONDS)) // extend timeout + .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) // extend timeout .executeTasks(true); awaitTradeLatch(); } else { @@ -451,7 +455,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D errorMessage -> { handleTaskRunnerFault(sender, response, errorMessage); })) - .withTimeout(TRADE_TIMEOUT_SECONDS)) + .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) .executeTasks(true); awaitTradeLatch(); } 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 8a1544d2..89366767 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 @@ -64,7 +64,7 @@ public class BuyerPreparePaymentSentMessage extends TradeTask { // skip if payout tx already created if (trade.getPayoutTxHex() != null) { - log.warn("Skipping preparation of payment sent message because payout tx is already created for {} {}", trade.getClass().getSimpleName(), trade.getId()); + log.warn("Skipping preparation of payment sent message because payout tx is already created for {} {}", trade.getClass().getSimpleName(), trade.getShortId()); complete(); return; } @@ -83,7 +83,7 @@ public class BuyerPreparePaymentSentMessage extends TradeTask { trade.importMultisigHex(); // create payout tx - log.info("Buyer creating unsigned payout tx"); + log.info("Buyer creating unsigned payout tx for {} {} ", trade.getClass().getSimpleName(), trade.getShortId()); MoneroTxWallet payoutTx = trade.createPayoutTx(); trade.setPayoutTx(payoutTx); trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java index 4caf2c84..80ea6cd3 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java @@ -28,6 +28,7 @@ import haveno.core.trade.Trade.State; import haveno.core.trade.messages.SignContractRequest; import haveno.core.trade.protocol.TradeProtocol; import haveno.core.xmr.model.XmrAddressEntry; +import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.SendDirectMessageListener; import lombok.extern.slf4j.Slf4j; import monero.daemon.model.MoneroOutput; @@ -78,37 +79,70 @@ public class MaybeSendSignContractRequest extends TradeTask { trade.addInitProgressStep(); // create deposit tx and freeze inputs - Integer subaddressIndex = null; - boolean reserveExactAmount = false; - if (trade instanceof MakerTrade) { - reserveExactAmount = processModel.getOpenOfferManager().getOpenOfferById(trade.getId()).get().isReserveExactAmount(); - if (reserveExactAmount) subaddressIndex = model.getXmrWalletService().getAddressEntry(trade.getId(), XmrAddressEntry.Context.OFFER_FUNDING).get().getSubaddressIndex(); + MoneroTxWallet depositTx = null; + synchronized (XmrWalletService.WALLET_LOCK) { + + // check for timeout + if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating deposit tx, tradeId=" + trade.getShortId()); + + // collect relevant info + Integer subaddressIndex = null; + boolean reserveExactAmount = false; + if (trade instanceof MakerTrade) { + reserveExactAmount = processModel.getOpenOfferManager().getOpenOfferById(trade.getId()).get().isReserveExactAmount(); + if (reserveExactAmount) subaddressIndex = model.getXmrWalletService().getAddressEntry(trade.getId(), XmrAddressEntry.Context.OFFER_FUNDING).get().getSubaddressIndex(); + } + + // thaw reserved outputs + if (trade.getSelf().getReserveTxKeyImages() != null) { + trade.getXmrWalletService().thawOutputs(trade.getSelf().getReserveTxKeyImages()); + } + + // attempt creating deposit tx + try { + synchronized (HavenoUtils.getWalletFunctionLock()) { + for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { + try { + depositTx = trade.getXmrWalletService().createDepositTx(trade, reserveExactAmount, subaddressIndex); + } catch (Exception e) { + log.warn("Error creating deposit tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage()); + if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying + } + + // check for timeout + if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating deposit tx, tradeId=" + trade.getShortId()); + if (depositTx != null) break; + } + } + } catch (Exception e) { + + // re-freeze reserved outputs + if (trade.getSelf().getReserveTxKeyImages() != null) { + trade.getXmrWalletService().freezeOutputs(trade.getSelf().getReserveTxKeyImages()); + } + + throw e; + } + + + // reset protocol timeout + trade.getProtocol().startTimeout(TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS); + + // collect reserved key images + List reservedKeyImages = new ArrayList(); + for (MoneroOutput input : depositTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex()); + + // update trade state + BigInteger securityDeposit = trade instanceof BuyerTrade ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee(); + trade.getSelf().setSecurityDeposit(securityDeposit.subtract(depositTx.getFee())); + trade.getSelf().setDepositTx(depositTx); + trade.getSelf().setDepositTxHash(depositTx.getHash()); + trade.getSelf().setDepositTxFee(depositTx.getFee()); + trade.getSelf().setReserveTxKeyImages(reservedKeyImages); + trade.getSelf().setPayoutAddressString(trade.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString()); // TODO (woodser): allow custom payout address? + trade.getSelf().setPaymentAccountPayload(trade.getProcessModel().getPaymentAccountPayload(trade.getSelf().getPaymentAccountId())); } - MoneroTxWallet depositTx = trade.getXmrWalletService().createDepositTx(trade, reserveExactAmount, subaddressIndex); - - // check if trade still exists - if (!processModel.getTradeManager().hasOpenTrade(trade)) { - throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getId()); - } - - // reset protocol timeout - trade.getProtocol().startTimeout(TradeProtocol.TRADE_TIMEOUT_SECONDS); - - // collect reserved key images - List reservedKeyImages = new ArrayList(); - for (MoneroOutput input : depositTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex()); - - // save process state - trade.getSelf().setDepositTx(depositTx); - trade.getSelf().setDepositTxHash(depositTx.getHash()); - trade.getSelf().setDepositTxFee(depositTx.getFee()); - trade.getSelf().setReserveTxKeyImages(reservedKeyImages); - trade.getSelf().setPayoutAddressString(trade.getXmrWalletService().getOrCreateAddressEntry(processModel.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString()); // TODO (woodser): allow custom payout address? - trade.getSelf().setPaymentAccountPayload(trade.getProcessModel().getPaymentAccountPayload(trade.getSelf().getPaymentAccountId())); - - // TODO: security deposit should be based on trade amount, not max offer amount - BigInteger securityDeposit = trade instanceof BuyerTrade ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee(); - trade.getSelf().setSecurityDeposit(securityDeposit.subtract(depositTx.getFee())); // maker signs deposit hash nonce to avoid challenge protocol byte[] sig = null; @@ -170,4 +204,8 @@ public class MaybeSendSignContractRequest extends TradeTask { processModel.getTradeManager().requestPersistence(); complete(); } + + private boolean isTimedOut() { + return !processModel.getTradeManager().hasOpenTrade(trade); + } } 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 94470ec9..9673a565 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 @@ -18,6 +18,7 @@ package haveno.core.trade.protocol.tasks; +import haveno.common.ThreadUtils; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; import haveno.core.trade.messages.DepositsConfirmedMessage; @@ -53,25 +54,26 @@ public class ProcessDepositsConfirmedMessage extends TradeTask { if (sender.getNodeAddress().equals(trade.getSeller().getNodeAddress()) && sender != trade.getSeller()) trade.getSeller().setNodeAddress(null); if (sender.getNodeAddress().equals(trade.getArbitrator().getNodeAddress()) && sender != trade.getArbitrator()) trade.getArbitrator().setNodeAddress(null); - // update multisig hex - sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex()); - // decrypt seller payment account payload if key given if (request.getSellerPaymentAccountKey() != null && trade.getTradePeer().getPaymentAccountPayload() == null) { log.info(trade.getClass().getSimpleName() + " decrypting using seller payment account key"); trade.decryptPeerPaymentAccountPayload(request.getSellerPaymentAccountKey()); } - // persist - processModel.getTradeManager().requestPersistence(); + // update multisig hex + sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex()); // try to import multisig hex (retry later) - try { - trade.importMultisigHex(); - } catch (Exception e) { - e.printStackTrace(); - } + ThreadUtils.submitToPool(() -> { + try { + trade.importMultisigHex(); + } catch (Exception e) { + e.printStackTrace(); + } + }); + // persist + processModel.getTradeManager().requestPersistence(); complete(); } catch (Throwable t) { failed(t); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java index 1d7ca555..4c8182f0 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 @@ -34,7 +34,6 @@ package haveno.core.trade.protocol.tasks; -import common.utils.GenUtils; import haveno.common.taskrunner.TaskRunner; import haveno.core.account.sign.SignedWitness; import haveno.core.support.dispute.Dispute; @@ -145,7 +144,7 @@ public class ProcessPaymentReceivedMessage extends TradeTask { log.info("Deferring signing and publishing payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId()); for (int i = 0; i < 5; i++) { if (trade.isPayoutPublished()) break; - GenUtils.waitFor(Trade.DEFER_PUBLISH_MS / 5); + HavenoUtils.waitFor(Trade.DEFER_PUBLISH_MS / 5); } if (!trade.isPayoutPublished()) trade.syncAndPollWallet(); } 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 467ff063..1793e7db 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 @@ -60,15 +60,6 @@ public class ProcessPaymentSentMessage extends TradeTask { // if seller, decrypt buyer's payment account payload if (trade.isSeller()) trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey()); trade.requestPersistence(); - - // try to import multisig hex off main thread (retry later) - new Thread(() -> { - try { - trade.importMultisigHex(); - } catch (Exception e) { - e.printStackTrace(); - } - }).start(); // update state trade.advanceState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java index fa747e14..73911506 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java @@ -37,7 +37,7 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask { runInterceptHook(); // check connection - trade.checkAndVerifyDaemonConnection(); + trade.verifyDaemonConnection(); // handle first time preparation if (trade.getArbitrator().getPaymentReceivedMessage() == null) { diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessSignContractResponse.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositRequest.java similarity index 88% rename from core/src/main/java/haveno/core/trade/protocol/tasks/ProcessSignContractResponse.java rename to core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositRequest.java index a2d71292..ccf54991 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessSignContractResponse.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositRequest.java @@ -29,14 +29,16 @@ import haveno.core.trade.protocol.TradePeer; import haveno.network.p2p.SendDirectMessageListener; import lombok.extern.slf4j.Slf4j; +import java.util.ArrayList; import java.util.Date; +import java.util.List; import java.util.UUID; @Slf4j -public class ProcessSignContractResponse extends TradeTask { +public class SendDepositRequest extends TradeTask { @SuppressWarnings({"unused"}) - public ProcessSignContractResponse(TaskRunner taskHandler, Trade trade) { + public SendDepositRequest(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @@ -107,7 +109,11 @@ public class ProcessSignContractResponse extends TradeTask { } }); } else { - log.info("Waiting for another contract signature to send deposit request"); + List awaitingSignaturesFrom = new ArrayList<>(); + if (processModel.getArbitrator().getContractSignature() == null) awaitingSignaturesFrom.add("arbitrator"); + if (processModel.getMaker().getContractSignature() == null) awaitingSignaturesFrom.add("maker"); + if (processModel.getTaker().getContractSignature() == null) awaitingSignaturesFrom.add("taker"); + log.info("Waiting for contract signature from {} to send deposit request", awaitingSignaturesFrom); complete(); // does not yet have needed signatures } } catch (Throwable t) { 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 1746556a..f3a47d28 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 @@ -23,6 +23,8 @@ import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.trade.protocol.TradeProtocol; import haveno.core.xmr.model.XmrAddressEntry; +import haveno.core.xmr.wallet.XmrWalletService; +import lombok.extern.slf4j.Slf4j; import monero.daemon.model.MoneroOutput; import monero.wallet.model.MoneroTxWallet; @@ -30,6 +32,7 @@ import java.math.BigInteger; import java.util.ArrayList; import java.util.List; +@Slf4j public class TakerReserveTradeFunds extends TradeTask { public TakerReserveTradeFunds(TaskRunner taskHandler, Trade trade) { @@ -42,28 +45,49 @@ public class TakerReserveTradeFunds extends TradeTask { runInterceptHook(); // create reserve tx - BigInteger penaltyFee = HavenoUtils.multiply(trade.getAmount(), trade.getOffer().getPenaltyFeePct()); - BigInteger takerFee = trade.getTakerFee(); - BigInteger sendAmount = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getAmount() : BigInteger.ZERO; - BigInteger securityDeposit = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getSellerSecurityDepositBeforeMiningFee() : trade.getBuyerSecurityDepositBeforeMiningFee(); - String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); - MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, takerFee, sendAmount, securityDeposit, returnAddress, false, null); + MoneroTxWallet reserveTx = null; + synchronized (XmrWalletService.WALLET_LOCK) { - // check if trade still exists - if (!processModel.getTradeManager().hasOpenTrade(trade)) { - throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getId()); + // check for timeout + if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId()); + + // collect relevant info + BigInteger penaltyFee = HavenoUtils.multiply(trade.getAmount(), trade.getOffer().getPenaltyFeePct()); + BigInteger takerFee = trade.getTakerFee(); + BigInteger sendAmount = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getAmount() : BigInteger.ZERO; + BigInteger securityDeposit = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getSellerSecurityDepositBeforeMiningFee() : trade.getBuyerSecurityDepositBeforeMiningFee(); + String returnAddress = trade.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); + + // attempt creating reserve tx + synchronized (HavenoUtils.getWalletFunctionLock()) { + for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { + try { + reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, takerFee, sendAmount, securityDeposit, returnAddress, false, null); + } catch (Exception e) { + log.warn("Error creating reserve tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage()); + if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying + } + + // check for timeout + if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId()); + if (reserveTx != null) break; + } + } + + // reset protocol timeout + trade.getProtocol().startTimeout(TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS); + + // collect reserved key images + List reservedKeyImages = new ArrayList(); + for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex()); + + // update trade state + trade.getTaker().setReserveTxKeyImages(reservedKeyImages); } - // collect reserved key images - List reservedKeyImages = new ArrayList(); - for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex()); - - // reset protocol timeout - trade.getProtocol().startTimeout(TradeProtocol.TRADE_TIMEOUT_SECONDS); - // save process state processModel.setReserveTx(reserveTx); - processModel.getTaker().setReserveTxKeyImages(reservedKeyImages); processModel.getTradeManager().requestPersistence(); trade.addInitProgressStep(); complete(); @@ -74,4 +98,8 @@ public class TakerReserveTradeFunds extends TradeTask { failed(t); } } + + private boolean isTimedOut() { + return !processModel.getTradeManager().hasOpenTrade(trade); + } } 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 1a7c0bcb..9f154931 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -22,7 +22,6 @@ import com.google.common.util.concurrent.Service.State; import com.google.inject.Inject; import com.google.inject.name.Named; -import common.utils.GenUtils; import common.utils.JsonUtils; import haveno.common.ThreadUtils; import haveno.common.UserThread; @@ -33,8 +32,6 @@ import haveno.common.util.Utilities; import haveno.core.api.AccountServiceListener; import haveno.core.api.CoreAccountService; import haveno.core.api.XmrConnectionService; -import haveno.core.offer.Offer; -import haveno.core.offer.OfferDirection; import haveno.core.offer.OpenOffer; import haveno.core.trade.BuyerTrade; import haveno.core.trade.HavenoUtils; @@ -131,10 +128,10 @@ public class XmrWalletService { private static final int NUM_MAX_WALLET_BACKUPS = 1; private static final int MONERO_LOG_LEVEL = -1; // monero library log level, -1 to disable private static final int MAX_SYNC_ATTEMPTS = 3; - private static final boolean PRINT_STACK_TRACE = false; + private static final boolean PRINT_RPC_STACK_TRACE = false; private static final String THREAD_ID = XmrWalletService.class.getSimpleName(); private static final long SHUTDOWN_TIMEOUT_MS = 60000; - private static final long NUM_BLOCKS_BEHIND_WARNING = 10; + private static final long NUM_BLOCKS_BEHIND_WARNING = 5; private final User user; private final Preferences preferences; @@ -155,7 +152,7 @@ public class XmrWalletService { private ChangeListener walletInitListener; private TradeManager tradeManager; private MoneroWallet wallet; - private Object walletLock = new Object(); + public static final Object WALLET_LOCK = new Object(); private boolean wasWalletSynced = false; private final Map> txCache = new HashMap>(); private boolean isClosingWallet = false; @@ -374,11 +371,21 @@ public class XmrWalletService { return useNativeXmrWallet && MoneroUtils.isNativeLibraryLoaded(); } + public MoneroSyncResult syncWallet() { + MoneroSyncResult result = syncWallet(wallet); + walletHeight.set(wallet.getHeight()); + return result; + } + /** * Sync the given wallet in a thread pool with other wallets. */ public MoneroSyncResult syncWallet(MoneroWallet wallet) { - Callable task = () -> wallet.sync(); + Callable task = () -> { + synchronized (HavenoUtils.getDaemonLock()) { + return wallet.sync(); + } + }; Future future = syncWalletThreadPool.submit(task); try { return future.get(); @@ -448,24 +455,26 @@ public class XmrWalletService { if (name.contains(File.separator)) throw new IllegalArgumentException("Path not expected: " + name); } - public MoneroTxWallet createTx(List destinations) { - synchronized (walletLock) { - try { - MoneroTxWallet tx = wallet.createTx(new MoneroTxConfig().setAccountIndex(0).setDestinations(destinations).setRelay(false).setCanSplit(false)); - //printTxs("XmrWalletService.createTx", tx); - requestSaveMainWallet(); - return tx; - } catch (Exception e) { - throw e; + public MoneroTxWallet createTx(MoneroTxConfig txConfig) { + synchronized (WALLET_LOCK) { + synchronized (HavenoUtils.getWalletFunctionLock()) { + return wallet.createTx(txConfig); } } } + public MoneroTxWallet createTx(List destinations) { + MoneroTxWallet tx = createTx(new MoneroTxConfig().setAccountIndex(0).setDestinations(destinations).setRelay(false).setCanSplit(false));; + //printTxs("XmrWalletService.createTx", tx); + requestSaveMainWallet(); + return tx; + } + /** * Thaw all outputs not reserved for a trade. */ public void thawUnreservedOutputs() { - synchronized (walletLock) { + synchronized (WALLET_LOCK) { // collect reserved outputs Set reservedKeyImages = new HashSet(); @@ -505,26 +514,25 @@ public class XmrWalletService { * @param keyImages the key images to freeze */ public void freezeOutputs(Collection keyImages) { - synchronized (walletLock) { + synchronized (WALLET_LOCK) { for (String keyImage : keyImages) wallet.freezeOutput(keyImage); + cacheWalletInfo(); requestSaveMainWallet(); - doPollWallet(false); } - updateBalanceListeners(); // TODO (monero-java): balance listeners not notified on freeze/thaw output } /** * Thaw the given outputs with a lock on the wallet. * - * @param keyImages the key images to thaw + * @param keyImages the key images to thaw (ignored if null or empty) */ public void thawOutputs(Collection keyImages) { - synchronized (walletLock) { + if (keyImages == null || keyImages.isEmpty()) return; + synchronized (WALLET_LOCK) { for (String keyImage : keyImages) wallet.thawOutput(keyImage); + cacheWalletInfo(); requestSaveMainWallet(); - doPollWallet(false); } - updateBalanceListeners(); // TODO (monero-java): balance listeners not notified on freeze/thaw output } private List getSubaddressesWithExactInput(BigInteger amount) { @@ -542,40 +550,6 @@ public class XmrWalletService { return new ArrayList(subaddressIndices); } - /** - * Create a reserve tx for an open offer and freeze its inputs. - * - * @param openOffer is the open offer to create a reserve tx for - */ - public MoneroTxWallet createReserveTx(OpenOffer openOffer) { - synchronized (walletLock) { - - // collect offer data - Offer offer = openOffer.getOffer(); - BigInteger penaltyFee = HavenoUtils.multiply(offer.getAmount(), offer.getPenaltyFeePct()); - BigInteger makerFee = offer.getMaxMakerFee(); - BigInteger sendAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.ZERO : offer.getAmount(); - BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit(); - String returnAddress = getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); - XmrAddressEntry fundingEntry = getAddressEntry(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null); - Integer preferredSubaddressIndex = fundingEntry == null ? null : fundingEntry.getSubaddressIndex(); - - // create reserve tx - MoneroTxWallet reserveTx = createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex); - - // collect reserved key images - List reservedKeyImages = new ArrayList(); - for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex()); - - // save offer state - openOffer.setReserveTxHash(reserveTx.getHash()); - openOffer.setReserveTxHex(reserveTx.getFullHex()); - openOffer.setReserveTxKey(reserveTx.getKey()); - offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages); - return reserveTx; - } - } - /** * Create the reserve tx and freeze its inputs. The full amount is returned * to the sender's payout address less the penalty and mining fees. @@ -587,14 +561,18 @@ public class XmrWalletService { * @param returnAddress return address for reserved funds * @param reserveExactAmount specifies to reserve the exact input amount * @param preferredSubaddressIndex preferred source subaddress to spend from (optional) - * @return a transaction to reserve a trade + * @return the reserve tx */ public MoneroTxWallet createReserveTx(BigInteger penaltyFee, BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String returnAddress, boolean reserveExactAmount, Integer preferredSubaddressIndex) { - log.info("Creating reserve tx with preferred subaddress index={}, return address={}", preferredSubaddressIndex, returnAddress); - long time = System.currentTimeMillis(); - MoneroTxWallet reserveTx = createTradeTx(penaltyFee, tradeFee, sendAmount, securityDeposit, returnAddress, reserveExactAmount, preferredSubaddressIndex); - log.info("Done creating reserve tx in {} ms", System.currentTimeMillis() - time); - return reserveTx; + synchronized (WALLET_LOCK) { + synchronized (HavenoUtils.getWalletFunctionLock()) { + log.info("Creating reserve tx with preferred subaddress index={}, return address={}", preferredSubaddressIndex, returnAddress); + long time = System.currentTimeMillis(); + MoneroTxWallet reserveTx = createTradeTx(penaltyFee, tradeFee, sendAmount, securityDeposit, returnAddress, reserveExactAmount, preferredSubaddressIndex); + log.info("Done creating reserve tx in {} ms", System.currentTimeMillis() - time); + return reserveTx; + } + } } /** @@ -606,28 +584,23 @@ public class XmrWalletService { * @return MoneroTxWallet the multisig deposit tx */ public MoneroTxWallet createDepositTx(Trade trade, boolean reserveExactAmount, Integer preferredSubaddressIndex) { - synchronized (walletLock) { - - // thaw reserved outputs - if (trade.getSelf().getReserveTxKeyImages() != null) { - thawOutputs(trade.getSelf().getReserveTxKeyImages()); + synchronized (WALLET_LOCK) { + synchronized (HavenoUtils.getWalletFunctionLock()) { + String multisigAddress = trade.getProcessModel().getMultisigAddress(); + BigInteger tradeFee = trade instanceof MakerTrade ? trade.getMakerFee() : trade.getTakerFee(); + BigInteger sendAmount = trade instanceof BuyerTrade ? BigInteger.ZERO : trade.getAmount(); + BigInteger securityDeposit = trade instanceof BuyerTrade ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee(); + long time = System.currentTimeMillis(); + log.info("Creating deposit tx for trade {} {} with multisig address={}", trade.getClass().getSimpleName(), trade.getShortId(), multisigAddress); + MoneroTxWallet depositTx = createTradeTx(null, tradeFee, sendAmount, securityDeposit, multisigAddress, reserveExactAmount, preferredSubaddressIndex); + log.info("Done creating deposit tx for trade {} {} in {} ms", trade.getClass().getSimpleName(), trade.getShortId(), System.currentTimeMillis() - time); + return depositTx; } - - // create deposit tx - String multisigAddress = trade.getProcessModel().getMultisigAddress(); - BigInteger tradeFee = trade instanceof MakerTrade ? trade.getMakerFee() : trade.getTakerFee(); - BigInteger sendAmount = trade instanceof BuyerTrade ? BigInteger.ZERO : trade.getAmount(); - BigInteger securityDeposit = trade instanceof BuyerTrade ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee(); - long time = System.currentTimeMillis(); - log.info("Creating deposit tx with multisig address={}", multisigAddress); - MoneroTxWallet depositTx = createTradeTx(null, tradeFee, sendAmount, securityDeposit, multisigAddress, reserveExactAmount, preferredSubaddressIndex); - log.info("Done creating deposit tx for trade {} {} in {} ms", trade.getClass().getSimpleName(), trade.getId(), System.currentTimeMillis() - time); - return depositTx; } } private MoneroTxWallet createTradeTx(BigInteger penaltyFee, BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String address, boolean reserveExactAmount, Integer preferredSubaddressIndex) { - synchronized (walletLock) { + synchronized (WALLET_LOCK) { MoneroWallet wallet = getWallet(); // create a list of subaddresses to attempt spending from in preferred order @@ -675,7 +648,7 @@ public class XmrWalletService { .setSubtractFeeFrom(0) // pay fee from transfer amount .setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY); if (!BigInteger.valueOf(0).equals(feeAmount)) txConfig.addDestination(HavenoUtils.getTradeFeeAddress(), feeAmount); - MoneroTxWallet tradeTx = wallet.createTx(txConfig); + MoneroTxWallet tradeTx = createTx(txConfig); // freeze inputs List keyImages = new ArrayList(); @@ -872,7 +845,7 @@ public class XmrWalletService { Runnable shutDownTask = () -> { // remove listeners - synchronized (walletLock) { + synchronized (WALLET_LOCK) { if (wallet != null) { for (MoneroWalletListenerI listener : new HashSet<>(wallet.getListeners())) { wallet.removeListener(listener); @@ -1174,7 +1147,7 @@ public class XmrWalletService { } public List getTxs() { - return getTxs(new MoneroTxQuery()); + return getTxs(new MoneroTxQuery().setIncludeOutputs(true)); } public List getTxs(MoneroTxQuery query) { @@ -1242,7 +1215,7 @@ public class XmrWalletService { // force restart main wallet if connection changed before synced if (!wasWalletSynced) { - if (!Boolean.TRUE.equals(connection.isConnected())) return; + if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) return; ThreadUtils.submitToPool(() -> { log.warn("Force restarting main wallet because connection changed before inital sync"); forceRestartMainWallet(); @@ -1275,7 +1248,7 @@ public class XmrWalletService { private void initMainWalletIfConnected() { ThreadUtils.execute(() -> { - synchronized (walletLock) { + synchronized (WALLET_LOCK) { if (wallet == null && xmrConnectionService.downloadPercentageProperty().get() == 1 && !isShutDownStarted) { maybeInitMainWallet(true); if (walletInitListener != null) xmrConnectionService.downloadPercentageProperty().removeListener(walletInitListener); @@ -1295,7 +1268,7 @@ public class XmrWalletService { } private void maybeInitMainWallet(boolean sync, int numAttempts) { - synchronized (walletLock) { + synchronized (WALLET_LOCK) { if (isShutDownStarted) return; // open or create wallet main wallet @@ -1304,7 +1277,7 @@ public class XmrWalletService { log.info("Initializing main wallet with monerod=" + (daemon == null ? "null" : daemon.getRpcConnection().getUri())); if (MoneroUtils.walletExists(xmrWalletFile.getPath())) { wallet = openWallet(MONERO_WALLET_NAME, rpcBindPort, isProxyApplied(wasWalletSynced)); - } else if (xmrConnectionService.getConnection() != null && Boolean.TRUE.equals(xmrConnectionService.getConnection().isConnected())) { + } else if (Boolean.TRUE.equals(xmrConnectionService.isConnected())) { wallet = createWallet(MONERO_WALLET_NAME, rpcBindPort); // set wallet creation date to yesterday to guarantee complete restore @@ -1393,7 +1366,7 @@ public class XmrWalletService { // get sync notifications from native wallet if (wallet instanceof MoneroWalletFull) { - if (runReconnectTestOnStartup) GenUtils.waitFor(1000); // delay sync to test + if (runReconnectTestOnStartup) HavenoUtils.waitFor(1000); // delay sync to test wallet.sync(new MoneroWalletListener() { @Override public void onSyncProgress(long height, long startHeight, long endHeight, double percentDone, String message) { @@ -1419,11 +1392,6 @@ public class XmrWalletService { if (height < xmrConnectionService.getTargetHeight()) updateSyncProgress(height); else { syncWithProgressLooper.stop(); - try { - doPollWallet(true); - } catch (Exception e) { - e.printStackTrace(); - } wasWalletSynced = true; updateSyncProgress(height); syncWithProgressLatch.countDown(); @@ -1465,19 +1433,19 @@ public class XmrWalletService { private MoneroWalletFull createWalletFull(MoneroWalletConfig config) { // must be connected to daemon - MoneroRpcConnection connection = xmrConnectionService.getConnection(); - if (connection == null || !Boolean.TRUE.equals(connection.isConnected())) throw new RuntimeException("Must be connected to daemon before creating wallet"); + if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) throw new RuntimeException("Must be connected to daemon before creating wallet"); // create wallet MoneroWalletFull walletFull = null; try { // create wallet + MoneroRpcConnection connection = xmrConnectionService.getConnection(); log.info("Creating full wallet " + config.getPath() + " connected to monerod=" + connection.getUri()); long time = System.currentTimeMillis(); config.setServer(connection); walletFull = MoneroWalletFull.createWallet(config); - walletFull.getDaemonConnection().setPrintStackTrace(PRINT_STACK_TRACE); + walletFull.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE); log.info("Done creating full wallet " + config.getPath() + " in " + (System.currentTimeMillis() - time) + " ms"); return walletFull; } catch (Exception e) { @@ -1499,7 +1467,7 @@ public class XmrWalletService { config.setNetworkType(getMoneroNetworkType()); config.setServer(connection); walletFull = MoneroWalletFull.openWallet(config); - if (walletFull.getDaemonConnection() != null) walletFull.getDaemonConnection().setPrintStackTrace(PRINT_STACK_TRACE); + if (walletFull.getDaemonConnection() != null) walletFull.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE); log.info("Done opening full wallet " + config.getPath()); return walletFull; } catch (Exception e) { @@ -1512,8 +1480,7 @@ public class XmrWalletService { private MoneroWalletRpc createWalletRpc(MoneroWalletConfig config, Integer port) { // must be connected to daemon - MoneroRpcConnection connection = xmrConnectionService.getConnection(); - if (connection == null || !Boolean.TRUE.equals(connection.isConnected())) throw new RuntimeException("Must be connected to daemon before creating wallet"); + if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) throw new RuntimeException("Must be connected to daemon before creating wallet"); // create wallet MoneroWalletRpc walletRpc = null; @@ -1521,17 +1488,18 @@ public class XmrWalletService { // start monero-wallet-rpc instance walletRpc = startWalletRpcInstance(port, isProxyApplied(false)); - walletRpc.getRpcConnection().setPrintStackTrace(PRINT_STACK_TRACE); + walletRpc.getRpcConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE); // prevent wallet rpc from syncing walletRpc.stopSyncing(); // create wallet + MoneroRpcConnection connection = xmrConnectionService.getConnection(); log.info("Creating RPC wallet " + config.getPath() + " connected to monerod=" + connection.getUri()); long time = System.currentTimeMillis(); config.setServer(connection); walletRpc.createWallet(config); - walletRpc.getDaemonConnection().setPrintStackTrace(PRINT_STACK_TRACE); + walletRpc.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE); log.info("Done creating RPC wallet " + config.getPath() + " in " + (System.currentTimeMillis() - time) + " ms"); return walletRpc; } catch (Exception e) { @@ -1547,7 +1515,7 @@ public class XmrWalletService { // start monero-wallet-rpc instance walletRpc = startWalletRpcInstance(port, applyProxyUri); - walletRpc.getRpcConnection().setPrintStackTrace(PRINT_STACK_TRACE); + walletRpc.getRpcConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE); // prevent wallet rpc from syncing walletRpc.stopSyncing(); @@ -1560,7 +1528,7 @@ public class XmrWalletService { log.info("Opening RPC wallet " + config.getPath() + " connected to daemon " + connection.getUri()); config.setServer(connection); walletRpc.openWallet(config); - if (walletRpc.getDaemonConnection() != null) walletRpc.getDaemonConnection().setPrintStackTrace(PRINT_STACK_TRACE); + if (walletRpc.getDaemonConnection() != null) walletRpc.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE); log.info("Done opening RPC wallet " + config.getPath()); return walletRpc; } catch (Exception e) { @@ -1613,7 +1581,7 @@ public class XmrWalletService { } private void onConnectionChanged(MoneroRpcConnection connection) { - synchronized (walletLock) { + synchronized (WALLET_LOCK) { if (wallet == null || isShutDownStarted) return; if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) return; String oldProxyUri = wallet == null || wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getProxyUri(); @@ -1634,7 +1602,7 @@ public class XmrWalletService { // sync wallet on new thread if (connection != null && !isShutDownStarted) { - wallet.getDaemonConnection().setPrintStackTrace(PRINT_STACK_TRACE); + wallet.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE); updatePollPeriod(); } @@ -1673,7 +1641,7 @@ public class XmrWalletService { private void closeMainWallet(boolean save) { stopPolling(); - synchronized (walletLock) { + synchronized (WALLET_LOCK) { try { if (wallet != null) { isClosingWallet = true; @@ -1697,13 +1665,13 @@ public class XmrWalletService { private void forceRestartMainWallet() { log.warn("Force restarting main wallet"); forceCloseMainWallet(); - synchronized (walletLock) { + synchronized (WALLET_LOCK) { maybeInitMainWallet(true); } } private void startPolling() { - synchronized (walletLock) { + synchronized (WALLET_LOCK) { if (isShutDownStarted || isPollInProgress()) return; log.info("Starting to poll main wallet"); updatePollPeriod(); @@ -1733,7 +1701,7 @@ public class XmrWalletService { } private void setPollPeriod(long pollPeriodMs) { - synchronized (walletLock) { + synchronized (WALLET_LOCK) { if (this.isShutDownStarted) return; if (this.pollPeriodMs != null && this.pollPeriodMs == pollPeriodMs) return; this.pollPeriodMs = pollPeriodMs; @@ -1751,71 +1719,51 @@ public class XmrWalletService { private void doPollWallet(boolean updateTxs) { synchronized (pollLock) { + if (isShutDownStarted) return; pollInProgress = true; try { - // log warning if wallet is too far behind daemon + // switch to best connection if daemon is too far behind MoneroDaemonInfo lastInfo = xmrConnectionService.getLastInfo(); if (lastInfo == null) { log.warn("Last daemon info is null"); return; } - long walletHeight = wallet.getHeight(); - if (wasWalletSynced && walletHeight < xmrConnectionService.getTargetHeight() - NUM_BLOCKS_BEHIND_WARNING && !Config.baseCurrencyNetwork().isTestnet()) { - log.warn("Main wallet is {} blocks behind monerod, wallet height={}, monerod height={},", xmrConnectionService.getTargetHeight() - walletHeight, walletHeight, lastInfo.getHeight()); + if (wasWalletSynced && walletHeight.get() < xmrConnectionService.getTargetHeight() - NUM_BLOCKS_BEHIND_WARNING && !Config.baseCurrencyNetwork().isTestnet()) { + log.warn("Updating connection because main wallet is {} blocks behind monerod, wallet height={}, monerod height={}", xmrConnectionService.getTargetHeight() - walletHeight.get(), walletHeight.get(), lastInfo.getHeight()); + xmrConnectionService.switchToBestConnection(); } // sync wallet if behind daemon - if (wallet.getHeight() < xmrConnectionService.getTargetHeight()) wallet.sync(); + if (walletHeight.get() < xmrConnectionService.getTargetHeight()) { + synchronized (WALLET_LOCK) { // avoid long sync from blocking other operations + syncWallet(); + } + } // fetch transactions from pool and store to cache // TODO: ideally wallet should sync every poll and then avoid updating from pool on fetching txs? if (updateTxs) { - try { - cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true)); - } catch (Exception e) { // fetch from pool can fail - log.warn("Error polling main wallet's transactions from the pool: {}", e.getMessage()); + synchronized (WALLET_LOCK) { // avoid long fetch from blocking other operations + synchronized (HavenoUtils.getDaemonLock()) { + try { + cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true)); + } catch (Exception e) { // fetch from pool can fail + if (!isShutDownStarted) log.warn("Error polling main wallet's transactions from the pool: {}", e.getMessage()); + } + } } } - // get basic wallet info - long height = wallet.getHeight(); - BigInteger balance = wallet.getBalance(); - BigInteger unlockedBalance = wallet.getUnlockedBalance(); - cachedSubaddresses = wallet.getSubaddresses(0); - cachedOutputs = wallet.getOutputs(); - - // cache and notify changes - if (cachedHeight == null) { - cachedHeight = height; - cachedBalance = balance; - cachedAvailableBalance = unlockedBalance; - } else { - - // notify listeners of new block - if (height != cachedHeight) { - cachedHeight = height; - onNewBlock(height); - } - - // notify listeners of balance change - if (!balance.equals(cachedBalance) || !unlockedBalance.equals(cachedAvailableBalance)) { - cachedBalance = balance; - cachedAvailableBalance = unlockedBalance; - onBalancesChanged(balance, unlockedBalance); - } - } + // cache wallet info + cacheWalletInfo(); } catch (Exception e) { - if (isShutDownStarted) return; + if (wallet == null || isShutDownStarted) return; boolean isConnectionRefused = e.getMessage() != null && e.getMessage().contains("Connection refused"); - if (isConnectionRefused && wallet != null) forceRestartMainWallet(); - else { - boolean isWalletConnected = isWalletConnectedToDaemon(); - if (!isWalletConnected) xmrConnectionService.checkConnection(); // check connection if wallet is not connected - if (wallet != null && isWalletConnected) { - log.warn("Error polling main wallet, errorMessage={}. Monerod={}", e.getMessage(), getConnectionService().getConnection()); - //e.printStackTrace(); - } + if (isConnectionRefused) forceRestartMainWallet(); + else if (isWalletConnectedToDaemon()) { + log.warn("Error polling main wallet, errorMessage={}. Monerod={}", e.getMessage(), getConnectionService().getConnection()); + //e.printStackTrace(); } } finally { pollInProgress = false; @@ -1824,7 +1772,7 @@ public class XmrWalletService { } public boolean isWalletConnectedToDaemon() { - synchronized (walletLock) { + synchronized (WALLET_LOCK) { try { if (wallet == null) return false; return wallet.isConnectedToDaemon(); @@ -1841,6 +1789,33 @@ public class XmrWalletService { }); } + private void cacheWalletInfo() { + + // get basic wallet info + long height = wallet.getHeight(); + BigInteger balance = wallet.getBalance(); + BigInteger unlockedBalance = wallet.getUnlockedBalance(); + cachedSubaddresses = wallet.getSubaddresses(0); + cachedOutputs = wallet.getOutputs(); + + // cache and notify changes + if (cachedHeight == null) { + cachedHeight = height; + cachedBalance = balance; + cachedAvailableBalance = unlockedBalance; + onNewBlock(height); + onBalancesChanged(balance, unlockedBalance); + } else { + boolean heightChanged = height != cachedHeight; + boolean balancesChanged = !balance.equals(cachedBalance) || !unlockedBalance.equals(cachedAvailableBalance); + cachedHeight = height; + cachedBalance = balance; + cachedAvailableBalance = unlockedBalance; + if (heightChanged) onNewBlock(height); + if (balancesChanged) onBalancesChanged(balance, unlockedBalance); + } + } + private void onBalancesChanged(BigInteger newBalance, BigInteger newUnlockedBalance) { updateBalanceListeners(); for (MoneroWalletListenerI listener : walletListeners) ThreadUtils.submitToPool(() -> listener.onBalancesChanged(newBalance, newUnlockedBalance)); diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcXmrConnectionService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcXmrConnectionService.java index 741704ef..eb1b8f6f 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcXmrConnectionService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcXmrConnectionService.java @@ -57,10 +57,10 @@ import haveno.proto.grpc.SetAutoSwitchReply; import haveno.proto.grpc.SetAutoSwitchRequest; import haveno.proto.grpc.SetConnectionReply; import haveno.proto.grpc.SetConnectionRequest; -import haveno.proto.grpc.StartCheckingConnectionsReply; -import haveno.proto.grpc.StartCheckingConnectionsRequest; -import haveno.proto.grpc.StopCheckingConnectionsReply; -import haveno.proto.grpc.StopCheckingConnectionsRequest; +import haveno.proto.grpc.StartCheckingConnectionReply; +import haveno.proto.grpc.StartCheckingConnectionRequest; +import haveno.proto.grpc.StopCheckingConnectionReply; +import haveno.proto.grpc.StopCheckingConnectionRequest; import haveno.proto.grpc.UrlConnection; import static haveno.proto.grpc.XmrConnectionsGrpc.XmrConnectionsImplBase; import static haveno.proto.grpc.XmrConnectionsGrpc.getAddConnectionMethod; @@ -72,8 +72,8 @@ import static haveno.proto.grpc.XmrConnectionsGrpc.getGetConnectionsMethod; import static haveno.proto.grpc.XmrConnectionsGrpc.getRemoveConnectionMethod; import static haveno.proto.grpc.XmrConnectionsGrpc.getSetAutoSwitchMethod; import static haveno.proto.grpc.XmrConnectionsGrpc.getSetConnectionMethod; -import static haveno.proto.grpc.XmrConnectionsGrpc.getStartCheckingConnectionsMethod; -import static haveno.proto.grpc.XmrConnectionsGrpc.getStopCheckingConnectionsMethod; +import static haveno.proto.grpc.XmrConnectionsGrpc.getStartCheckingConnectionMethod; +import static haveno.proto.grpc.XmrConnectionsGrpc.getStopCheckingConnectionMethod; import io.grpc.ServerInterceptor; import io.grpc.stub.StreamObserver; import java.net.MalformedURLException; @@ -102,7 +102,7 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase { public void addConnection(AddConnectionRequest request, StreamObserver responseObserver) { handleRequest(responseObserver, () -> { - coreApi.addMoneroConnection(toMoneroRpcConnection(request.getConnection())); + coreApi.addXmrConnection(toMoneroRpcConnection(request.getConnection())); return AddConnectionReply.newBuilder().build(); }); } @@ -111,7 +111,7 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase { public void removeConnection(RemoveConnectionRequest request, StreamObserver responseObserver) { handleRequest(responseObserver, () -> { - coreApi.removeMoneroConnection(validateUri(request.getUrl())); + coreApi.removeXmrConnection(validateUri(request.getUrl())); return RemoveConnectionReply.newBuilder().build(); }); } @@ -120,7 +120,7 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase { public void getConnection(GetConnectionRequest request, StreamObserver responseObserver) { handleRequest(responseObserver, () -> { - UrlConnection replyConnection = toUrlConnection(coreApi.getMoneroConnection()); + UrlConnection replyConnection = toUrlConnection(coreApi.getXmrConnection()); GetConnectionReply.Builder builder = GetConnectionReply.newBuilder(); if (replyConnection != null) { builder.setConnection(replyConnection); @@ -145,10 +145,10 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase { StreamObserver responseObserver) { handleRequest(responseObserver, () -> { if (request.getUrl() != null && !request.getUrl().isEmpty()) - coreApi.setMoneroConnection(validateUri(request.getUrl())); + coreApi.setXmrConnection(validateUri(request.getUrl())); else if (request.hasConnection()) - coreApi.setMoneroConnection(toMoneroRpcConnection(request.getConnection())); - else coreApi.setMoneroConnection((MoneroRpcConnection) null); // disconnect from client + coreApi.setXmrConnection(toMoneroRpcConnection(request.getConnection())); + else coreApi.setXmrConnection((MoneroRpcConnection) null); // disconnect from client return SetConnectionReply.newBuilder().build(); }); } @@ -157,7 +157,7 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase { public void checkConnection(CheckConnectionRequest request, StreamObserver responseObserver) { handleRequest(responseObserver, () -> { - MoneroRpcConnection connection = coreApi.checkMoneroConnection(); + MoneroRpcConnection connection = coreApi.checkXmrConnection(); UrlConnection replyConnection = toUrlConnection(connection); CheckConnectionReply.Builder builder = CheckConnectionReply.newBuilder(); if (replyConnection != null) { @@ -179,22 +179,22 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase { } @Override - public void startCheckingConnections(StartCheckingConnectionsRequest request, - StreamObserver responseObserver) { + public void startCheckingConnection(StartCheckingConnectionRequest request, + StreamObserver responseObserver) { handleRequest(responseObserver, () -> { int refreshMillis = request.getRefreshPeriod(); Long refreshPeriod = refreshMillis == 0 ? null : (long) refreshMillis; - coreApi.startCheckingMoneroConnection(refreshPeriod); - return StartCheckingConnectionsReply.newBuilder().build(); + coreApi.startCheckingXmrConnection(refreshPeriod); + return StartCheckingConnectionReply.newBuilder().build(); }); } @Override - public void stopCheckingConnections(StopCheckingConnectionsRequest request, - StreamObserver responseObserver) { + public void stopCheckingConnection(StopCheckingConnectionRequest request, + StreamObserver responseObserver) { handleRequest(responseObserver, () -> { - coreApi.stopCheckingMoneroConnection(); - return StopCheckingConnectionsReply.newBuilder().build(); + coreApi.stopCheckingXmrConnection(); + return StopCheckingConnectionReply.newBuilder().build(); }); } @@ -202,7 +202,7 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase { public void getBestAvailableConnection(GetBestAvailableConnectionRequest request, StreamObserver responseObserver) { handleRequest(responseObserver, () -> { - MoneroRpcConnection connection = coreApi.getBestAvailableMoneroConnection(); + MoneroRpcConnection connection = coreApi.getBestAvailableXmrConnection(); UrlConnection replyConnection = toUrlConnection(connection); GetBestAvailableConnectionReply.Builder builder = GetBestAvailableConnectionReply.newBuilder(); if (replyConnection != null) { @@ -216,7 +216,7 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase { public void setAutoSwitch(SetAutoSwitchRequest request, StreamObserver responseObserver) { handleRequest(responseObserver, () -> { - coreApi.setMoneroConnectionAutoSwitch(request.getAutoSwitch()); + coreApi.setXmrConnectionAutoSwitch(request.getAutoSwitch()); return SetAutoSwitchReply.newBuilder().build(); }); } @@ -300,8 +300,8 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase { put(getSetConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); put(getCheckConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); put(getCheckConnectionsMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); - put(getStartCheckingConnectionsMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); - put(getStopCheckingConnectionsMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); + put(getStartCheckingConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); + put(getStopCheckingConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); put(getGetBestAvailableConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); put(getSetAutoSwitchMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); }} diff --git a/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java b/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java index 8d9a83a8..e7c4c89a 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositView.java @@ -91,6 +91,7 @@ import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.util.Callback; +import monero.common.MoneroUtils; import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroWalletListener; import net.glxn.qrgen.QRCode; @@ -365,7 +366,7 @@ public class DepositView extends ActivatableView { @NotNull private String getPaymentUri() { - return xmrWalletService.getWallet().getPaymentUri(new MoneroTxConfig() + return MoneroUtils.getPaymentUri(new MoneroTxConfig() .setAddress(addressTextField.getAddress()) .setAmount(HavenoUtils.coinToAtomicUnits(getAmount())) .setNote(paymentLabelString)); diff --git a/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java b/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java index 78fe9845..99edbca2 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java @@ -261,7 +261,7 @@ public class WithdrawalView extends ActivatableView { // create tx if (amount.compareTo(BigInteger.ZERO) <= 0) throw new RuntimeException(Res.get("portfolio.pending.step5_buyer.amountTooLow")); - MoneroTxWallet tx = xmrWalletService.getWallet().createTx(new MoneroTxConfig() + MoneroTxWallet tx = xmrWalletService.createTx(new MoneroTxConfig() .setAccountIndex(0) .setAmount(amount) .setAddress(withdrawToAddress) diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java index f30a930c..7dbbd23a 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java @@ -613,10 +613,12 @@ public abstract class MutableOfferViewModel ext if (offer.getState() == Offer.State.OFFER_FEE_RESERVED) errorMessage.set(errMessage + Res.get("createOffer.errorInfo")); else errorMessage.set(errMessage); - updateButtonDisableState(); - updateSpinnerInfo(); + UserThread.execute(() -> { + updateButtonDisableState(); + updateSpinnerInfo(); + resultHandler.run(); - resultHandler.run(); + }); }); updateButtonDisableState(); diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java index 54a9f359..c8531387 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java @@ -21,6 +21,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.inject.Inject; import com.google.inject.name.Named; import haveno.common.ClockWatcher; +import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.network.MessageState; @@ -101,6 +102,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel messageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); private Subscription tradeStateSubscription; + private Subscription paymentAccountDecryptedSubscription; private Subscription payoutStateSubscription; private Subscription messageStateSubscription; @Getter @@ -146,6 +148,11 @@ public class PendingTradesViewModel extends ActivatableWithDataModel { onTradeStateChanged(state); }); + paymentAccountDecryptedSubscription = EasyBind.subscribe(trade.getProcessModel().getPaymentAccountDecryptedProperty(), decrypted -> { + refresh(); + }); payoutStateSubscription = EasyBind.subscribe(trade.payoutStateProperty(), state -> { onPayoutStateChanged(state); }); @@ -191,6 +205,14 @@ public class PendingTradesViewModel extends ActivatableWithDataModel { + sellerState.set(UNDEFINED); + buyerState.set(BuyerState.UNDEFINED); + onTradeStateChanged(trade.getState()); + }); + } + private void onMessageStateChanged(MessageState messageState) { messageStateProperty.set(messageState); } 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 6d426bee..1ee4943d 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 @@ -221,7 +221,7 @@ public class BuyerStep2View extends TradeStepView { 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/desktop/src/main/java/haveno/desktop/util/GUIUtil.java b/desktop/src/main/java/haveno/desktop/util/GUIUtil.java index 5f468fd1..86b7ebfe 100644 --- a/desktop/src/main/java/haveno/desktop/util/GUIUtil.java +++ b/desktop/src/main/java/haveno/desktop/util/GUIUtil.java @@ -93,6 +93,7 @@ import javafx.stage.StageStyle; import javafx.util.Callback; import javafx.util.StringConverter; import lombok.extern.slf4j.Slf4j; +import monero.common.MoneroUtils; import monero.daemon.model.MoneroTx; import monero.wallet.MoneroWallet; import monero.wallet.model.MoneroTxConfig; @@ -686,7 +687,7 @@ public class GUIUtil { } public static String getMoneroURI(String address, BigInteger amount, String label, MoneroWallet wallet) { - return wallet.getPaymentUri(new MoneroTxConfig() + return MoneroUtils.getPaymentUri(new MoneroTxConfig() .setAddress(address) .setAmount(amount) .setNote(label)); diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index 96437dce..c3dcf498 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -315,9 +315,9 @@ service XmrConnections { } rpc CheckConnections(CheckConnectionsRequest) returns (CheckConnectionsReply) { } - rpc StartCheckingConnections(StartCheckingConnectionsRequest) returns (StartCheckingConnectionsReply) { + rpc StartCheckingConnection(StartCheckingConnectionRequest) returns (StartCheckingConnectionReply) { } - rpc StopCheckingConnections(StopCheckingConnectionsRequest) returns (StopCheckingConnectionsReply) { + rpc StopCheckingConnection(StopCheckingConnectionRequest) returns (StopCheckingConnectionReply) { } rpc GetBestAvailableConnection(GetBestAvailableConnectionRequest) returns (GetBestAvailableConnectionReply) { } @@ -388,15 +388,15 @@ message CheckConnectionsReply { repeated UrlConnection connections = 1; } -message StartCheckingConnectionsRequest { +message StartCheckingConnectionRequest { int32 refresh_period = 1; // milliseconds } -message StartCheckingConnectionsReply {} +message StartCheckingConnectionReply {} -message StopCheckingConnectionsRequest {} +message StopCheckingConnectionRequest {} -message StopCheckingConnectionsReply {} +message StopCheckingConnectionReply {} message GetBestAvailableConnectionRequest {}