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 {}