diff --git a/Makefile b/Makefile index 8d36b2b4..2d411185 100644 --- a/Makefile +++ b/Makefile @@ -210,6 +210,15 @@ monerod-stagenet: --bootstrap-daemon-address auto \ --rpc-access-control-origins http://localhost:8080 \ +monerod-stagenet-custom: + ./.localnet/monerod \ + --stagenet \ + --no-zmq \ + --p2p-bind-port 39080 \ + --rpc-bind-port 39081 \ + --bootstrap-daemon-address auto \ + --rpc-access-control-origins http://localhost:8080 \ + seednode-stagenet: ./haveno-seednode$(APP_EXT) \ --baseCurrencyNetwork=XMR_STAGENET \ diff --git a/core/src/main/java/haveno/core/api/CoreAccountService.java b/core/src/main/java/haveno/core/api/CoreAccountService.java index 4259e294..7f24c06c 100644 --- a/core/src/main/java/haveno/core/api/CoreAccountService.java +++ b/core/src/main/java/haveno/core/api/CoreAccountService.java @@ -98,7 +98,9 @@ public class CoreAccountService { if (accountExists()) throw new IllegalStateException("Cannot create account if account already exists"); keyRing.generateKeys(password); this.password = password; - for (AccountServiceListener listener : new ArrayList(listeners)) listener.onAccountCreated(); + synchronized (listeners) { + for (AccountServiceListener listener : new ArrayList<>(listeners)) listener.onAccountCreated(); + } } public void openAccount(String password) throws IncorrectPasswordException { @@ -106,7 +108,7 @@ public class CoreAccountService { if (keyRing.unlockKeys(password, false)) { this.password = password; synchronized (listeners) { - for (AccountServiceListener listener : listeners) listener.onAccountOpened(); + for (AccountServiceListener listener : new ArrayList<>(listeners)) listener.onAccountOpened(); } } else { throw new IllegalStateException("keyRing.unlockKeys() returned false, that should never happen"); @@ -121,7 +123,7 @@ public class CoreAccountService { keyStorage.saveKeyRing(keyRing, oldPassword, newPassword); this.password = newPassword; synchronized (listeners) { - for (AccountServiceListener listener : listeners) listener.onPasswordChanged(oldPassword, newPassword); + for (AccountServiceListener listener : new ArrayList<>(listeners)) listener.onPasswordChanged(oldPassword, newPassword); } } @@ -135,7 +137,7 @@ public class CoreAccountService { if (!isAccountOpen()) throw new IllegalStateException("Cannot close unopened account"); keyRing.lockKeys(); // closed account means the keys are locked synchronized (listeners) { - for (AccountServiceListener listener : listeners) listener.onAccountClosed(); + for (AccountServiceListener listener : new ArrayList<>(listeners)) listener.onAccountClosed(); } } @@ -168,7 +170,7 @@ public class CoreAccountService { File dataDir = new File(config.appDataDir.getPath()); ZipUtils.unzipToDir(dataDir, inputStream, bufferSize); synchronized (listeners) { - for (AccountServiceListener listener : listeners) listener.onAccountRestored(onShutdown); + for (AccountServiceListener listener : new ArrayList<>(listeners)) listener.onAccountRestored(onShutdown); } } @@ -176,7 +178,7 @@ public class CoreAccountService { try { if (isAccountOpen()) closeAccount(); synchronized (listeners) { - for (AccountServiceListener listener : listeners) listener.onAccountDeleted(onShutdown); + for (AccountServiceListener listener : new ArrayList<>(listeners)) listener.onAccountDeleted(onShutdown); } File dataDir = new File(config.appDataDir.getPath()); // TODO (woodser): deleting directory after gracefulShutdown() so services don't throw when they try to persist (e.g. XmrTxProofService), but gracefulShutdown() should honor read-only shutdown FileUtil.deleteDirectory(dataDir, null, false); diff --git a/core/src/main/java/haveno/core/api/CoreDisputeAgentsService.java b/core/src/main/java/haveno/core/api/CoreDisputeAgentsService.java index d0f798f4..0e46a2f2 100644 --- a/core/src/main/java/haveno/core/api/CoreDisputeAgentsService.java +++ b/core/src/main/java/haveno/core/api/CoreDisputeAgentsService.java @@ -171,7 +171,6 @@ class CoreDisputeAgentsService { ErrorMessageHandler errorMessageHandler) { Arbitrator arbitrator = new Arbitrator( p2PService.getAddress(), - xmrWalletService.getWallet().getPrimaryAddress(), // TODO: how is this used? keyRing.getPubKeyRing(), new ArrayList<>(languageCodes), new Date().getTime(), diff --git a/core/src/main/java/haveno/core/api/CoreMoneroConnectionsService.java b/core/src/main/java/haveno/core/api/CoreMoneroConnectionsService.java index e6750f53..f1075b6d 100644 --- a/core/src/main/java/haveno/core/api/CoreMoneroConnectionsService.java +++ b/core/src/main/java/haveno/core/api/CoreMoneroConnectionsService.java @@ -1,6 +1,5 @@ package haveno.core.api; -import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.config.BaseCurrencyNetwork; import haveno.common.config.Config; @@ -9,6 +8,8 @@ import haveno.core.xmr.model.EncryptedConnectionList; import haveno.core.xmr.setup.DownloadListener; import haveno.core.xmr.setup.WalletsSetup; import haveno.network.Socks5ProxyProvider; +import haveno.network.p2p.P2PService; +import haveno.network.p2p.P2PServiceListener; import javafx.beans.property.IntegerProperty; import javafx.beans.property.LongProperty; import javafx.beans.property.ObjectProperty; @@ -57,8 +58,9 @@ public final class CoreMoneroConnectionsService { new MoneroRpcConnection("http://127.0.0.1:28081").setPriority(1) )); DEFAULT_CONNECTIONS.put(BaseCurrencyNetwork.XMR_STAGENET, Arrays.asList( - new MoneroRpcConnection("http://127.0.0.1:38081").setPriority(1), // localhost is first priority, use loopback address to match url generated by local node service - new MoneroRpcConnection("http://45.63.8.26:38081").setPriority(2), + new MoneroRpcConnection("http://127.0.0.1:38081").setPriority(1), // localhost is first priority, use loopback address 127.0.0.1 to match url used by local node service + new MoneroRpcConnection("http://127.0.0.1:39081").setPriority(2), // from makefile: `monerod-stagenet-custom` + new MoneroRpcConnection("http://45.63.8.26:38081").setPriority(2), // hosted by haveno new MoneroRpcConnection("http://stagenet.community.rino.io:38081").setPriority(2), new MoneroRpcConnection("http://stagenet.melo.tools:38081").setPriority(2), new MoneroRpcConnection("http://node.sethforprivacy.com:38089").setPriority(2), @@ -89,14 +91,17 @@ public final class CoreMoneroConnectionsService { private final DownloadListener downloadListener = new DownloadListener(); private Socks5ProxyProvider socks5ProxyProvider; + private boolean isInitialized; private MoneroDaemonRpc daemon; @Getter private MoneroDaemonInfo lastInfo; - private boolean isInitialized = false; - private TaskLooper updateDaemonLooper;; + private TaskLooper daemonPollLooper; + private boolean isShutDownStarted; + private List listeners = new ArrayList<>(); @Inject - public CoreMoneroConnectionsService(Config config, + public CoreMoneroConnectionsService(P2PService p2PService, + Config config, CoreContext coreContext, WalletsSetup walletsSetup, CoreAccountService accountService, @@ -112,35 +117,43 @@ public final class CoreMoneroConnectionsService { this.connectionList = connectionList; this.socks5ProxyProvider = socks5ProxyProvider; - // initialize after account open and basic setup - walletsSetup.addSetupTaskHandler(() -> { // TODO: use something better than legacy WalletSetup for notification to initialize - - // initialize from connections read from disk - initialize(); - - // listen for account to be opened or password changed - accountService.addListener(new AccountServiceListener() { - - @Override - public void onAccountOpened() { - try { - log.info(getClass() + ".onAccountOpened() called"); - initialize(); - } catch (Exception e) { - e.printStackTrace(); - throw new RuntimeException(e); - } - } - - @Override - public void onPasswordChanged(String oldPassword, String newPassword) { - log.info(getClass() + ".onPasswordChanged({}, {}) called", oldPassword, newPassword); - connectionList.changePassword(oldPassword, newPassword); - } - }); + // initialize when connected to p2p network + p2PService.addP2PServiceListener(new P2PServiceListener() { + @Override + public void onTorNodeReady() { + initialize(); + } + @Override + public void onHiddenServicePublished() {} + @Override + public void onDataReceived() {} + @Override + public void onNoSeedNodeAvailable() {} + @Override + public void onNoPeersAvailable() {} + @Override + public void onUpdatedDataReceived() {} }); } + public void onShutDownStarted() { + log.info("{}.onShutDownStarted()", getClass().getSimpleName()); + isShutDownStarted = true; + synchronized (this) { + // ensures request not in progress + } + } + + public void shutDown() { + log.info("Shutting down started for {}", getClass().getSimpleName()); + synchronized (lock) { + isInitialized = false; + if (daemonPollLooper != null) daemonPollLooper.stop(); + connectionManager.stopCheckingConnection(); + daemon = null; + } + } + // ------------------------ CONNECTION MANAGEMENT ------------------------- public MoneroDaemonRpc getDaemon() { @@ -154,11 +167,11 @@ public final class CoreMoneroConnectionsService { public void addListener(MoneroConnectionManagerListener listener) { synchronized (lock) { - connectionManager.addListener(listener); + listeners.add(listener); } } - public boolean isConnected() { + public Boolean isConnected() { return connectionManager.isConnected(); } @@ -225,8 +238,8 @@ public final class CoreMoneroConnectionsService { public void startCheckingConnection(Long refreshPeriod) { synchronized (lock) { accountService.checkAccountOpen(); - connectionManager.startCheckingConnection(refreshPeriod == null ? getDefaultRefreshPeriodMs() : refreshPeriod); connectionList.setRefreshPeriod(refreshPeriod); + updatePolling(); } } @@ -257,17 +270,11 @@ public final class CoreMoneroConnectionsService { return getConnection() != null && HavenoUtils.isLocalHost(getConnection().getUri()); } - public long getDefaultRefreshPeriodMs() { - if (daemon == null) return REFRESH_PERIOD_LOCAL_MS; - else { - if (isConnectionLocal()) { - if (lastInfo != null && (lastInfo.isBusySyncing() || (lastInfo.getHeightWithoutBootstrap() != null && lastInfo.getHeightWithoutBootstrap() > 0 && lastInfo.getHeightWithoutBootstrap() < lastInfo.getHeight()))) return REFRESH_PERIOD_HTTP_MS; // refresh slower if syncing or bootstrapped - else return REFRESH_PERIOD_LOCAL_MS; // TODO: announce faster refresh after done syncing - } else if (getConnection().isOnion()) { - return REFRESH_PERIOD_ONION_MS; - } else { - return REFRESH_PERIOD_HTTP_MS; - } + public long getRefreshPeriodMs() { + if (connectionList.getRefreshPeriod() < 0 || connectionList.getRefreshPeriod() > 0) { + return connectionList.getRefreshPeriod(); + } else { + return getDefaultRefreshPeriodMs(); } } @@ -329,7 +336,48 @@ public final class CoreMoneroConnectionsService { // ------------------------------- HELPERS -------------------------------- + private long getDefaultRefreshPeriodMs() { + if (daemon == null) return REFRESH_PERIOD_LOCAL_MS; + else { + if (isConnectionLocal()) { + if (lastInfo != null && (lastInfo.isBusySyncing() || (lastInfo.getHeightWithoutBootstrap() != null && lastInfo.getHeightWithoutBootstrap() > 0 && lastInfo.getHeightWithoutBootstrap() < lastInfo.getHeight()))) return REFRESH_PERIOD_HTTP_MS; // refresh slower if syncing or bootstrapped + else return REFRESH_PERIOD_LOCAL_MS; // TODO: announce faster refresh after done syncing + } else if (getConnection().isOnion()) { + return REFRESH_PERIOD_ONION_MS; + } else { + return REFRESH_PERIOD_HTTP_MS; + } + } + } + private void initialize() { + + // initialize connections + initializeConnections(); + + // listen for account to be opened or password changed + accountService.addListener(new AccountServiceListener() { + + @Override + public void onAccountOpened() { + try { + log.info(getClass() + ".onAccountOpened() called"); + initialize(); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + + @Override + public void onPasswordChanged(String oldPassword, String newPassword) { + log.info(getClass() + ".onPasswordChanged({}, {}) called", oldPassword, newPassword); + connectionList.changePassword(oldPassword, newPassword); + } + }); + } + + private void initializeConnections() { synchronized (lock) { // reset connection manager @@ -365,12 +413,11 @@ public final class CoreMoneroConnectionsService { currentConnectionUri = Optional.of(connection.getUri()); } - // restore configuration and check connection + // restore configuration if ("".equals(config.xmrNode)) connectionManager.setAutoSwitch(connectionList.getAutoSwitch()); - long refreshPeriod = connectionList.getRefreshPeriod(); - if (refreshPeriod > 0) connectionManager.startCheckingConnection(refreshPeriod); - else if (refreshPeriod == 0) connectionManager.startCheckingConnection(); - else checkConnection(); + + // check connection + checkConnection(); // run once if (!isInitialized) { @@ -392,21 +439,23 @@ public final class CoreMoneroConnectionsService { }); } - // if offline and last connection is local, start local node if offline + // if offline and last connection is local node, start local node if it's offline currentConnectionUri.ifPresent(uri -> { try { - if (!connectionManager.isConnected() && HavenoUtils.isLocalHost(uri) && !nodeService.isOnline()) { + if (!connectionManager.isConnected() && nodeService.equalsUri(uri) && !nodeService.isOnline()) { + log.info("Starting local node"); nodeService.startMoneroNode(); } } catch (Exception e) { log.warn("Unable to start local monero node: " + e.getMessage()); + e.printStackTrace(); } }); // prefer to connect to local node unless prevented by configuration if ("".equals(config.xmrNode) && - (!connectionManager.isConnected() || connectionManager.getAutoSwitch()) && - nodeService.isConnected()) { + (!connectionManager.isConnected() || connectionManager.getAutoSwitch()) && + nodeService.isConnected()) { MoneroRpcConnection connection = connectionManager.getConnectionByUri(nodeService.getDaemon().getRpcConnection().getUri()); if (connection != null) { connection.checkConnection(connectionManager.getTimeout()); @@ -417,22 +466,23 @@ public final class CoreMoneroConnectionsService { // if using legacy desktop app, connect to best available connection if (!coreContext.isApiUser() && "".equals(config.xmrNode)) { connectionManager.setAutoSwitch(true); - connectionManager.setConnection(connectionManager.getBestAvailableConnection()); + MoneroRpcConnection bestConnection = connectionManager.getBestAvailableConnection(); + log.info("Setting best available connection for monerod: " + (bestConnection == null ? null : bestConnection.getUri())); + connectionManager.setConnection(bestConnection); } // register connection change listener - if (!isInitialized) { - connectionManager.addListener(this::onConnectionChanged); - isInitialized = true; - } + connectionManager.addListener(this::onConnectionChanged); + isInitialized = true; - // announce connection + // update connection state onConnectionChanged(connectionManager.getConnection()); } } private void onConnectionChanged(MoneroRpcConnection currentConnection) { - // TODO: ignore if shutdown + log.info("CoreMoneroConnetionsService.onConnectionChanged() uri={}, connected=", currentConnection == null ? null : currentConnection.getUri(), currentConnection == null ? "false" : currentConnection.isConnected()); + if (isShutDownStarted) return; synchronized (lock) { if (currentConnection == null) { daemon = null; @@ -443,31 +493,46 @@ public final class CoreMoneroConnectionsService { connectionList.addConnection(currentConnection); connectionList.setCurrentConnectionUri(currentConnection.getUri()); } - startPollingDaemon(); } - } + updatePolling(); - private void startPollingDaemon() { + // notify listeners synchronized (lock) { - updateDaemonInfo(); - if (updateDaemonLooper != null) updateDaemonLooper.stop(); - UserThread.runAfter(() -> { - synchronized (lock) { - if (updateDaemonLooper != null) updateDaemonLooper.stop(); - updateDaemonLooper = new TaskLooper(() -> updateDaemonInfo()); - updateDaemonLooper.start(getDefaultRefreshPeriodMs()); - } - }, getDefaultRefreshPeriodMs() / 1000); + for (MoneroConnectionManagerListener listener : listeners) listener.onConnectionChanged(currentConnection); } } - private void updateDaemonInfo() { + private void updatePolling() { + new Thread(() -> { + synchronized (lock) { + stopPolling(); + if (getRefreshPeriodMs() > 0) startPolling(); + } + }).start(); + } + + private void startPolling() { + synchronized (lock) { + if (daemonPollLooper != null) daemonPollLooper.stop(); + daemonPollLooper = new TaskLooper(() -> pollDaemonInfo()); + daemonPollLooper.start(getRefreshPeriodMs()); + } + } + + private void stopPolling() { + synchronized (lock) { + if (daemonPollLooper != null) daemonPollLooper.stop(); + } + } + + private void pollDaemonInfo() { + if (isShutDownStarted) return; try { - log.trace("Updating daemon info"); + log.debug("Polling daemon info"); if (daemon == null) throw new RuntimeException("No daemon connection"); - lastInfo = daemon.getInfo(); - //System.out.println(JsonUtils.serialize(lastInfo)); - //System.out.println(JsonUtils.serialize(daemon.getSyncInfo())); + synchronized (this) { + lastInfo = daemon.getInfo(); + } chainHeight.set(lastInfo.getTargetHeight() == 0 ? lastInfo.getHeight() : lastInfo.getTargetHeight()); // set peer connections @@ -478,20 +543,33 @@ public final class CoreMoneroConnectionsService { // // TODO: peers unknown due to restricted RPC call // } // numPeers.set(peers.get().size()); - numPeers.set(lastInfo.getNumOutgoingConnections() + lastInfo.getNumIncomingConnections()); + numPeers.set(lastInfo.getNumOutgoingConnections() + lastInfo.getNumIncomingConnections()); peers.set(new ArrayList()); + // log recovery message if (lastErrorTimestamp != null) { log.info("Successfully fetched daemon info after previous error"); lastErrorTimestamp = null; } + + // update and notify connected state + if (!Boolean.TRUE.equals(connectionManager.isConnected())) { + connectionManager.checkConnection(); + } } catch (Exception e) { - if (lastErrorTimestamp == null || System.currentTimeMillis() - lastErrorTimestamp > MIN_ERROR_LOG_PERIOD_MS) { + + // log error message periodically + if ((lastErrorTimestamp == null || System.currentTimeMillis() - lastErrorTimestamp > MIN_ERROR_LOG_PERIOD_MS)) { lastErrorTimestamp = System.currentTimeMillis(); log.warn("Could not update daemon info: " + e.getMessage()); if (DevEnv.isDevMode()) e.printStackTrace(); } - if (connectionManager.getAutoSwitch()) connectionManager.setConnection(connectionManager.getBestAvailableConnection()); + + // check connection which notifies of changes + synchronized (this) { + if (connectionManager.getAutoSwitch()) connectionManager.setConnection(connectionManager.getBestAvailableConnection()); + else connectionManager.checkConnection(); + } } } diff --git a/core/src/main/java/haveno/core/api/CoreMoneroNodeService.java b/core/src/main/java/haveno/core/api/CoreMoneroNodeService.java index 8e729bf0..23530ae7 100644 --- a/core/src/main/java/haveno/core/api/CoreMoneroNodeService.java +++ b/core/src/main/java/haveno/core/api/CoreMoneroNodeService.java @@ -23,6 +23,7 @@ import haveno.core.trade.HavenoUtils; import haveno.core.user.Preferences; import haveno.core.xmr.MoneroNodeSettings; import lombok.extern.slf4j.Slf4j; +import monero.common.MoneroUtils; import monero.daemon.MoneroDaemonRpc; import javax.inject.Inject; @@ -44,7 +45,7 @@ public class CoreMoneroNodeService { public static final String MONEROD_DIR = Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_LOCAL ? System.getProperty("user.dir") + File.separator + ".localnet" : Config.appDataDir().getAbsolutePath(); public static final String MONEROD_NAME = Utilities.isWindows() ? "monerod.exe" : "monerod"; public static final String MONEROD_PATH = MONEROD_DIR + File.separator + MONEROD_NAME; - private static final String MONEROD_DATADIR = MONEROD_DIR + File.separator + Config.baseCurrencyNetwork().toString().toLowerCase() + File.separator + "node1"; + private static final String MONEROD_DATADIR = Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_LOCAL ? MONEROD_DIR + File.separator + Config.baseCurrencyNetwork().toString().toLowerCase() + File.separator + "node1" : null; // use default directory unless local private final Preferences preferences; private final List listeners = new ArrayList<>(); @@ -59,15 +60,17 @@ public class CoreMoneroNodeService { // client to the local Monero node private MoneroDaemonRpc daemon; - - @Inject - public CoreMoneroNodeService(Preferences preferences) { - this.preferences = preferences; - Integer rpcPort = null; + private static Integer rpcPort; + static { if (Config.baseCurrencyNetwork().isMainnet()) rpcPort = 18081; else if (Config.baseCurrencyNetwork().isTestnet()) rpcPort = 28081; else if (Config.baseCurrencyNetwork().isStagenet()) rpcPort = 38081; else throw new RuntimeException("Base network is not local testnet, stagenet, or mainnet"); + } + + @Inject + public CoreMoneroNodeService(Preferences preferences) { + this.preferences = preferences; this.daemon = new MoneroDaemonRpc("http://" + HavenoUtils.LOOPBACK_HOST + ":" + rpcPort); } @@ -90,6 +93,10 @@ public class CoreMoneroNodeService { return daemon.getRpcConnection().checkConnection(5000); } + public boolean equalsUri(String uri) { + return HavenoUtils.isLocalHost(uri) && MoneroUtils.parseUri(uri).getPort() == rpcPort; + } + /** * Check if local Monero node is online. */ diff --git a/core/src/main/java/haveno/core/app/HavenoExecutable.java b/core/src/main/java/haveno/core/app/HavenoExecutable.java index 9da79c55..48d54064 100644 --- a/core/src/main/java/haveno/core/app/HavenoExecutable.java +++ b/core/src/main/java/haveno/core/app/HavenoExecutable.java @@ -34,25 +34,28 @@ import haveno.common.setup.UncaughtExceptionHandler; import haveno.common.util.Utilities; import haveno.core.api.AccountServiceListener; import haveno.core.api.CoreAccountService; +import haveno.core.api.CoreMoneroConnectionsService; +import haveno.core.offer.OfferBookService; import haveno.core.offer.OpenOfferManager; import haveno.core.provider.price.PriceFeedService; import haveno.core.setup.CorePersistedDataHost; import haveno.core.setup.CoreSetup; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import haveno.core.trade.HavenoUtils; -import haveno.core.trade.TradeManager; import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.trade.txproof.xmr.XmrTxProofService; import haveno.core.xmr.setup.WalletsSetup; import haveno.core.xmr.wallet.BtcWalletService; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.P2PService; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.io.Console; -import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -74,7 +77,8 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven protected Injector injector; protected AppModule module; protected Config config; - private boolean isShutdownInProgress; + @Getter + protected boolean isShutdownInProgress; private boolean isReadOnly; private Thread keepRunningThread; private AtomicInteger keepRunningResult = new AtomicInteger(EXIT_SUCCESS); @@ -303,12 +307,13 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven // This might need to be overwritten in case the application is not using all modules @Override public void gracefulShutDown(ResultHandler onShutdown, boolean systemExit) { - log.info("Start graceful shutDown"); + log.info("Starting graceful shut down of {}", getClass().getSimpleName()); + + // ignore if shut down in progress if (isShutdownInProgress) { log.info("Ignoring call to gracefulShutDown, already in progress"); return; } - isShutdownInProgress = true; ResultHandler resultHandler; @@ -328,33 +333,41 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven } try { + + // notify trade protocols and wallets to prepare for shut down before shutting down + Set tasks = new HashSet(); + tasks.add(() -> injector.getInstance(XmrWalletService.class).onShutDownStarted()); + tasks.add(() -> injector.getInstance(CoreMoneroConnectionsService.class).onShutDownStarted()); + HavenoUtils.executeTasks(tasks); // notify in parallel + injector.getInstance(PriceFeedService.class).shutDown(); injector.getInstance(ArbitratorManager.class).shutDown(); injector.getInstance(TradeStatisticsManager.class).shutDown(); injector.getInstance(XmrTxProofService.class).shutDown(); injector.getInstance(AvoidStandbyModeService.class).shutDown(); - log.info("TradeManager and XmrWalletService shutdown started"); - HavenoUtils.executeTasks(Arrays.asList( // shut down trade and main wallets at same time - () -> injector.getInstance(TradeManager.class).shutDown(), - () -> injector.getInstance(XmrWalletService.class).shutDown(!isReadOnly))); - log.info("OpenOfferManager shutdown started"); + + // shut down open offer manager + log.info("Shutting down OpenOfferManager, OfferBookService, and P2PService"); injector.getInstance(OpenOfferManager.class).shutDown(() -> { - log.info("OpenOfferManager shutdown completed"); - injector.getInstance(BtcWalletService.class).shutDown(); + // shut down offer book service + injector.getInstance(OfferBookService.class).shutDown(); - // We need to shutdown BitcoinJ before the P2PService as it uses Tor. - WalletsSetup walletsSetup = injector.getInstance(WalletsSetup.class); - walletsSetup.shutDownComplete.addListener((ov, o, n) -> { - log.info("WalletsSetup shutdown completed"); + // shut down p2p service + injector.getInstance(P2PService.class).shutDown(() -> { + log.info("Done shutting down OpenOfferManager, OfferBookService, and P2PService"); - injector.getInstance(P2PService.class).shutDown(() -> { - log.info("P2PService shutdown completed"); + // shut down monero wallets and connections + injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> { + log.info("Graceful shutdown completed. Exiting now."); module.close(injector); completeShutdown(resultHandler, EXIT_SUCCESS, systemExit); }); + injector.getInstance(BtcWalletService.class).shutDown(); + injector.getInstance(XmrWalletService.class).shutDown(); + injector.getInstance(CoreMoneroConnectionsService.class).shutDown(); + injector.getInstance(WalletsSetup.class).shutDown(); }); - walletsSetup.shutDown(); }); } catch (Throwable t) { log.error("App shutdown failed with exception {}", t.toString()); diff --git a/core/src/main/java/haveno/core/app/HavenoSetup.java b/core/src/main/java/haveno/core/app/HavenoSetup.java index ae9e30c5..64ca865d 100644 --- a/core/src/main/java/haveno/core/app/HavenoSetup.java +++ b/core/src/main/java/haveno/core/app/HavenoSetup.java @@ -44,6 +44,7 @@ import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.support.dispute.mediation.MediationManager; import haveno.core.support.dispute.refund.RefundManager; +import haveno.core.trade.HavenoUtils; import haveno.core.trade.TradeManager; import haveno.core.trade.TradeTxException; import haveno.core.user.Preferences; @@ -246,7 +247,7 @@ public class HavenoSetup { this.refundManager = refundManager; this.arbitrationManager = arbitrationManager; - xmrWalletService.setHavenoSetup(this); + HavenoUtils.havenoSetup = this; MemPoolSpaceTxBroadcaster.init(socks5ProxyProvider, preferences, localBitcoinNode); } diff --git a/core/src/main/java/haveno/core/app/P2PNetworkSetup.java b/core/src/main/java/haveno/core/app/P2PNetworkSetup.java index 31587858..350aab4a 100644 --- a/core/src/main/java/haveno/core/app/P2PNetworkSetup.java +++ b/core/src/main/java/haveno/core/app/P2PNetworkSetup.java @@ -81,7 +81,7 @@ public class P2PNetworkSetup { this.preferences = preferences; } - BooleanProperty init(Runnable initWalletServiceHandler, @Nullable Consumer displayTorNetworkSettingsHandler) { + BooleanProperty init(Runnable onReadyHandler, @Nullable Consumer displayTorNetworkSettingsHandler) { StringProperty bootstrapState = new SimpleStringProperty(); StringProperty bootstrapWarning = new SimpleStringProperty(); BooleanProperty hiddenServicePublished = new SimpleBooleanProperty(); @@ -146,8 +146,8 @@ public class P2PNetworkSetup { priceFeedService.setCurrencyCodeOnInit(); priceFeedService.requestPrices(); - // invoke handler to initialize wallet - initWalletServiceHandler.run(); + // invoke handler when network ready + onReadyHandler.run(); } @Override diff --git a/core/src/main/java/haveno/core/app/WalletAppSetup.java b/core/src/main/java/haveno/core/app/WalletAppSetup.java index 6b981f57..867433aa 100644 --- a/core/src/main/java/haveno/core/app/WalletAppSetup.java +++ b/core/src/main/java/haveno/core/app/WalletAppSetup.java @@ -40,6 +40,8 @@ import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import monero.daemon.model.MoneroDaemonInfo; + import org.bitcoinj.core.RejectMessage; import org.bitcoinj.core.VersionMessage; import org.bitcoinj.store.BlockStoreException; @@ -115,7 +117,8 @@ public class WalletAppSetup { if (exception == null) { double percentage = (double) downloadPercentage; btcSyncProgress.set(percentage); - Long bestChainHeight = connectionService.getDaemon() == null ? null : connectionService.getDaemon().getInfo().getHeight(); + MoneroDaemonInfo lastInfo = connectionService.getLastInfo(); + Long bestChainHeight = lastInfo == null ? null : lastInfo.getHeight(); String chainHeightAsString = bestChainHeight != null && bestChainHeight > 0 ? String.valueOf(bestChainHeight) : ""; diff --git a/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java b/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java index 6bec246f..f6f7331d 100644 --- a/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java +++ b/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java @@ -26,9 +26,12 @@ import haveno.common.handlers.ResultHandler; import haveno.common.persistence.PersistenceManager; import haveno.common.setup.GracefulShutDownHandler; import haveno.common.util.Profiler; +import haveno.core.api.CoreMoneroConnectionsService; import haveno.core.app.HavenoExecutable; +import haveno.core.offer.OfferBookService; import haveno.core.offer.OpenOfferManager; import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager; +import haveno.core.trade.HavenoUtils; import haveno.core.xmr.setup.WalletsSetup; import haveno.core.xmr.wallet.BtcWalletService; import haveno.core.xmr.wallet.XmrWalletService; @@ -42,7 +45,9 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Comparator; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; @@ -76,25 +81,54 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable { // We don't use the gracefulShutDown implementation of the super class as we have a limited set of modules @Override public void gracefulShutDown(ResultHandler resultHandler) { - log.info("gracefulShutDown"); + log.info("Starting graceful shut down of {}", getClass().getSimpleName()); + + // ignore if shut down in progress + if (isShutdownInProgress) { + log.info("Ignoring call to gracefulShutDown, already in progress"); + return; + } + isShutdownInProgress = true; + try { if (injector != null) { + + // notify trade protocols and wallets to prepare for shut down before shutting down + Set tasks = new HashSet(); + tasks.add(() -> injector.getInstance(XmrWalletService.class).onShutDownStarted()); + tasks.add(() -> injector.getInstance(CoreMoneroConnectionsService.class).onShutDownStarted()); + HavenoUtils.executeTasks(tasks); // notify in parallel + JsonFileManager.shutDownAllInstances(); injector.getInstance(ArbitratorManager.class).shutDown(); - injector.getInstance(XmrWalletService.class).shutDown(true); - injector.getInstance(OpenOfferManager.class).shutDown(() -> injector.getInstance(P2PService.class).shutDown(() -> { - injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> { - module.close(injector); - PersistenceManager.flushAllDataToDiskAtShutdown(() -> { - resultHandler.handleResult(); - log.info("Graceful shutdown completed. Exiting now."); - UserThread.runAfter(() -> System.exit(HavenoExecutable.EXIT_SUCCESS), 1); + // shut down open offer manager + log.info("Shutting down OpenOfferManager, OfferBookService, and P2PService"); + injector.getInstance(OpenOfferManager.class).shutDown(() -> { + + // shut down offer book service + injector.getInstance(OfferBookService.class).shutDown(); + + // shut down p2p service + injector.getInstance(P2PService.class).shutDown(() -> { + log.info("Done shutting down OpenOfferManager, OfferBookService, and P2PService"); + + // shut down monero wallets and connections + injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> { + module.close(injector); + PersistenceManager.flushAllDataToDiskAtShutdown(() -> { + resultHandler.handleResult(); + log.info("Graceful shutdown completed. Exiting now."); + UserThread.runAfter(() -> System.exit(HavenoExecutable.EXIT_SUCCESS), 1); + }); }); + injector.getInstance(BtcWalletService.class).shutDown(); + injector.getInstance(XmrWalletService.class).shutDown(); + injector.getInstance(CoreMoneroConnectionsService.class).shutDown(); + injector.getInstance(WalletsSetup.class).shutDown(); }); - injector.getInstance(WalletsSetup.class).shutDown(); - injector.getInstance(BtcWalletService.class).shutDown(); - })); + }); + // we wait max 5 sec. UserThread.runAfter(() -> { PersistenceManager.flushAllDataToDiskAtShutdown(() -> { diff --git a/core/src/main/java/haveno/core/offer/OfferBookService.java b/core/src/main/java/haveno/core/offer/OfferBookService.java index de4b7ff2..76c195fe 100644 --- a/core/src/main/java/haveno/core/offer/OfferBookService.java +++ b/core/src/main/java/haveno/core/offer/OfferBookService.java @@ -97,7 +97,7 @@ public class OfferBookService { connectionsService.addListener(new MoneroConnectionManagerListener() { @Override public void onConnectionChanged(MoneroRpcConnection connection) { - if (keyImagePoller == null) return; + maybeInitializeKeyImagePoller(); keyImagePoller.setDaemon(connectionsService.getDaemon()); keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs()); } @@ -111,8 +111,8 @@ public class OfferBookService { synchronized (offerBookChangedListeners) { offerBookChangedListeners.forEach(listener -> { if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) { - maybeInitializeKeyImagePoller(); OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload(); + maybeInitializeKeyImagePoller(); keyImagePoller.addKeyImages(offerPayload.getReserveTxKeyImages()); Offer offer = new Offer(offerPayload); offer.setPriceFeedService(priceFeedService); @@ -130,8 +130,8 @@ public class OfferBookService { synchronized (offerBookChangedListeners) { offerBookChangedListeners.forEach(listener -> { if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) { - maybeInitializeKeyImagePoller(); OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload(); + maybeInitializeKeyImagePoller(); keyImagePoller.removeKeyImages(offerPayload.getReserveTxKeyImages()); Offer offer = new Offer(offerPayload); offer.setPriceFeedService(priceFeedService); @@ -257,6 +257,10 @@ public class OfferBookService { } } + public void shutDown() { + if (keyImagePoller != null) keyImagePoller.clearKeyImages(); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Private @@ -276,11 +280,12 @@ public class OfferBookService { } }); - // first poll after 5s + // first poll after 20s + // TODO: remove? new Thread(() -> { - GenUtils.waitFor(5000); + GenUtils.waitFor(20000); keyImagePoller.poll(); - }); + }).start(); } private long getKeyImageRefreshPeriodMs() { diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index cb1aaeb0..91b853e7 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -202,8 +202,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe @Override public void onConnectionChanged(MoneroRpcConnection connection) { maybeInitializeKeyImagePoller(); - signedOfferKeyImagePoller.setDaemon(connectionsService.getDaemon()); - signedOfferKeyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs()); } }); @@ -258,10 +256,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe }); // first poll in 5s + // TODO: remove? new Thread(() -> { GenUtils.waitFor(5000); signedOfferKeyImagePoller.poll(); - }); + }).start(); } private long getKeyImageRefreshPeriodMs() { @@ -334,6 +333,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe stopped = true; p2PService.getPeerManager().removeListener(this); p2PService.removeDecryptedDirectMessageListener(this); + signedOfferKeyImagePoller.clearKeyImages(); stopPeriodicRefreshOffersTimer(); stopPeriodicRepublishOffersTimer(); 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 247ca763..7314f04f 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java @@ -53,12 +53,12 @@ public class MakerReserveOfferFunds extends Task { BigInteger makerFee = offer.getMakerFee(); BigInteger sendAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.valueOf(0) : offer.getAmount(); BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getBuyerSecurityDeposit() : offer.getSellerSecurityDeposit(); - String returnAddress = model.getXmrWalletService().getNewAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); + String returnAddress = model.getXmrWalletService().getNewAddressEntry(offer.getId(), XmrAddressEntry.Context.RESERVED_FOR_TRADE).getAddressString(); MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(makerFee, sendAmount, securityDeposit, returnAddress); // check for error in case creating reserve tx exceeded timeout // TODO: better way? - if (!model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).isPresent()) { + if (!model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.RESERVED_FOR_TRADE).isPresent()) { throw new RuntimeException("An error has occurred posting offer " + offer.getId() + " causing its subaddress entry to be deleted"); } diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java index 94d18309..dfdd60b0 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java @@ -60,7 +60,7 @@ public class MakerSendSignOfferRequest extends Task { runInterceptHook(); // create request for arbitrator to sign offer - String returnAddress = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(); + String returnAddress = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.RESERVED_FOR_TRADE).get().getAddressString(); SignOfferRequest request = new SignOfferRequest( model.getOffer().getId(), P2PService.getMyNodeAddress(), 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 6c55eb78..9daf0085 100644 --- a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java @@ -465,13 +465,21 @@ public abstract class DisputeManager> extends Sup DisputeValidation.validateDisputeData(dispute); DisputeValidation.validateNodeAddresses(dispute, config); DisputeValidation.validateSenderNodeAddress(dispute, message.getSenderNodeAddress()); - DisputeValidation.validatePaymentAccountPayload(dispute); //DisputeValidation.testIfDisputeTriesReplay(dispute, disputeList.getList()); } catch (DisputeValidation.ValidationException e) { validationExceptions.add(e); throw e; } + // try to validate payment account + // TODO: add field to dispute details: valid, invalid, missing + try { + DisputeValidation.validatePaymentAccountPayload(dispute); + } catch (Exception e) { + log.warn(e.getMessage()); + trade.prependErrorMessage(e.getMessage()); + } + // get sender senderPubKeyRing = trade.isArbitrator() ? (dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing()) : trade.getArbitrator().getPubKeyRing(); TradePeer sender = trade.getTradePeer(senderPubKeyRing); diff --git a/core/src/main/java/haveno/core/support/dispute/arbitration/arbitrator/Arbitrator.java b/core/src/main/java/haveno/core/support/dispute/arbitration/arbitrator/Arbitrator.java index d1db0807..99125ad4 100644 --- a/core/src/main/java/haveno/core/support/dispute/arbitration/arbitrator/Arbitrator.java +++ b/core/src/main/java/haveno/core/support/dispute/arbitration/arbitrator/Arbitrator.java @@ -37,10 +37,8 @@ import java.util.Optional; @Slf4j @Getter public final class Arbitrator extends DisputeAgent { - private final String xmrAddress; public Arbitrator(NodeAddress nodeAddress, - String xmrAddress, PubKeyRing pubKeyRing, List languageCodes, long registrationDate, @@ -59,8 +57,6 @@ public final class Arbitrator extends DisputeAgent { emailAddress, info, extraDataMap); - - this.xmrAddress = xmrAddress; } /////////////////////////////////////////////////////////////////////////////////////////// @@ -71,7 +67,6 @@ public final class Arbitrator extends DisputeAgent { public protobuf.StoragePayload toProtoMessage() { protobuf.Arbitrator.Builder builder = protobuf.Arbitrator.newBuilder() .setNodeAddress(nodeAddress.toProtoMessage()) - .setXmrAddress(xmrAddress) .setPubKeyRing(pubKeyRing.toProtoMessage()) .addAllLanguageCodes(languageCodes) .setRegistrationDate(registrationDate) @@ -85,7 +80,6 @@ public final class Arbitrator extends DisputeAgent { public static Arbitrator fromProto(protobuf.Arbitrator proto) { return new Arbitrator(NodeAddress.fromProto(proto.getNodeAddress()), - proto.getXmrAddress(), PubKeyRing.fromProto(proto.getPubKeyRing()), new ArrayList<>(proto.getLanguageCodesList()), proto.getRegistrationDate(), @@ -103,8 +97,6 @@ public final class Arbitrator extends DisputeAgent { @Override public String toString() { - return "Arbitrator{" + - ",\n xmrAddress='" + xmrAddress + '\'' + - "\n} " + super.toString(); + return "Arbitrator{} " + super.toString(); } } diff --git a/core/src/main/java/haveno/core/trade/HavenoUtils.java b/core/src/main/java/haveno/core/trade/HavenoUtils.java index f93a5efa..29af7274 100644 --- a/core/src/main/java/haveno/core/trade/HavenoUtils.java +++ b/core/src/main/java/haveno/core/trade/HavenoUtils.java @@ -25,6 +25,7 @@ import haveno.common.crypto.KeyRing; import haveno.common.crypto.PubKeyRing; import haveno.common.crypto.Sig; import haveno.common.util.Utilities; +import haveno.core.app.HavenoSetup; import haveno.core.offer.Offer; import haveno.core.offer.OfferPayload; import haveno.core.support.dispute.arbitration.ArbitrationManager; @@ -35,6 +36,8 @@ import haveno.core.trade.messages.PaymentSentMessage; import haveno.core.util.JsonUtil; import haveno.network.p2p.NodeAddress; import lombok.extern.slf4j.Slf4j; +import monero.common.MoneroRpcConnection; + import org.bitcoinj.core.Coin; import javax.annotation.Nullable; @@ -69,8 +72,9 @@ public class HavenoUtils { private static final int POOL_SIZE = 10; private static final ExecutorService POOL = Executors.newFixedThreadPool(POOL_SIZE); - public static ArbitrationManager arbitrationManager; // TODO: better way to share reference? - + // TODO: better way to share refernces? + public static ArbitrationManager arbitrationManager; + public static HavenoSetup havenoSetup; // ----------------------- CONVERSION UTILS ------------------------------- @@ -502,4 +506,10 @@ public class HavenoUtils { public static String toCamelCase(String underscore) { return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, underscore); } + + public static boolean connectionConfigsEqual(MoneroRpcConnection c1, MoneroRpcConnection c2) { + if (c1 == c2) return true; + if (c1 == null) return false; + return c1.equals(c2); // equality considers uri, username, and password + } } diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index c978c7c4..a16d2b97 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -20,6 +20,7 @@ package haveno.core.trade; import com.google.common.base.Preconditions; import com.google.protobuf.ByteString; import com.google.protobuf.Message; + import haveno.common.UserThread; import haveno.common.crypto.Encryption; import haveno.common.crypto.PubKeyRing; @@ -72,6 +73,7 @@ import monero.common.TaskLooper; import monero.daemon.MoneroDaemon; import monero.daemon.model.MoneroTx; import monero.wallet.MoneroWallet; +import monero.wallet.MoneroWalletRpc; import monero.wallet.model.MoneroDestination; import monero.wallet.model.MoneroMultisigSignResult; import monero.wallet.model.MoneroOutputWallet; @@ -80,6 +82,8 @@ import monero.wallet.model.MoneroTxQuery; import monero.wallet.model.MoneroTxSet; import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroWalletListener; + +import org.apache.commons.lang3.StringUtils; import org.bitcoinj.core.Coin; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; @@ -94,7 +98,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; @@ -385,6 +388,8 @@ public abstract class Trade implements Tradable, Model { @Getter transient private boolean isInitialized; @Getter + transient private boolean isShutDownStarted; + @Getter transient private boolean isShutDown; // Added in v1.2.0 @@ -572,12 +577,15 @@ public abstract class Trade implements Tradable, Model { /////////////////////////////////////////////////////////////////////////////////////////// public void initialize(ProcessModelServiceProvider serviceProvider) { + if (isInitialized) throw new IllegalStateException(getClass().getSimpleName() + " " + getId() + " is already initialized"); + + // set arbitrator pub key ring once known serviceProvider.getArbitratorManager().getDisputeAgentByNodeAddress(getArbitratorNodeAddress()).ifPresent(arbitrator -> { getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing()); }); // listen to daemon connection - xmrWalletService.getConnectionsService().addListener(newConnection -> setDaemonConnection(newConnection)); + xmrWalletService.getConnectionsService().addListener(newConnection -> onConnectionChanged(newConnection)); // check if done if (isPayoutUnlocked()) { @@ -585,19 +593,19 @@ public abstract class Trade implements Tradable, Model { return; } - // reset payment sent state if no ack receive - if (getState().ordinal() >= Trade.State.BUYER_CONFIRMED_IN_UI_PAYMENT_SENT.ordinal() && getState().ordinal() < Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG.ordinal()) { + // reset buyer's payment sent state if no ack receive + if (this instanceof BuyerTrade && getState().ordinal() >= Trade.State.BUYER_CONFIRMED_IN_UI_PAYMENT_SENT.ordinal() && getState().ordinal() < Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG.ordinal()) { log.warn("Resetting state of {} {} from {} to {} because no ack was received", getClass().getSimpleName(), getId(), getState(), Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN); setState(Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN); } - // reset payment received state if no ack receive - if (getState().ordinal() >= Trade.State.SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT.ordinal() && getState().ordinal() < Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG.ordinal()) { + // reset seller's payment received state if no ack receive + if (this instanceof SellerTrade && getState().ordinal() >= Trade.State.SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT.ordinal() && getState().ordinal() < Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG.ordinal()) { log.warn("Resetting state of {} {} from {} to {} because no ack was received", getClass().getSimpleName(), getId(), getState(), Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG); setState(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG); } - // handle trade state events + // handle trade phase events tradePhaseSubscription = EasyBind.subscribe(phaseProperty, newValue -> { if (isDepositsPublished() && !isPayoutUnlocked()) updateWalletRefreshPeriod(); if (isCompleted()) { @@ -652,18 +660,28 @@ public abstract class Trade implements Tradable, Model { xmrWalletService.addWalletListener(idlePayoutSyncer); } - if (isDepositRequested()) { - - // start syncing and polling trade wallet - updateSyncing(); - - // allow state notifications to process before returning - CountDownLatch latch = new CountDownLatch(1); - UserThread.execute(() -> latch.countDown()); - HavenoUtils.awaitLatch(latch); + // send deposit confirmed message on startup or event + if (isDepositsConfirmed()) { + new Thread(() -> getProtocol().maybeSendDepositsConfirmedMessages()).start(); + } else { + EasyBind.subscribe(stateProperty(), state -> { + if (isDepositsConfirmed()) { + new Thread(() -> getProtocol().maybeSendDepositsConfirmedMessages()).start(); + } + }); } + // reprocess pending payout messages + this.getProtocol().maybeReprocessPaymentReceivedMessage(false); + HavenoUtils.arbitrationManager.maybeReprocessDisputeClosedMessage(this, false); + + // trade is initialized but not synced isInitialized = true; + + // sync wallet if applicable + if (!isDepositRequested() || isPayoutUnlocked()) return; + if (xmrWalletService.getConnectionsService().getConnection() == null || Boolean.FALSE.equals(xmrWalletService.getConnectionsService().isConnected())) return; + updateSyncing(); } public void requestPersistence() { @@ -710,8 +728,8 @@ public abstract class Trade implements Tradable, Model { synchronized (walletLock) { if (wallet != null) return wallet; if (!walletExists()) return null; - if (isShutDown) throw new RuntimeException("Cannot open wallet for " + getClass().getSimpleName() + " " + getId() + " because trade is shut down"); - if (!isShutDown) wallet = xmrWalletService.openWallet(getWalletName()); + if (isShutDownStarted) throw new RuntimeException("Cannot open wallet for " + getClass().getSimpleName() + " " + getId() + " because shut down is started"); + else wallet = xmrWalletService.openWallet(getWalletName()); return wallet; } } @@ -744,7 +762,7 @@ public abstract class Trade implements Tradable, Model { 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()); - getWallet().sync(); + xmrWalletService.syncWallet(getWallet()); pollWallet(); log.info("Done syncing wallet for {} {}", getClass().getSimpleName(), getId()); } @@ -753,7 +771,7 @@ public abstract class Trade implements Tradable, Model { try { syncWallet(); } catch (Exception e) { - if (!isShutDown) { + if (!isShutDown && walletExists()) { log.warn("Error syncing trade wallet for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage()); } } @@ -761,7 +779,7 @@ public abstract class Trade implements Tradable, Model { public void syncWalletNormallyForMs(long syncNormalDuration) { syncNormalStartTime = System.currentTimeMillis(); - setWalletRefreshPeriod(xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs()); + setWalletRefreshPeriod(xmrWalletService.getConnectionsService().getRefreshPeriodMs()); UserThread.runAfter(() -> { if (!isShutDown && System.currentTimeMillis() >= syncNormalStartTime + syncNormalDuration) updateWalletRefreshPeriod(); }, syncNormalDuration); @@ -772,7 +790,11 @@ public abstract class Trade implements Tradable, Model { if (getBuyer().getUpdatedMultisigHex() != null) multisigHexes.add(getBuyer().getUpdatedMultisigHex()); if (getSeller().getUpdatedMultisigHex() != null) multisigHexes.add(getSeller().getUpdatedMultisigHex()); if (getArbitrator().getUpdatedMultisigHex() != null) multisigHexes.add(getArbitrator().getUpdatedMultisigHex()); - if (!multisigHexes.isEmpty()) getWallet().importMultisigHex(multisigHexes.toArray(new String[0])); + if (!multisigHexes.isEmpty()) { + log.info("Importing multisig hex for {} {}", getClass().getSimpleName(), getId()); + getWallet().importMultisigHex(multisigHexes.toArray(new String[0])); + log.info("Done importing multisig hex for {} {}", getClass().getSimpleName(), getId()); + } } public void changeWalletPassword(String oldPassword, String newPassword) { @@ -791,13 +813,22 @@ public abstract class Trade implements Tradable, Model { private void closeWallet() { synchronized (walletLock) { - if (wallet == null) throw new RuntimeException("Trade wallet to close was not previously opened for trade " + getId()); + if (wallet == null) throw new RuntimeException("Trade wallet to close is not open for trade " + getId()); stopPolling(); xmrWalletService.closeWallet(wallet, true); wallet = null; } } + private void stopWallet() { + synchronized (walletLock) { + if (wallet == null) throw new RuntimeException("Trade wallet to close is not open for trade " + getId()); + stopPolling(); + xmrWalletService.stopWallet(wallet, wallet.getPath(), true); + wallet = null; + } + } + public void deleteWallet() { synchronized (walletLock) { if (walletExists()) { @@ -808,14 +839,10 @@ public abstract class Trade implements Tradable, Model { throw new RuntimeException("Refusing to delete wallet for " + getClass().getSimpleName() + " " + getId() + " because the deposit txs have been published but payout tx has not unlocked"); } - // check if wallet balance > dust - BigInteger maxBalance = isDepositsPublished() ? getMakerDepositTx().getFee().min(getTakerDepositTx().getFee()) : BigInteger.ZERO; - if (getWallet().getBalance().compareTo(maxBalance) > 0) { - throw new RuntimeException("Refusing to delete wallet for " + getClass().getSimpleName() + " " + getId() + " because its balance is more than dust"); - } + // force stop the wallet + if (wallet != null) stopWallet(); - // close and delete trade wallet - if (wallet != null) closeWallet(); + // delete wallet log.info("Deleting wallet for {} {}", getClass().getSimpleName(), getId()); xmrWalletService.deleteWallet(getWalletName()); @@ -882,8 +909,7 @@ public abstract class Trade implements Tradable, Model { // check connection to monero daemon checkWalletConnection(); - // import multisig hex - importMultisigHex(); + // check multisig import if (getWallet().isMultisigImportNeeded()) throw new RuntimeException("Cannot create payout tx because multisig import is needed"); // gather info @@ -979,8 +1005,8 @@ public abstract class Trade implements Tradable, Model { BigInteger expectedSellerPayout = sellerDepositAmount.subtract(tradeAmount).subtract(txCost.divide(BigInteger.valueOf(2))); 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's daemon connection - checkWalletConnection(); + // check wallet connection + if (sign || publish) checkWalletConnection(); // handle tx signing if (sign) { @@ -1005,18 +1031,11 @@ public abstract class Trade implements Tradable, Model { // 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 = null; - try { - feeEstimateTx = createPayoutTx(); - } catch (Exception e) { - log.warn("Could not recreate payout tx to verify fee: " + e.getMessage()); - } - if (feeEstimateTx != null) { - 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); - } + 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 @@ -1072,7 +1091,7 @@ public abstract class Trade implements Tradable, Model { if (depositId == null) return null; try { if (trader.getDepositTx() == null || !trader.getDepositTx().isConfirmed()) { - trader.setDepositTx(getTxFromWalletOrDaemon(depositId)); + trader.setDepositTx(getDepositTxFromWalletOrDaemon(depositId)); } return trader.getDepositTx(); } catch (MoneroError e) { @@ -1081,12 +1100,18 @@ public abstract class Trade implements Tradable, Model { } } - private MoneroTx getTxFromWalletOrDaemon(String txId) { + private MoneroTx getDepositTxFromWalletOrDaemon(String txId) { MoneroTx tx = null; + + // first check wallet if (getWallet() != null) { - try { tx = getWallet().getTx(txId); } // TODO monero-java: return null if tx not found - catch (Exception e) { } + List filteredTxs = getWallet().getTxs(new MoneroTxQuery() + .setHash(txId) + .setIsConfirmed(isDepositsConfirmed() ? true : null)); // avoid checking pool if confirmed + if (filteredTxs.size() == 1) tx = filteredTxs.get(0); } + + // then check daemon if (tx == null) tx = getXmrWalletService().getTxWithCache(txId); return tx; } @@ -1126,19 +1151,27 @@ public abstract class Trade implements Tradable, Model { } } - public void shutDown() { + public void onShutDownStarted() { + isShutDownStarted = true; + if (wallet != null) log.info("{} {} onShutDownStarted()", getClass().getSimpleName(), getId()); + synchronized (this) { + synchronized (walletLock) { + stopPolling(); // allow locks to release before stopping + } + } + } + + public void shutDown() { + if (wallet != null) log.info("{} {} onShutDown()", getClass().getSimpleName(), getId()); synchronized (this) { - log.info("Shutting down {} {}", getClass().getSimpleName(), getId()); isInitialized = false; isShutDown = true; - if (wallet != null) closeWallet(); + synchronized (walletLock) { + if (wallet != null) closeWallet(); + } if (tradePhaseSubscription != null) tradePhaseSubscription.unsubscribe(); if (payoutStateSubscription != null) payoutStateSubscription.unsubscribe(); - if (idlePayoutSyncer != null) { - xmrWalletService.removeWalletListener(idlePayoutSyncer); - idlePayoutSyncer = null; - } - log.info("Done shutting down {} {}", getClass().getSimpleName(), getId()); + idlePayoutSyncer = null; // main wallet removes listener itself } } @@ -1280,6 +1313,18 @@ public abstract class Trade implements Tradable, Model { errorMessageProperty.set(errorMessage); } + public void prependErrorMessage(String errorMessage) { + StringBuilder sb = new StringBuilder(); + sb.append(errorMessage); + if (this.errorMessage != null && !this.errorMessage.isEmpty()) { + sb.append("\n\n---- Previous Error -----\n\n"); + sb.append(this.errorMessage); + } + String appendedErrorMessage = sb.toString(); + this.errorMessage = appendedErrorMessage; + errorMessageProperty.set(appendedErrorMessage); + } + public void setAssetTxProofResult(@Nullable AssetTxProofResult assetTxProofResult) { this.assetTxProofResult = assetTxProofResult; assetTxProofResultUpdateProperty.set(assetTxProofResultUpdateProperty.get() + 1); @@ -1434,6 +1479,8 @@ public abstract class Trade implements Tradable, Model { final long tradeTime = getTakeOfferDate().getTime(); MoneroDaemon daemonRpc = xmrWalletService.getDaemon(); if (daemonRpc == null) throw new RuntimeException("Cannot set start time for trade " + getId() + " because it has no connection to monerod"); + if (getMakerDepositTx() == null || getTakerDepositTx() == null) throw new RuntimeException("Cannot set start time for trade " + getId() + " because its unlocked deposit tx is null. Is client connected to a daemon?"); + long maxHeight = Math.max(getMakerDepositTx().getHeight(), getTakerDepositTx().getHeight()); long blockTime = daemonRpc.getBlockByHeight(maxHeight).getTimestamp(); @@ -1465,7 +1512,7 @@ public abstract class Trade implements Tradable, Model { } public boolean isDepositsPublished() { - return getState().getPhase().ordinal() >= Phase.DEPOSITS_PUBLISHED.ordinal(); + return getState().getPhase().ordinal() >= Phase.DEPOSITS_PUBLISHED.ordinal() && getMaker().getDepositTxHash() != null && getTaker().getDepositTxHash() != null; } public boolean isFundsLockedIn() { @@ -1473,11 +1520,11 @@ public abstract class Trade implements Tradable, Model { } public boolean isDepositsConfirmed() { - return getState().getPhase().ordinal() >= Phase.DEPOSITS_CONFIRMED.ordinal(); + return isDepositsPublished() && getState().getPhase().ordinal() >= Phase.DEPOSITS_CONFIRMED.ordinal(); } public boolean isDepositsUnlocked() { - return getState().getPhase().ordinal() >= Phase.DEPOSITS_UNLOCKED.ordinal(); + return isDepositsPublished() && getState().getPhase().ordinal() >= Phase.DEPOSITS_UNLOCKED.ordinal(); } public boolean isPaymentSent() { @@ -1616,7 +1663,7 @@ public abstract class Trade implements Tradable, Model { */ public long getReprocessDelayInSeconds(int reprocessCount) { int retryCycles = 3; // reprocess on next refresh periods for first few attempts (app might auto switch to a good connection) - if (reprocessCount < retryCycles) return xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs() / 1000; + if (reprocessCount < retryCycles) return xmrWalletService.getConnectionsService().getRefreshPeriodMs() / 1000; long delay = 60; for (int i = retryCycles; i < reprocessCount; i++) delay *= 2; return Math.min(MAX_REPROCESS_DELAY_SECONDS, delay); @@ -1642,15 +1689,27 @@ public abstract class Trade implements Tradable, Model { return tradeVolumeProperty; } - private void setDaemonConnection(MoneroRpcConnection connection) { + private void onConnectionChanged(MoneroRpcConnection connection) { synchronized (walletLock) { - if (isShutDown) return; - MoneroWallet wallet = getWallet(); - if (wallet == null) return; + + // check if ignored + if (isShutDownStarted) return; + if (getWallet() == null) return; + if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) return; + + // set daemon connection (must restart monero-wallet-rpc if proxy uri changed) + String oldProxyUri = wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getProxyUri(); + String newProxyUri = connection == null ? null : connection.getProxyUri(); log.info("Setting daemon connection for trade wallet {}: {}", getId() , connection == null ? null : connection.getUri()); - wallet.setDaemonConnection(connection); + if (wallet instanceof MoneroWalletRpc && !StringUtils.equals(oldProxyUri, newProxyUri)) { + log.info("Restarting monero-wallet-rpc for trade wallet to set proxy URI {}: {}", getId() , connection == null ? null : connection.getUri()); + closeWallet(); + wallet = getWallet(); + } else { + wallet.setDaemonConnection(connection); + } updateWalletRefreshPeriod(); - + // sync and reprocess messages on new thread if (connection != null && !Boolean.FALSE.equals(connection.isConnected())) { HavenoUtils.submitTask(() -> { @@ -1665,14 +1724,14 @@ public abstract class Trade implements Tradable, Model { } private void updateSyncing() { - if (isShutDown) return; + if (isShutDownStarted) return; if (!isIdling()) { updateWalletRefreshPeriod(); trySyncWallet(); } else { long startSyncingInMs = ThreadLocalRandom.current().nextLong(0, getWalletRefreshPeriod()); // random time to start syncing UserThread.runAfter(() -> { - if (!isShutDown) { + if (!isShutDownStarted) { updateWalletRefreshPeriod(); trySyncWallet(); } @@ -1680,13 +1739,13 @@ public abstract class Trade implements Tradable, Model { } } - private void updateWalletRefreshPeriod() { + public void updateWalletRefreshPeriod() { setWalletRefreshPeriod(getWalletRefreshPeriod()); } private void setWalletRefreshPeriod(long walletRefreshPeriod) { synchronized (walletLock) { - if (this.isShutDown) return; + if (this.isShutDownStarted) return; if (this.walletRefreshPeriod != null && this.walletRefreshPeriod == walletRefreshPeriod) return; this.walletRefreshPeriod = walletRefreshPeriod; if (getWallet() != null) { @@ -1702,7 +1761,7 @@ public abstract class Trade implements Tradable, Model { synchronized (walletLock) { if (txPollLooper != null) return; log.info("Starting to poll wallet for {} {}", getClass().getSimpleName(), getId()); - txPollLooper = new TaskLooper(() -> { pollWallet(); }); + txPollLooper = new TaskLooper(() -> pollWallet()); txPollLooper.start(walletRefreshPeriod); } } @@ -1719,53 +1778,61 @@ public abstract class Trade implements Tradable, Model { private void pollWallet() { try { + // skip if either deposit tx id is unknown + if (processModel.getMaker().getDepositTxHash() == null || processModel.getTaker().getDepositTxHash() == null) return; + // skip if payout unlocked if (isPayoutUnlocked()) return; - // rescan spent if deposits unlocked - if (isDepositsUnlocked()) getWallet().rescanSpent(); + // rescan spent outputs to detect payout tx after deposits unlocked + if (isDepositsUnlocked() && !isPayoutPublished()) getWallet().rescanSpent(); - // get txs with outputs - List txs; - try { - txs = getWallet().getTxs(new MoneroTxQuery() - .setHashes(Arrays.asList(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash())) - .setIncludeOutputs(true)); - } catch (Exception e) { - if (!isShutDown) log.info("Could not fetch deposit txs from wallet for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage()); // expected at first - return; + // get txs from trade wallet + boolean payoutExpected = isPaymentReceived() || processModel.getPaymentReceivedMessage() != null || disputeState.ordinal() > DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG.ordinal() || processModel.getDisputeClosedMessage() != null; + boolean checkPool = !isDepositsConfirmed() || (!isPayoutConfirmed() && payoutExpected); + MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true); + if (!checkPool) query.setInTxPool(false); // avoid pool check if possible + List txs = wallet.getTxs(query); + + // warn on double spend // TODO: other handling? + for (MoneroTxWallet tx : txs) { + if (Boolean.TRUE.equals(tx.isDoubleSpendSeen())) log.warn("Double spend seen for tx {} for {} {}", tx.getHash(), getClass().getSimpleName(), getId()); } // check deposit txs if (!isDepositsUnlocked()) { - if (txs.size() == 2) { - - // update trader state - boolean makerFirst = txs.get(0).getHash().equals(processModel.getMaker().getDepositTxHash()); - getMaker().setDepositTx(makerFirst ? txs.get(0) : txs.get(1)); - getTaker().setDepositTx(makerFirst ? txs.get(1) : txs.get(0)); - - // set security deposits - if (getBuyer().getSecurityDeposit().longValueExact() == 0) { - BigInteger buyerSecurityDeposit = ((MoneroTxWallet) getBuyer().getDepositTx()).getIncomingAmount(); - BigInteger sellerSecurityDeposit = ((MoneroTxWallet) getSeller().getDepositTx()).getIncomingAmount().subtract(getAmount()); - getBuyer().setSecurityDeposit(buyerSecurityDeposit); - getSeller().setSecurityDeposit(sellerSecurityDeposit); - } - - // set deposits published state - setStateDepositsPublished(); - - // check if deposit txs confirmed - if (txs.get(0).isConfirmed() && txs.get(1).isConfirmed()) setStateDepositsConfirmed(); - if (!txs.get(0).isLocked() && !txs.get(1).isLocked()) setStateDepositsUnlocked(); + + // update trader txs + MoneroTxWallet makerDepositTx = null; + MoneroTxWallet takerDepositTx = null; + for (MoneroTxWallet tx : txs) { + if (tx.getHash().equals(processModel.getMaker().getDepositTxHash())) makerDepositTx = tx; + if (tx.getHash().equals(processModel.getTaker().getDepositTxHash())) takerDepositTx = tx; } + getMaker().setDepositTx(makerDepositTx); + getTaker().setDepositTx(takerDepositTx); + + // skip if deposit txs not seen + if (makerDepositTx == null || takerDepositTx == null) return; + + // set security deposits + if (getBuyer().getSecurityDeposit().longValueExact() == 0) { + BigInteger buyerSecurityDeposit = ((MoneroTxWallet) getBuyer().getDepositTx()).getIncomingAmount(); + BigInteger sellerSecurityDeposit = ((MoneroTxWallet) getSeller().getDepositTx()).getIncomingAmount().subtract(getAmount()); + getBuyer().setSecurityDeposit(buyerSecurityDeposit); + getSeller().setSecurityDeposit(sellerSecurityDeposit); + } + + // update state + setStateDepositsPublished(); + if (makerDepositTx.isConfirmed() && takerDepositTx.isConfirmed()) setStateDepositsConfirmed(); + if (!makerDepositTx.isLocked() && !takerDepositTx.isLocked()) setStateDepositsUnlocked(); } // check payout tx - else { + if (isDepositsUnlocked()) { - // check if deposit txs spent (appears on payout published) + // check if any outputs spent (observed on payout published) for (MoneroTxWallet tx : txs) { for (MoneroOutputWallet output : tx.getOutputsWallet()) { if (Boolean.TRUE.equals(output.isSpent())) { @@ -1775,19 +1842,18 @@ public abstract class Trade implements Tradable, Model { } // check for outgoing txs (appears after wallet submits payout tx or on payout confirmed) - List outgoingTxs = getWallet().getTxs(new MoneroTxQuery().setIsOutgoing(true)); - if (!outgoingTxs.isEmpty()) { - MoneroTxWallet payoutTx = outgoingTxs.get(0); - setPayoutTx(payoutTx); - setPayoutStatePublished(); - if (payoutTx.isConfirmed()) setPayoutStateConfirmed(); - if (!payoutTx.isLocked()) setPayoutStateUnlocked(); + for (MoneroTxWallet tx : txs) { + if (tx.isOutgoing()) { + setPayoutTx(tx); + setPayoutStatePublished(); + if (tx.isConfirmed()) setPayoutStateConfirmed(); + if (!tx.isLocked()) setPayoutStateUnlocked(); + } } } } catch (Exception e) { if (!isShutDown && getWallet() != null && isWalletConnected()) { log.warn("Error polling trade wallet {}: {}", getId(), e.getMessage()); - e.printStackTrace(); } } } @@ -1795,7 +1861,7 @@ public abstract class Trade implements Tradable, Model { private long getWalletRefreshPeriod() { if (isIdling()) return IDLE_SYNC_PERIOD_MS; - return xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs(); + return xmrWalletService.getConnectionsService().getRefreshPeriodMs(); } private void setStateDepositsPublished() { @@ -1867,7 +1933,7 @@ public abstract class Trade implements Tradable, Model { } catch (Exception e) { processing = false; e.printStackTrace(); - if (isInitialized && !isShutDown && !isWalletConnected()) throw e; + if (isInitialized && !isShutDownStarted && !isWalletConnected()) throw e; } }); } diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index 4eab1740..b565f0c1 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -18,6 +18,8 @@ package haveno.core.trade; import com.google.common.collect.ImmutableList; + +import common.utils.GenUtils; import haveno.common.ClockWatcher; import haveno.common.UserThread; import haveno.common.crypto.KeyRing; @@ -67,6 +69,7 @@ import haveno.core.user.User; import haveno.core.util.Validator; import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.wallet.XmrWalletService; +import haveno.network.p2p.BootstrapListener; import haveno.network.p2p.DecryptedDirectMessageListener; import haveno.network.p2p.DecryptedMessageWithPubKey; import haveno.network.p2p.NodeAddress; @@ -85,6 +88,7 @@ import monero.wallet.model.MoneroOutputQuery; import org.bitcoinj.core.Coin; import org.bouncycastle.crypto.params.KeyParameter; import org.fxmisc.easybind.EasyBind; +import org.fxmisc.easybind.monadic.MonadicBinding; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -198,6 +202,13 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi p2PService.addDecryptedDirectMessageListener(this); failedTradesManager.setUnFailTradeCallback(this::unFailTrade); + + // initialize trades when connected to p2p network + p2PService.addP2PServiceListener(new BootstrapListener() { + public void onUpdatedDataReceived() { + initPersistedTrades(); + } + }); } @@ -249,27 +260,24 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi public void onAllServicesInitialized() { - // initialize - initialize(); - // listen for account updates accountService.addListener(new AccountServiceListener() { @Override public void onAccountCreated() { - log.info(getClass().getSimpleName() + ".accountService.onAccountCreated()"); - initialize(); + log.info(TradeManager.class + ".accountService.onAccountCreated()"); + initPersistedTrades(); } @Override public void onAccountOpened() { - log.info(getClass().getSimpleName() + ".accountService.onAccountOpened()"); - initialize(); + log.info(TradeManager.class + ".accountService.onAccountOpened()"); + initPersistedTrades(); } @Override public void onAccountClosed() { - log.info(getClass().getSimpleName() + ".accountService.onAccountClosed()"); + log.info(TradeManager.class + ".accountService.onAccountClosed()"); closeAllTrades(); } @@ -280,21 +288,36 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi }); } - private void initialize() { + public void onShutDownStarted() { + log.info("{}.onShutDownStarted()", getClass().getSimpleName()); - // initialize trades off main thread - new Thread(() -> initPersistedTrades()).start(); + // collect trades to prepare + Set trades = new HashSet(); + trades.addAll(tradableList.getList()); + trades.addAll(closedTradableManager.getClosedTrades()); + trades.addAll(failedTradesManager.getObservableList()); - getObservableList().addListener((ListChangeListener) change -> onTradesChanged()); - onTradesChanged(); - - xmrWalletService.setTradeManager(this); - - // thaw unreserved outputs - thawUnreservedOutputs(); + // prepare to shut down trades in parallel + Set tasks = new HashSet(); + for (Trade trade : trades) tasks.add(() -> { + try { + trade.onShutDownStarted(); + } catch (Exception e) { + if (e.getMessage() != null && e.getMessage().contains("Connection reset")) return; // expected if shut down with ctrl+c + log.warn("Error notifying {} {} that shut down started {}", getClass().getSimpleName(), trade.getId()); + e.printStackTrace(); + } + }); + try { + HavenoUtils.executeTasks(tasks); + } catch (Exception e) { + log.warn("Error notifying trades that shut down started: {}", e.getMessage()); + e.printStackTrace(); + } } public void shutDown() { + log.info("Shutting down {}", getClass().getSimpleName()); isShutDown = true; closeAllTrades(); } @@ -313,7 +336,9 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi try { trade.shutDown(); } catch (Exception e) { - log.warn("Error closing trade subprocess. Was Haveno stopped manually with ctrl+c?"); + if (e.getMessage() != null && (e.getMessage().contains("Connection reset") || e.getMessage().contains("Connection refused"))) return; // expected if shut down with ctrl+c + log.warn("Error closing {} {}", trade.getClass().getSimpleName(), trade.getId()); + e.printStackTrace(); } }); try { @@ -372,54 +397,80 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi /////////////////////////////////////////////////////////////////////////////////////////// private void initPersistedTrades() { + log.info("Initializing persisted trades"); - // get all trades - List trades = getAllTrades(); + // initialize off main thread + new Thread(() -> { - // open trades in parallel since each may open a multisig wallet - log.info("Initializing trades"); - int threadPoolSize = 10; - Set tasks = new HashSet(); - for (Trade trade : trades) { - tasks.add(new Runnable() { - @Override - public void run() { + // get all trades + List trades = getAllTrades(); + + // initialize trades in parallel + int threadPoolSize = 10; + Set tasks = new HashSet(); + for (Trade trade : trades) { + tasks.add(() -> { initPersistedTrade(trade); - } - }); - }; - HavenoUtils.executeTasks(tasks, threadPoolSize); - log.info("Done initializing trades"); - // reset any available address entries - if (isShutDown) return; - xmrWalletService.getAddressEntriesForAvailableBalanceStream() - .filter(addressEntry -> addressEntry.getOfferId() != null) - .forEach(addressEntry -> { - log.warn("Swapping pending {} entries at startup. offerId={}", addressEntry.getContext(), addressEntry.getOfferId()); - xmrWalletService.swapTradeEntryToAvailableEntry(addressEntry.getOfferId(), addressEntry.getContext()); + // remove trade if protocol didn't initialize + if (getOpenTrade(trade.getId()).isPresent() && !trade.isDepositRequested()) { + log.warn("Removing persisted {} {} with uid={} because it did not finish initializing (state={})", trade.getClass().getSimpleName(), trade.getId(), trade.getUid(), trade.getState()); + removeTradeOnError(trade); + } }); + }; + HavenoUtils.executeTasks(tasks, threadPoolSize); + log.info("Done initializing persisted trades"); + if (isShutDown) return; - // notify that persisted trades initialized - persistedTradesInitialized.set(true); - - // We do not include failed trades as they should not be counted anyway in the trade statistics - Set nonFailedTrades = new HashSet<>(closedTradableManager.getClosedTrades()); - nonFailedTrades.addAll(tradableList.getList()); - String referralId = referralIdService.getOptionalReferralId().orElse(null); - boolean isTorNetworkNode = p2PService.getNetworkNode() instanceof TorNetworkNode; - tradeStatisticsManager.maybeRepublishTradeStatistics(nonFailedTrades, referralId, isTorNetworkNode); - - // sync idle trades once in background after active trades - for (Trade trade : trades) { - if (trade.isIdling()) { - HavenoUtils.submitTask(() -> trade.syncWallet()); + // sync idle trades once in background after active trades + for (Trade trade : trades) { + if (trade.isIdling()) { + HavenoUtils.submitTask(() -> trade.syncWallet()); + } } - } + + getObservableList().addListener((ListChangeListener) change -> onTradesChanged()); + onTradesChanged(); + + xmrWalletService.setTradeManager(this); + + // process after all wallets initialized + MonadicBinding walletsInitialized = EasyBind.combine(HavenoUtils.havenoSetup.getWalletInitialized(), persistedTradesInitialized, (a, b) -> a && b); + walletsInitialized.subscribe((observable, oldValue, newValue) -> { + if (!newValue) return; + + // thaw unreserved outputs + thawUnreservedOutputs(); + + // reset any available funded address entries + xmrWalletService.getAddressEntriesForAvailableBalanceStream() + .filter(addressEntry -> addressEntry.getOfferId() != null) + .forEach(addressEntry -> { + log.warn("Swapping pending {} entries at startup. offerId={}", addressEntry.getContext(), addressEntry.getOfferId()); + xmrWalletService.swapTradeEntryToAvailableEntry(addressEntry.getOfferId(), addressEntry.getContext()); + }); + }); + + // notify that persisted trades initialized + persistedTradesInitialized.set(true); + + // We do not include failed trades as they should not be counted anyway in the trade statistics + // TODO: remove stats? + Set nonFailedTrades = new HashSet<>(closedTradableManager.getClosedTrades()); + nonFailedTrades.addAll(tradableList.getList()); + String referralId = referralIdService.getOptionalReferralId().orElse(null); + boolean isTorNetworkNode = p2PService.getNetworkNode() instanceof TorNetworkNode; + tradeStatisticsManager.maybeRepublishTradeStatistics(nonFailedTrades, referralId, isTorNetworkNode); + }).start(); + + // allow execution to start + GenUtils.waitFor(100); } private void initPersistedTrade(Trade trade) { if (isShutDown) return; + if (getTradeProtocol(trade) != null) return; initTradeAndProtocol(trade, createTradeProtocol(trade)); requestPersistence(); scheduleDeletionIfUnfunded(trade); @@ -1100,16 +1151,16 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } private void addTrade(Trade trade) { - synchronized(tradableList) { + synchronized (tradableList) { if (tradableList.add(trade)) { requestPersistence(); } } } - private synchronized void removeTrade(Trade trade) { + private void removeTrade(Trade trade) { log.info("TradeManager.removeTrade() " + trade.getId()); - synchronized(tradableList) { + synchronized (tradableList) { if (!tradableList.contains(trade)) return; // remove trade @@ -1121,9 +1172,9 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } } - private synchronized void removeTradeOnError(Trade trade) { + private void removeTradeOnError(Trade trade) { log.info("TradeManager.removeTradeOnError() " + trade.getId()); - synchronized(tradableList) { + synchronized (tradableList) { if (!tradableList.contains(trade)) return; // unreserve taker key images @@ -1150,18 +1201,18 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } private void scheduleDeletionIfUnfunded(Trade trade) { - if (trade.isDepositRequested() && !trade.isDepositsPublished()) { - log.warn("Scheduling to delete trade if unfunded for {} {}", trade.getClass().getSimpleName(), trade.getId()); + if (getOpenTrade(trade.getId()).isPresent() && trade.isDepositRequested() && !trade.isDepositsPublished()) { + log.warn("Scheduling to delete open trade if unfunded for {} {}", trade.getClass().getSimpleName(), trade.getId()); UserThread.runAfter(() -> { if (isShutDown) return; // get trade's deposit txs from daemon - MoneroTx makerDepositTx = xmrWalletService.getDaemon().getTx(trade.getMaker().getDepositTxHash()); - MoneroTx takerDepositTx = xmrWalletService.getDaemon().getTx(trade.getTaker().getDepositTxHash()); + MoneroTx makerDepositTx = trade.getMaker().getDepositTxHash() == null ? null : xmrWalletService.getDaemon().getTx(trade.getMaker().getDepositTxHash()); + MoneroTx takerDepositTx = trade.getTaker().getDepositTxHash() == null ? null : xmrWalletService.getDaemon().getTx(trade.getTaker().getDepositTxHash()); // delete multisig trade wallet if neither deposit tx published if (makerDepositTx == null && takerDepositTx == null) { - log.warn("Deleting {} {} after protocol timeout", trade.getClass().getSimpleName(), trade.getId()); + log.warn("Deleting {} {} after protocol error", trade.getClass().getSimpleName(), trade.getId()); removeTrade(trade); failedTradesManager.removeTrade(trade); if (trade.walletExists()) trade.deleteWallet(); diff --git a/core/src/main/java/haveno/core/trade/messages/InitTradeRequest.java b/core/src/main/java/haveno/core/trade/messages/InitTradeRequest.java index d103c9a0..810e9c28 100644 --- a/core/src/main/java/haveno/core/trade/messages/InitTradeRequest.java +++ b/core/src/main/java/haveno/core/trade/messages/InitTradeRequest.java @@ -182,7 +182,7 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag ",\n reserveTxHex=" + reserveTxHex + ",\n reserveTxKey=" + reserveTxKey + ",\n payoutAddress=" + payoutAddress + - ",\n makerSignature=" + Utilities.byteArrayToInteger(makerSignature) + + ",\n makerSignature=" + (makerSignature == null ? null : Utilities.byteArrayToInteger(makerSignature)) + "\n} " + super.toString(); } } diff --git a/core/src/main/java/haveno/core/trade/messages/PaymentSentMessage.java b/core/src/main/java/haveno/core/trade/messages/PaymentSentMessage.java index 04a8e71f..a9c8f10a 100644 --- a/core/src/main/java/haveno/core/trade/messages/PaymentSentMessage.java +++ b/core/src/main/java/haveno/core/trade/messages/PaymentSentMessage.java @@ -141,10 +141,11 @@ public final class PaymentSentMessage extends TradeMailboxMessage { @Override public String toString() { return "PaymentSentMessage{" + + ",\n tradeId=" + tradeId + + ",\n uid='" + uid + '\'' + ",\n senderNodeAddress=" + senderNodeAddress + ",\n counterCurrencyTxId=" + counterCurrencyTxId + ",\n counterCurrencyExtraData=" + counterCurrencyExtraData + - ",\n uid='" + uid + '\'' + ",\n payoutTxHex=" + payoutTxHex + ",\n updatedMultisigHex=" + updatedMultisigHex + ",\n paymentAccountKey=" + paymentAccountKey + diff --git a/core/src/main/java/haveno/core/trade/protocol/FluentProtocol.java b/core/src/main/java/haveno/core/trade/protocol/FluentProtocol.java index e03dcf99..f51fac17 100644 --- a/core/src/main/java/haveno/core/trade/protocol/FluentProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/FluentProtocol.java @@ -324,7 +324,7 @@ public class FluentProtocol { log.info(info); return Result.VALID.info(info); } else { - String info = MessageFormat.format("We received a {0} but we are are not in the expected state. " + + String info = MessageFormat.format("We received a {0} but we are not in the expected state. " + "Expected states={1}, Trade state= {2}, tradeId={3}", trigger, expectedStates, 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 42bb11b2..dc0b2356 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -52,7 +52,7 @@ 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.RemoveOffer; -import haveno.core.trade.protocol.tasks.ResendDisputeClosedMessageWithPayout; +import haveno.core.trade.protocol.tasks.MaybeResendDisputeClosedMessageWithPayout; import haveno.core.trade.protocol.tasks.TradeTask; import haveno.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness; import haveno.core.util.Validator; @@ -242,32 +242,19 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } protected void onInitialized() { - if (!trade.isCompleted()) { - processModel.getP2PService().addDecryptedDirectMessageListener(this); - } - // initialize trade - trade.initialize(processModel.getProvider()); + // listen for direct messages unless completed + if (!trade.isCompleted()) processModel.getP2PService().addDecryptedDirectMessageListener(this); + + // initialize trade with lock + synchronized (trade) { + trade.initialize(processModel.getProvider()); + } // process mailbox messages MailboxMessageService mailboxMessageService = processModel.getP2PService().getMailboxMessageService(); - mailboxMessageService.addDecryptedMailboxListener(this); + if (!trade.isCompleted()) mailboxMessageService.addDecryptedMailboxListener(this); handleMailboxCollection(mailboxMessageService.getMyDecryptedMailboxMessages()); - - // send deposit confirmed message on startup or event - if (trade.getState().ordinal() >= Trade.State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN.ordinal()) { - new Thread(() -> sendDepositsConfirmedMessages()).start(); - } else { - EasyBind.subscribe(trade.stateProperty(), state -> { - if (state == Trade.State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN) { - new Thread(() -> sendDepositsConfirmedMessages()).start(); - } - }); - } - - // reprocess payout messages if pending - maybeReprocessPaymentReceivedMessage(true); - HavenoUtils.arbitrationManager.maybeReprocessDisputeClosedMessage(trade, true); } public void maybeReprocessPaymentReceivedMessage(boolean reprocessOnError) { @@ -448,7 +435,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D .setup(tasks( ProcessDepositsConfirmedMessage.class, VerifyPeersAccountAgeWitness.class, - ResendDisputeClosedMessageWithPayout.class) + MaybeResendDisputeClosedMessageWithPayout.class) .using(new TradeTaskRunner(trade, () -> { handleTaskRunnerSuccess(sender, response); @@ -475,7 +462,8 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D // the mailbox msg once wallet is ready and trade state set. synchronized (trade) { if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_SENT.ordinal()) { - log.warn("Ignoring PaymentSentMessage which was already processed"); + log.warn("Received another PaymentSentMessage which was already processed, ACKing"); + handleTaskRunnerSuccess(peer, message); return; } latchTrade(); @@ -518,6 +506,11 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D return; } synchronized (trade) { + if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_RECEIVED.ordinal()) { + log.warn("Received another PaymentReceivedMessage which was already processed, ACKing"); + handleTaskRunnerSuccess(peer, message); + return; + } latchTrade(); Validator.checkTradeId(processModel.getOfferId(), message); processModel.setTradeMessage(message); @@ -844,7 +837,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } } - private void sendDepositsConfirmedMessages() { + public void maybeSendDepositsConfirmedMessages() { synchronized (trade) { if (!trade.isInitialized()) return; // skip if shutting down if (trade.getProcessModel().isDepositsConfirmedMessagesDelivered()) return; // skip if already delivered @@ -860,7 +853,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D // retry in 15 minutes UserThread.runAfter(() -> { - sendDepositsConfirmedMessages(); + maybeSendDepositsConfirmedMessages(); }, 15, TimeUnit.MINUTES); handleTaskRunnerFault(null, null, "SendDepositsConfirmedMessages", errorMessage); }))) diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequest.java index f34ee969..05e65cfa 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/MakerSendInitTradeRequest.java @@ -70,7 +70,7 @@ public class MakerSendInitTradeRequest extends TradeTask { trade.getSelf().getReserveTxHash(), trade.getSelf().getReserveTxHex(), trade.getSelf().getReserveTxKey(), - model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(), + model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.RESERVED_FOR_TRADE).get().getAddressString(), null); // send request to arbitrator diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ResendDisputeClosedMessageWithPayout.java b/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeResendDisputeClosedMessageWithPayout.java similarity index 95% rename from core/src/main/java/haveno/core/trade/protocol/tasks/ResendDisputeClosedMessageWithPayout.java rename to core/src/main/java/haveno/core/trade/protocol/tasks/MaybeResendDisputeClosedMessageWithPayout.java index 9931c6d1..30c3e3d8 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ResendDisputeClosedMessageWithPayout.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeResendDisputeClosedMessageWithPayout.java @@ -32,10 +32,10 @@ import java.util.List; import static com.google.common.base.Preconditions.checkNotNull; @Slf4j -public class ResendDisputeClosedMessageWithPayout extends TradeTask { +public class MaybeResendDisputeClosedMessageWithPayout extends TradeTask { @SuppressWarnings({"unused"}) - public ResendDisputeClosedMessageWithPayout(TaskRunner taskHandler, Trade trade) { + public MaybeResendDisputeClosedMessageWithPayout(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } 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 0b34f2aa..7d581e45 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 @@ -83,7 +83,7 @@ public class MaybeSendSignContractRequest extends TradeTask { trade.getSelf().setDepositTx(depositTx); trade.getSelf().setDepositTxHash(depositTx.getHash()); trade.getSelf().setReserveTxKeyImages(reservedKeyImages); - trade.getSelf().setPayoutAddressString(trade.getXmrWalletService().getAddressEntry(processModel.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString()); // TODO (woodser): allow custom payout address? + 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())); // maker signs deposit hash nonce to avoid challenge protocol 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 add97ee2..55b60f9e 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 @@ -53,14 +53,21 @@ 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()); } + processModel.getTradeManager().requestPersistence(); // in case importing multisig hex fails + + // update multisig hex + sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex()); + try { + trade.importMultisigHex(); + } catch (Exception e) { + log.warn("Error importing multisig hex for {} {}: {}", trade.getClass().getSimpleName(), trade.getId(), e.getMessage()); + e.printStackTrace(); + } // persist and complete processModel.getTradeManager().requestPersistence(); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessInitMultisigRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessInitMultisigRequest.java index 70b06ab5..15ee01b2 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessInitMultisigRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessInitMultisigRequest.java @@ -111,6 +111,7 @@ public class ProcessInitMultisigRequest extends TradeTask { processModel.setMultisigAddress(result.getAddress()); trade.saveWallet(); // save multisig wallet on completion trade.setStateIfValidTransitionTo(Trade.State.MULTISIG_COMPLETED); + trade.updateWalletRefreshPeriod(); // starts syncing } // update multisig participants if new state to communicate 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 580424ab..343dcdec 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 @@ -113,7 +113,8 @@ public class ProcessPaymentReceivedMessage extends TradeTask { // wait to sign and publish payout tx if defer flag set (seller recently saw payout tx arrive at buyer) boolean isSigned = message.getSignedPayoutTxHex() != null; - if (trade instanceof ArbitratorTrade && !isSigned && message.isDeferPublishPayout()) { + boolean deferSignAndPublish = trade instanceof ArbitratorTrade && !isSigned && message.isDeferPublishPayout(); + if (deferSignAndPublish) { log.info("Deferring signing and publishing payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId()); GenUtils.waitFor(Trade.DEFER_PUBLISH_MS); if (!trade.isPayoutUnlocked()) trade.syncWallet(); @@ -135,6 +136,7 @@ public class ProcessPaymentReceivedMessage extends TradeTask { trade.verifyPayoutTx(trade.getPayoutTxHex(), false, true); } } catch (Exception e) { + trade.syncWallet(); if (trade.isPayoutPublished()) log.info("Payout tx already published for {} {}", trade.getClass().getName(), trade.getId()); else throw e; } 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 0d9261e2..0079d281 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 @@ -47,10 +47,7 @@ public class ProcessPaymentSentMessage extends TradeTask { // update latest peer address trade.getBuyer().setNodeAddress(processModel.getTempTradePeerNodeAddress()); - // if seller, decrypt buyer's payment account payload - if (trade.isSeller()) trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey()); - - // update state + // update state from message processModel.setPaymentSentMessage(message); trade.setPayoutTxHex(message.getPayoutTxHex()); trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex()); @@ -59,6 +56,20 @@ public class ProcessPaymentSentMessage extends TradeTask { if (counterCurrencyTxId != null && counterCurrencyTxId.length() < 100) trade.setCounterCurrencyTxId(counterCurrencyTxId); String counterCurrencyExtraData = message.getCounterCurrencyExtraData(); if (counterCurrencyExtraData != null && counterCurrencyExtraData.length() < 100) trade.setCounterCurrencyExtraData(counterCurrencyExtraData); + + // if seller, decrypt buyer's payment account payload + if (trade.isSeller()) trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey()); + trade.requestPersistence(); + + // import multisig hex + try { + trade.importMultisigHex(); + } catch (Exception e) { + log.warn("Error importing multisig hex for {} {}: {}", trade.getClass().getSimpleName(), trade.getId(), e.getMessage()); + e.printStackTrace(); + } + + // update state trade.advanceState(trade.isSeller() ? Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG : Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); trade.requestPersistence(); complete(); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SendMailboxMessageTask.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SendMailboxMessageTask.java index a8db771e..be107ed4 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SendMailboxMessageTask.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SendMailboxMessageTask.java @@ -58,8 +58,8 @@ public abstract class SendMailboxMessageTask extends TradeTask { TradeMailboxMessage message = getTradeMailboxMessage(id); setStateSent(); NodeAddress peersNodeAddress = getReceiverNodeAddress(); - log.info("Send {} to peer {} for {} {}", trade.getClass().getSimpleName(), trade.getId(), - message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + log.info("Send {} to peer {} for {} {}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, trade.getClass().getSimpleName(), trade.getId(), message.getUid()); TradeTask task = this; processModel.getP2PService().getMailboxMessageService().sendEncryptedMailboxMessage( 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 f7797785..46038c4a 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java @@ -43,7 +43,7 @@ public class TakerReserveTradeFunds extends TradeTask { BigInteger takerFee = trade.getTakerFee(); BigInteger sendAmount = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getOffer().getAmount() : BigInteger.valueOf(0); BigInteger securityDeposit = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getOffer().getSellerSecurityDeposit() : trade.getOffer().getBuyerSecurityDeposit(); - String returnAddress = model.getXmrWalletService().getAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(); + String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(takerFee, sendAmount, securityDeposit, returnAddress); // collect reserved key images diff --git a/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java b/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java index 461b605c..86ac9f2e 100644 --- a/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java +++ b/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java @@ -78,7 +78,7 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl extraDataMap.put(OfferPayload.REFERRAL_ID, referralId); } - NodeAddress arbitratorNodeAddress = checkNotNull(trade.getArbitrator().getNodeAddress()); + NodeAddress arbitratorNodeAddress = checkNotNull(trade.getArbitrator().getNodeAddress(), "Arbitrator address is null", trade.getClass().getSimpleName(), trade.getId()); // The first 4 chars are sufficient to identify an arbitrator. // For testing with regtest/localhost we use the full address as its localhost and would result in diff --git a/core/src/main/java/haveno/core/trade/statistics/TradeStatisticsManager.java b/core/src/main/java/haveno/core/trade/statistics/TradeStatisticsManager.java index 08eb927a..3a720c7e 100644 --- a/core/src/main/java/haveno/core/trade/statistics/TradeStatisticsManager.java +++ b/core/src/main/java/haveno/core/trade/statistics/TradeStatisticsManager.java @@ -170,7 +170,13 @@ public class TradeStatisticsManager { return; } - TradeStatistics3 tradeStatistics3 = TradeStatistics3.from(trade, referralId, isTorNetworkNode); + TradeStatistics3 tradeStatistics3 = null; + try { + tradeStatistics3 = TradeStatistics3.from(trade, referralId, isTorNetworkNode); + } catch (Exception e) { + log.warn("Error getting trade statistic for {} {}: {}", trade.getClass().getName(), trade.getId(), e.getMessage()); + return; + } boolean hasTradeStatistics3 = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics3.getHash())); if (hasTradeStatistics3) { log.debug("Trade: {}. We have already a tradeStatistics matching the hash of tradeStatistics3.", diff --git a/core/src/main/java/haveno/core/xmr/Balances.java b/core/src/main/java/haveno/core/xmr/Balances.java index 3b48759a..be607c02 100644 --- a/core/src/main/java/haveno/core/xmr/Balances.java +++ b/core/src/main/java/haveno/core/xmr/Balances.java @@ -37,6 +37,7 @@ import lombok.extern.slf4j.Slf4j; import monero.common.MoneroError; import monero.wallet.model.MoneroOutputQuery; import monero.wallet.model.MoneroOutputWallet; +import monero.wallet.model.MoneroTxQuery; import monero.wallet.model.MoneroTxWallet; import javax.inject.Inject; @@ -127,8 +128,10 @@ public class Balances { List openTrades = tradeManager.getTradesStreamWithFundsLockedIn().collect(Collectors.toList()); for (Trade trade : openTrades) { try { - MoneroTxWallet depositTx = xmrWalletService.getWallet().getTx(trade.getSelf().getDepositTxHash()); - if (!depositTx.isConfirmed()) continue; // outputs are frozen until confirmed by arbitrator's broadcast + List depositTxs = xmrWalletService.getWallet().getTxs(new MoneroTxQuery() + .setHash(trade.getSelf().getDepositTxHash()) + .setInTxPool(false)); // don't check pool + if (depositTxs.size() != 1 || !depositTxs.get(0).isConfirmed()) continue; // outputs are frozen until confirmed by arbitrator's broadcast } catch (MoneroError e) { continue; } diff --git a/core/src/main/java/haveno/core/xmr/setup/MoneroWalletRpcManager.java b/core/src/main/java/haveno/core/xmr/setup/MoneroWalletRpcManager.java index c59bd919..3afcb915 100644 --- a/core/src/main/java/haveno/core/xmr/setup/MoneroWalletRpcManager.java +++ b/core/src/main/java/haveno/core/xmr/setup/MoneroWalletRpcManager.java @@ -17,135 +17,144 @@ import java.util.Map; @Slf4j public class MoneroWalletRpcManager { - private static final String RPC_BIND_PORT_ARGUMENT = "--rpc-bind-port"; - private static int NUM_ALLOWED_ATTEMPTS = 2; // allow this many attempts to bind to an assigned port - private Integer startPort; - private final Map registeredPorts = new HashMap<>(); + private static final String RPC_BIND_PORT_ARGUMENT = "--rpc-bind-port"; + private static int NUM_ALLOWED_ATTEMPTS = 2; // allow this many attempts to bind to an assigned port + private Integer startPort; + private final Map registeredPorts = new HashMap<>(); - /** - * Manage monero-wallet-rpc instances by auto-assigning ports. - */ - public MoneroWalletRpcManager() { } + /** + * Manage monero-wallet-rpc instances by auto-assigning ports. + */ + public MoneroWalletRpcManager() { + } - /** - * Manage monero-wallet-rpc instances by assigning consecutive ports from a starting port. - * - * @param startPort is the starting port to bind to - */ - public MoneroWalletRpcManager(int startPort) { - this.startPort = startPort; - } + /** + * Manage monero-wallet-rpc instances by assigning consecutive ports from a + * starting port. + * + * @param startPort is the starting port to bind to + */ + public MoneroWalletRpcManager(int startPort) { + this.startPort = startPort; + } - /** - * Start a new instance of monero-wallet-rpc. - * - * @param cmd command line parameters to start monero-wallet-rpc - * @return a client connected to the monero-wallet-rpc instance - */ - public MoneroWalletRpc startInstance(List cmd) { - try { + /** + * Start a new instance of monero-wallet-rpc. + * + * @param cmd command line parameters to start monero-wallet-rpc + * @return a client connected to the monero-wallet-rpc instance + */ + public MoneroWalletRpc startInstance(List cmd) { + try { - // register given port - if (cmd.contains(RPC_BIND_PORT_ARGUMENT)) { - int portArgumentPosition = cmd.indexOf(RPC_BIND_PORT_ARGUMENT) + 1; - int port = Integer.parseInt(cmd.get(portArgumentPosition)); - synchronized (registeredPorts) { - if (registeredPorts.containsKey(port)) throw new RuntimeException("Port " + port + " is already registered"); - registeredPorts.put(port, null); - } - MoneroWalletRpc walletRpc = new MoneroWalletRpc(cmd); // starts monero-wallet-rpc process - synchronized (registeredPorts) { - registeredPorts.put(port, walletRpc); - } - return walletRpc; - } + // register given port + if (cmd.contains(RPC_BIND_PORT_ARGUMENT)) { + int portArgumentPosition = cmd.indexOf(RPC_BIND_PORT_ARGUMENT) + 1; + int port = Integer.parseInt(cmd.get(portArgumentPosition)); + synchronized (registeredPorts) { + if (registeredPorts.containsKey(port)) throw new RuntimeException("Port " + port + " is already registered"); + registeredPorts.put(port, null); + } + MoneroWalletRpc walletRpc = new MoneroWalletRpc(cmd); // starts monero-wallet-rpc process + synchronized (registeredPorts) { + registeredPorts.put(port, walletRpc); + } + return walletRpc; + } - // register assigned ports up to maximum attempts - else { - int numAttempts = 0; - while (numAttempts < NUM_ALLOWED_ATTEMPTS) { - int port; - try { - numAttempts++; + // register assigned ports up to maximum attempts + else { + int numAttempts = 0; + while (numAttempts < NUM_ALLOWED_ATTEMPTS) { + int port; + try { + numAttempts++; - // get port - if (startPort != null) port = registerNextPort(); - else { - ServerSocket socket = new ServerSocket(0); - port = socket.getLocalPort(); - socket.close(); - synchronized (registeredPorts) { - registeredPorts.put(port, null); + // get port + if (startPort != null) port = registerNextPort(); + else { + ServerSocket socket = new ServerSocket(0); + port = socket.getLocalPort(); + socket.close(); + synchronized (registeredPorts) { + registeredPorts.put(port, null); + } } - } - // start monero-wallet-rpc - List cmdCopy = new ArrayList<>(cmd); // preserve original cmd - cmdCopy.add(RPC_BIND_PORT_ARGUMENT); - cmdCopy.add("" + port); - MoneroWalletRpc walletRpc = new MoneroWalletRpc(cmdCopy); // start monero-wallet-rpc process - synchronized (registeredPorts) { - registeredPorts.put(port, walletRpc); - } - return walletRpc; - } catch (Exception e) { - if (numAttempts >= NUM_ALLOWED_ATTEMPTS) { - log.error("Unable to start monero-wallet-rpc instance after {} attempts", NUM_ALLOWED_ATTEMPTS); - throw e; - } - } - } - throw new MoneroError("Failed to start monero-wallet-rpc instance after " + NUM_ALLOWED_ATTEMPTS + " attempts"); // should never reach here - } - } catch (IOException e) { - throw new MoneroError(e); - } - } + // start monero-wallet-rpc + List cmdCopy = new ArrayList<>(cmd); // preserve original cmd + cmdCopy.add(RPC_BIND_PORT_ARGUMENT); + cmdCopy.add("" + port); + MoneroWalletRpc walletRpc = new MoneroWalletRpc(cmdCopy); // start monero-wallet-rpc process + synchronized (registeredPorts) { + registeredPorts.put(port, walletRpc); + } + return walletRpc; + } catch (Exception e) { + if (numAttempts >= NUM_ALLOWED_ATTEMPTS) { + log.error("Unable to start monero-wallet-rpc instance after {} attempts", NUM_ALLOWED_ATTEMPTS); + throw e; + } + } + } + throw new MoneroError("Failed to start monero-wallet-rpc instance after " + NUM_ALLOWED_ATTEMPTS + " attempts"); // should never reach here + } + } catch (IOException e) { + throw new MoneroError(e); + } + } - /** - * Stop an instance of monero-wallet-rpc. - * - * @param walletRpc the client connected to the monero-wallet-rpc instance to stop - */ - public void stopInstance(MoneroWalletRpc walletRpc) { + /** + * Stop an instance of monero-wallet-rpc. + * + * @param walletRpc the client connected to the monero-wallet-rpc instance to stop + * @param path the wallet path, since the wallet might be closed + * @param force specifies if the process should be forcibly destroyed + */ + public void stopInstance(MoneroWalletRpc walletRpc, String path, boolean force) { - // unregister port - int port = -1; - synchronized (registeredPorts) { - boolean found = false; - for (Map.Entry entry : registeredPorts.entrySet()) { - if (walletRpc == entry.getValue()) { - found = true; - try { - port = entry.getKey(); - unregisterPort(port); - } catch (Exception e) { - throw new MoneroError(e); - } - break; - } - } - if (!found) throw new RuntimeException("MoneroWalletRpc instance not registered with a port"); - } + // unregister port + int port = unregisterPort(walletRpc); - // stop process - String pid = walletRpc.getProcess() == null ? null : String.valueOf(walletRpc.getProcess().pid()); - log.info("Stopping MoneroWalletRpc path={}, port={}, pid={}", walletRpc.getPath(), port, pid); - walletRpc.stopProcess(); - } + // stop process + String pid = walletRpc.getProcess() == null ? null : String.valueOf(walletRpc.getProcess().pid()); + log.info("Stopping MoneroWalletRpc path={}, port={}, pid={}", path, port, pid); + walletRpc.stopProcess(force); + } - private int registerNextPort() throws IOException { - synchronized (registeredPorts) { + private int registerNextPort() throws IOException { + synchronized (registeredPorts) { int port = startPort; while (registeredPorts.containsKey(port)) port++; registeredPorts.put(port, null); return port; - } - } + } + } - private void unregisterPort(int port) { - synchronized (registeredPorts) { - registeredPorts.remove(port); - } - } + private int unregisterPort(MoneroWalletRpc walletRpc) { + synchronized (registeredPorts) { + int port = -1; + boolean found = false; + for (Map.Entry entry : registeredPorts.entrySet()) { + if (walletRpc == entry.getValue()) { + found = true; + try { + port = entry.getKey(); + removePort(port); + } catch (Exception e) { + throw new MoneroError(e); + } + break; + } + } + if (!found) throw new RuntimeException("MoneroWalletRpc instance not registered with a port"); + return port; + } + } + + private void removePort(int port) { + synchronized (registeredPorts) { + registeredPorts.remove(port); + } + } } diff --git a/core/src/main/java/haveno/core/xmr/wallet/MoneroKeyImagePoller.java b/core/src/main/java/haveno/core/xmr/wallet/MoneroKeyImagePoller.java index 872ff293..c76b65d7 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/MoneroKeyImagePoller.java +++ b/core/src/main/java/haveno/core/xmr/wallet/MoneroKeyImagePoller.java @@ -117,7 +117,9 @@ public class MoneroKeyImagePoller { * @return the key images to listen to */ public Collection getKeyImages() { - return new ArrayList(keyImages); + synchronized (keyImages) { + return new ArrayList(keyImages); + } } /** @@ -197,6 +199,13 @@ public class MoneroKeyImagePoller { } } + /** + * Clear the key images which stops polling. + */ + public void clearKeyImages() { + setKeyImages(); + } + /** * Indicates if the given key image is spent. * @@ -215,37 +224,45 @@ public class MoneroKeyImagePoller { log.warn("Cannot poll key images because daemon is null"); return; } + + // get copy of key images to fetch + List keyImages = new ArrayList(getKeyImages()); + + // fetch spent statuses + List spentStatuses = null; try { - - // fetch spent statuses - List spentStatuses = keyImages.isEmpty() ? new ArrayList() : daemon.getKeyImageSpentStatuses(keyImages); - - // collect changed statuses - Map changedStatuses = new HashMap(); - synchronized (lastStatuses) { - synchronized (keyImages) { - for (int i = 0; i < keyImages.size(); i++) { - if (lastStatuses.get(keyImages.get(i)) != spentStatuses.get(i)) { - lastStatuses.put(keyImages.get(i), spentStatuses.get(i)); - changedStatuses.put(keyImages.get(i), spentStatuses.get(i)); - } - } - } - } - - // announce changes - if (!changedStatuses.isEmpty()) { - for (MoneroKeyImageListener listener : new ArrayList(listeners)) { - listener.onSpentStatusChanged(changedStatuses); - } + if (keyImages.isEmpty()) spentStatuses = new ArrayList(); + else { + spentStatuses = daemon.getKeyImageSpentStatuses(keyImages); // TODO monero-java: if order of getKeyImageSpentStatuses is guaranteed, then it should take list parameter } } catch (Exception e) { - log.warn("Error polling key images: " + e.getMessage()); + log.warn("Error polling spent status of key images: " + e.getMessage()); + return; + } + + // collect changed statuses + Map changedStatuses = new HashMap(); + synchronized (lastStatuses) { + for (int i = 0; i < spentStatuses.size(); i++) { + if (spentStatuses.get(i) != lastStatuses.get(keyImages.get(i))) { + lastStatuses.put(keyImages.get(i), spentStatuses.get(i)); + changedStatuses.put(keyImages.get(i), spentStatuses.get(i)); + } + } + } + + // announce changes + if (!changedStatuses.isEmpty()) { + for (MoneroKeyImageListener listener : new ArrayList(listeners)) { + listener.onSpentStatusChanged(changedStatuses); + } } } private void refreshPolling() { - setIsPolling(keyImages.size() > 0 && listeners.size() > 0); + synchronized (keyImages) { + setIsPolling(keyImages.size() > 0 && listeners.size() > 0); + } } private synchronized void setIsPolling(boolean enabled) { 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 af71bfd6..256a0889 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -2,6 +2,8 @@ package haveno.core.xmr.wallet; import com.google.common.util.concurrent.Service.State; import com.google.inject.name.Named; + +import common.utils.GenUtils; import common.utils.JsonUtils; import haveno.common.UserThread; import haveno.common.config.BaseCurrencyNetwork; @@ -12,7 +14,6 @@ import haveno.common.util.Utilities; import haveno.core.api.AccountServiceListener; import haveno.core.api.CoreAccountService; import haveno.core.api.CoreMoneroConnectionsService; -import haveno.core.app.HavenoSetup; import haveno.core.offer.Offer; import haveno.core.trade.BuyerTrade; import haveno.core.trade.HavenoUtils; @@ -41,6 +42,7 @@ import monero.wallet.model.MoneroDestination; import monero.wallet.model.MoneroOutputQuery; import monero.wallet.model.MoneroOutputWallet; import monero.wallet.model.MoneroSubaddress; +import monero.wallet.model.MoneroSyncResult; import monero.wallet.model.MoneroTransferQuery; import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroTxQuery; @@ -48,6 +50,8 @@ import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroWalletConfig; import monero.wallet.model.MoneroWalletListener; import monero.wallet.model.MoneroWalletListenerI; + +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -64,7 +68,11 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.Callable; import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -88,6 +96,8 @@ public class XmrWalletService { private static final double SECURITY_DEPOSIT_TOLERANCE = Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_LOCAL ? 0.25 : 0.05; // security deposit can absorb miner fee up to percent private static final double DUST_TOLERANCE = 0.01; // max dust as percent of mining fee private static final int NUM_MAX_BACKUP_WALLETS = 10; + private static final int MONERO_LOG_LEVEL = 0; + private static final boolean PRINT_STACK_TRACE = false; private final CoreAccountService accountService; private final CoreMoneroConnectionsService connectionsService; @@ -103,9 +113,8 @@ public class XmrWalletService { private TradeManager tradeManager; private MoneroWalletRpc wallet; private final Map> txCache = new HashMap>(); - private boolean isShutDown = false; - - private HavenoSetup havenoSetup; + private boolean isShutDownStarted = false; + private ExecutorService syncWalletThreadPool = Executors.newFixedThreadPool(10); // TODO: adjust based on connection type @Inject XmrWalletService(CoreAccountService accountService, @@ -122,6 +131,9 @@ public class XmrWalletService { this.rpcBindPort = rpcBindPort; this.xmrWalletFile = new File(walletDir, MONERO_WALLET_NAME); + // set monero logging + MoneroUtils.setLogLevel(MONERO_LOG_LEVEL); + // initialize after account open and basic setup walletsSetup.addSetupTaskHandler(() -> { // TODO: use something better than legacy WalletSetup for notification to initialize @@ -210,7 +222,7 @@ public class XmrWalletService { public MoneroWalletRpc createWallet(String walletName) { log.info("{}.createWallet({})", getClass().getSimpleName(), walletName); - if (isShutDown) throw new IllegalStateException("Cannot create wallet because shutting down"); + if (isShutDownStarted) throw new IllegalStateException("Cannot create wallet because shutting down"); return createWalletRpc(new MoneroWalletConfig() .setPath(walletName) .setPassword(getWalletPassword()), @@ -219,13 +231,26 @@ public class XmrWalletService { public MoneroWalletRpc openWallet(String walletName) { log.info("{}.openWallet({})", getClass().getSimpleName(), walletName); - if (isShutDown) throw new IllegalStateException("Cannot open wallet because shutting down"); + if (isShutDownStarted) throw new IllegalStateException("Cannot open wallet because shutting down"); return openWalletRpc(new MoneroWalletConfig() .setPath(walletName) .setPassword(getWalletPassword()), null); } + /** + * Sync the given wallet in a thread pool with other wallets. + */ + public MoneroSyncResult syncWallet(MoneroWallet wallet) { + Callable task = () -> wallet.sync(); + Future future = syncWalletThreadPool.submit(task); + try { + return future.get(); + } catch (Exception e) { + throw new MoneroError(e.getMessage()); + } + } + public void saveWallet(MoneroWallet wallet, boolean backup) { wallet.save(); if (backup) backupWallet(wallet.getPath()); @@ -234,17 +259,25 @@ public class XmrWalletService { public void closeWallet(MoneroWallet wallet, boolean save) { log.info("{}.closeWallet({}, {})", getClass().getSimpleName(), wallet.getPath(), save); MoneroError err = null; + String path = wallet.getPath(); try { - String path = wallet.getPath(); wallet.close(save); if (save) backupWallet(path); } catch (MoneroError e) { err = e; } - MONERO_WALLET_RPC_MANAGER.stopInstance((MoneroWalletRpc) wallet); + stopWallet(wallet, path); if (err != null) throw err; } + public void stopWallet(MoneroWallet wallet, String path) { + stopWallet(wallet, path, false); + } + + public void stopWallet(MoneroWallet wallet, String path, boolean force) { + MONERO_WALLET_RPC_MANAGER.stopInstance((MoneroWalletRpc) wallet, path, force); + } + public void deleteWallet(String walletName) { log.info("{}.deleteWallet({})", getClass().getSimpleName(), walletName); if (!walletExists(walletName)) throw new Error("Wallet does not exist at path: " + walletName); @@ -506,7 +539,7 @@ public class XmrWalletService { synchronized (txCache) { for (MoneroTx tx : txs) txCache.remove(tx.getHash()); } - }, connectionsService.getDefaultRefreshPeriodMs() / 1000); + }, connectionsService.getRefreshPeriodMs() / 1000); return txs; } } @@ -518,33 +551,50 @@ public class XmrWalletService { public List getTxsWithCache(List txHashes) { synchronized (txCache) { + try { + // get cached txs + List cachedTxs = new ArrayList(); + List uncachedTxHashes = new ArrayList(); + for (int i = 0; i < txHashes.size(); i++) { + if (txCache.containsKey(txHashes.get(i))) cachedTxs.add(txCache.get(txHashes.get(i)).orElse(null)); + else uncachedTxHashes.add(txHashes.get(i)); + } - // get cached txs - List cachedTxs = new ArrayList(); - List uncachedTxHashes = new ArrayList(); - for (int i = 0; i < txHashes.size(); i++) { - if (txCache.containsKey(txHashes.get(i))) cachedTxs.add(txCache.get(txHashes.get(i)).orElse(null)); - else uncachedTxHashes.add(txHashes.get(i)); + // return txs from cache if available, otherwise fetch + return uncachedTxHashes.isEmpty() ? cachedTxs : getTxs(txHashes); + } catch (Exception e) { + if (!isShutDownStarted) throw e; + return null; } - - // return txs from cache if available, otherwise fetch - return uncachedTxHashes.isEmpty() ? cachedTxs : getTxs(txHashes); } } - private void closeMainWallet(boolean save) { - try { - closeWallet(wallet, true); - wallet = null; - walletListeners.clear(); - } catch (Exception e) { - log.warn("Error closing main monero-wallet-rpc subprocess. Was Haveno stopped manually with ctrl+c?"); + public void onShutDownStarted() { + log.info("XmrWalletService.onShutDownStarted()"); + this.isShutDownStarted = true; + + // remove listeners which stops polling wallet + // TODO monero-java: wallet.stopPolling()? + if (wallet != null) { + for (MoneroWalletListenerI listener : new HashSet<>(wallet.getListeners())) { + wallet.removeListener(listener); + } } + + // prepare trades for shut down + if (tradeManager != null) tradeManager.onShutDownStarted(); } - public void shutDown(boolean save) { - this.isShutDown = true; - closeMainWallet(save); + public void shutDown() { + log.info("Shutting down {}", getClass().getSimpleName()); + + // shut down trade and main wallets at same time + walletListeners.clear(); + List tasks = new ArrayList(); + if (tradeManager != null) tasks.add(() -> tradeManager.shutDown()); + tasks.add(() -> closeMainWallet(true)); + HavenoUtils.executeTasks(tasks); + log.info("Done shutting down all wallets"); } // ------------------------------ PRIVATE HELPERS ------------------------- @@ -555,48 +605,53 @@ public class XmrWalletService { maybeInitMainWallet(); // set and listen to daemon connection - connectionsService.addListener(newConnection -> setDaemonConnection(newConnection)); + connectionsService.addListener(newConnection -> onConnectionChanged(newConnection)); } - private void maybeInitMainWallet() { - if (wallet != null) throw new RuntimeException("Main wallet is already initialized"); + private synchronized void maybeInitMainWallet() { - // open or create wallet - MoneroDaemonRpc daemon = connectionsService.getDaemon(); - log.info("Initializing main wallet with " + (daemon == null ? "daemon: null" : "monerod uri=" + daemon.getRpcConnection().getUri())); - MoneroWalletConfig walletConfig = new MoneroWalletConfig().setPath(MONERO_WALLET_NAME).setPassword(getWalletPassword()); - if (MoneroUtils.walletExists(xmrWalletFile.getPath())) { - wallet = openWalletRpc(walletConfig, rpcBindPort); - } else if (connectionsService.getConnection() != null && Boolean.TRUE.equals(connectionsService.getConnection().isConnected())) { - wallet = createWalletRpc(walletConfig, rpcBindPort); + // open or create wallet main wallet + if (wallet == null) { + MoneroDaemonRpc daemon = connectionsService.getDaemon(); + log.info("Initializing main wallet with monerod=" + (daemon == null ? "null" : daemon.getRpcConnection().getUri())); + MoneroWalletConfig walletConfig = new MoneroWalletConfig().setPath(MONERO_WALLET_NAME).setPassword(getWalletPassword()); + if (MoneroUtils.walletExists(xmrWalletFile.getPath())) { + wallet = openWalletRpc(walletConfig, rpcBindPort); + } else if (connectionsService.getConnection() != null && Boolean.TRUE.equals(connectionsService.getConnection().isConnected())) { + wallet = createWalletRpc(walletConfig, rpcBindPort); + } } - // handle when wallet initialized and synced + // sync wallet if open if (wallet != null) { log.info("Monero wallet uri={}, path={}", wallet.getRpcConnection().getUri(), wallet.getPath()); - try { + while (!HavenoUtils.havenoSetup.getWalletInitialized().get()) { + try { - // sync main wallet - log.info("Syncing main wallet"); - long time = System.currentTimeMillis(); - wallet.sync(); // blocking - log.info("Done syncing main wallet in " + (System.currentTimeMillis() - time) + " ms"); - wallet.startSyncing(connectionsService.getDefaultRefreshPeriodMs()); - if (getMoneroNetworkType() != MoneroNetworkType.MAINNET) log.info("Monero wallet balance={}, unlocked balance={}", wallet.getBalance(0), wallet.getUnlockedBalance(0)); - - // TODO: using this to signify both daemon and wallet synced, refactor sync handling of both - connectionsService.doneDownload(); - - // save but skip backup on initialization - saveMainWallet(false); - } catch (Exception e) { - log.warn("Error syncing main wallet: {}", e.getMessage()); + // sync main wallet + log.info("Syncing main wallet"); + long time = System.currentTimeMillis(); + wallet.sync(); // blocking + log.info("Done syncing main wallet in " + (System.currentTimeMillis() - time) + " ms"); + wallet.startSyncing(connectionsService.getRefreshPeriodMs()); + if (getMoneroNetworkType() != MoneroNetworkType.MAINNET) log.info("Monero wallet balance={}, unlocked balance={}", wallet.getBalance(0), wallet.getUnlockedBalance(0)); + + // TODO: using this to signify both daemon and wallet synced, use separate sync handlers + connectionsService.doneDownload(); + + // notify setup that main wallet is initialized + // TODO: app fully initializes after this is set to true, even though wallet might not be initialized if unconnected. wallet will be created when connection detected + // refactor startup to call this and sync off main thread? but the calls to e.g. getBalance() fail with 'wallet and network is not yet initialized' + HavenoUtils.havenoSetup.getWalletInitialized().set(true); + + // save but skip backup on initialization + saveMainWallet(false); + } catch (Exception e) { + log.warn("Error syncing main wallet: {}. Trying again in {} seconds", e.getMessage(), connectionsService.getRefreshPeriodMs() / 1000); + GenUtils.waitFor(connectionsService.getRefreshPeriodMs()); + } } - // notify setup that main wallet is initialized - // TODO: move to try..catch? refactor startup to call this and sync off main thread? - havenoSetup.getWalletInitialized().set(true); // TODO: change to listener pattern - // register internal listener to notify external listeners wallet.addListener(new XmrWalletListener()); } @@ -610,6 +665,7 @@ public class XmrWalletService { // start monero-wallet-rpc instance MoneroWalletRpc walletRpc = startWalletRpcInstance(port); + walletRpc.getRpcConnection().setPrintStackTrace(PRINT_STACK_TRACE); // create wallet try { @@ -621,11 +677,12 @@ public class XmrWalletService { log.info("Creating wallet " + config.getPath() + " connected to daemon " + connection.getUri()); long time = System.currentTimeMillis(); walletRpc.createWallet(config.setServer(connection)); + walletRpc.getDaemonConnection().setPrintStackTrace(PRINT_STACK_TRACE); log.info("Done creating wallet " + config.getPath() + " in " + (System.currentTimeMillis() - time) + " ms"); return walletRpc; } catch (Exception e) { e.printStackTrace(); - MONERO_WALLET_RPC_MANAGER.stopInstance(walletRpc); + stopWallet(walletRpc, config.getPath()); throw e; } } @@ -634,6 +691,7 @@ public class XmrWalletService { // start monero-wallet-rpc instance MoneroWalletRpc walletRpc = startWalletRpcInstance(port); + walletRpc.getRpcConnection().setPrintStackTrace(PRINT_STACK_TRACE); // open wallet try { @@ -644,11 +702,12 @@ public class XmrWalletService { // open wallet log.info("Opening wallet " + config.getPath()); walletRpc.openWallet(config.setServer(connectionsService.getConnection())); + if (walletRpc.getDaemonConnection() != null) walletRpc.getDaemonConnection().setPrintStackTrace(PRINT_STACK_TRACE); log.info("Done opening wallet " + config.getPath()); return walletRpc; } catch (Exception e) { e.printStackTrace(); - MONERO_WALLET_RPC_MANAGER.stopInstance(walletRpc); + stopWallet(walletRpc, config.getPath()); throw e; } } @@ -693,15 +752,23 @@ public class XmrWalletService { return MONERO_WALLET_RPC_MANAGER.startInstance(cmd); } - // TODO: monero-wallet-rpc needs restarted if applying tor proxy - private void setDaemonConnection(MoneroRpcConnection connection) { - if (isShutDown) return; - log.info("Setting wallet daemon connection: " + (connection == null ? null : connection.getUri())); + private void onConnectionChanged(MoneroRpcConnection connection) { + if (isShutDownStarted) return; + if (wallet != null && HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) return; + + log.info("Setting main wallet daemon connection: " + (connection == null ? null : connection.getUri())); + String oldProxyUri = wallet == null || wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getProxyUri(); + String newProxyUri = connection == null ? null : connection.getProxyUri(); if (wallet == null) maybeInitMainWallet(); - else { + else if (wallet instanceof MoneroWalletRpc && !StringUtils.equals(oldProxyUri, newProxyUri)) { + log.info("Restarting main wallet since proxy URI has changed"); + closeMainWallet(true); + maybeInitMainWallet(); + } else { wallet.setDaemonConnection(connection); + if (connection != null) wallet.getDaemonConnection().setPrintStackTrace(PRINT_STACK_TRACE); if (connection != null && !Boolean.FALSE.equals(connection.isConnected())) { - wallet.startSyncing(connectionsService.getDefaultRefreshPeriodMs()); + wallet.startSyncing(connectionsService.getRefreshPeriodMs()); new Thread(() -> { try { wallet.sync(); @@ -711,6 +778,8 @@ public class XmrWalletService { }).start(); } } + + log.info("Done setting main wallet daemon connection: " + (connection == null ? null : connection.getUri())); } private void notifyBalanceListeners() { @@ -755,6 +824,17 @@ public class XmrWalletService { HavenoUtils.executeTasks(tasks, Math.min(10, 1 + trades.size())); } + private void closeMainWallet(boolean save) { + try { + if (wallet != null) { + closeWallet(wallet, true); + wallet = null; + } + } catch (Exception e) { + log.warn("Error closing main monero-wallet-rpc subprocess: " + e.getMessage() + ". Was Haveno stopped manually with ctrl+c?"); + } + } + // ----------------------------- LEGACY APP ------------------------------- public synchronized XmrAddressEntry getNewAddressEntry() { @@ -764,7 +844,7 @@ public class XmrWalletService { public synchronized XmrAddressEntry getNewAddressEntry(String offerId, XmrAddressEntry.Context context) { // try to use available and not yet used entries - List incomingTxs = getIncomingTxs(null); // pre-fetch all incoming txs to avoid query per subaddress + List incomingTxs = getIncomingTxs(); // prefetch all incoming txs to avoid query per subaddress Optional emptyAvailableAddressEntry = getAddressEntryListAsImmutableList().stream().filter(e -> XmrAddressEntry.Context.AVAILABLE == e.getContext()).filter(e -> isSubaddressUnused(e.getSubaddressIndex(), incomingTxs)).findAny(); if (emptyAvailableAddressEntry.isPresent()) return xmrAddressEntryList.swapAvailableToAddressEntryWithOfferId(emptyAvailableAddressEntry.get(), context, offerId); @@ -820,7 +900,6 @@ public class XmrWalletService { log.info("resetAddressEntriesForOpenOffer offerId={}", offerId); swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.OFFER_FUNDING); swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.RESERVED_FOR_TRADE); - swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.TRADE_PAYOUT); } public synchronized void resetAddressEntriesForPendingTrade(String offerId) { @@ -881,8 +960,9 @@ public class XmrWalletService { } public List getUnusedAddressEntries() { + List incomingTxs = getIncomingTxs(); // prefetch all incoming txs to avoid query per subaddress return getAvailableAddressEntries().stream() - .filter(e -> isSubaddressUnused(e.getSubaddressIndex())) + .filter(e -> isSubaddressUnused(e.getSubaddressIndex(), incomingTxs)) .collect(Collectors.toList()); } @@ -997,10 +1077,6 @@ public class XmrWalletService { log.info("\n" + tracePrefix + ":" + sb.toString()); } - public void setHavenoSetup(HavenoSetup havenoSetup) { - this.havenoSetup = havenoSetup; - } - // -------------------------------- HELPERS ------------------------------- /** diff --git a/core/src/test/java/haveno/core/arbitration/ArbitratorManagerTest.java b/core/src/test/java/haveno/core/arbitration/ArbitratorManagerTest.java index fdc1ba5a..3a40148c 100644 --- a/core/src/test/java/haveno/core/arbitration/ArbitratorManagerTest.java +++ b/core/src/test/java/haveno/core/arbitration/ArbitratorManagerTest.java @@ -54,11 +54,11 @@ public class ArbitratorManagerTest { add("es"); }}; - Arbitrator one = new Arbitrator(new NodeAddress("arbitrator:1"), null, null, + Arbitrator one = new Arbitrator(new NodeAddress("arbitrator:1"), null, languagesOne, 0L, null, "", null, null, null); - Arbitrator two = new Arbitrator(new NodeAddress("arbitrator:2"), null, null, + Arbitrator two = new Arbitrator(new NodeAddress("arbitrator:2"), null, languagesTwo, 0L, null, "", null, null, null); @@ -90,11 +90,11 @@ public class ArbitratorManagerTest { add("es"); }}; - Arbitrator one = new Arbitrator(new NodeAddress("arbitrator:1"), null, null, + Arbitrator one = new Arbitrator(new NodeAddress("arbitrator:1"), null, languagesOne, 0L, null, "", null, null, null); - Arbitrator two = new Arbitrator(new NodeAddress("arbitrator:2"), null, null, + Arbitrator two = new Arbitrator(new NodeAddress("arbitrator:2"), null, languagesTwo, 0L, null, "", null, null, null); diff --git a/core/src/test/java/haveno/core/arbitration/ArbitratorTest.java b/core/src/test/java/haveno/core/arbitration/ArbitratorTest.java index 508f4515..27cb6d5a 100644 --- a/core/src/test/java/haveno/core/arbitration/ArbitratorTest.java +++ b/core/src/test/java/haveno/core/arbitration/ArbitratorTest.java @@ -39,7 +39,6 @@ public class ArbitratorTest { public static Arbitrator getArbitratorMock() { return new Arbitrator(new NodeAddress("host", 1000), - "xmraddress", new PubKeyRing(getBytes(100), getBytes(100)), Lists.newArrayList(), new Date().getTime(), diff --git a/desktop/src/main/java/haveno/desktop/main/account/register/arbitrator/ArbitratorRegistrationViewModel.java b/desktop/src/main/java/haveno/desktop/main/account/register/arbitrator/ArbitratorRegistrationViewModel.java index 4f68e907..138d0037 100644 --- a/desktop/src/main/java/haveno/desktop/main/account/register/arbitrator/ArbitratorRegistrationViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/account/register/arbitrator/ArbitratorRegistrationViewModel.java @@ -45,7 +45,6 @@ public class ArbitratorRegistrationViewModel extends AgentRegistrationViewModel< String emailAddress) { return new Arbitrator( p2PService.getAddress(), - xmrWalletService.getWallet().getPrimaryAddress(), // TODO: how is arbitrator address used? keyRing.getPubKeyRing(), new ArrayList<>(languageCodes), new Date().getTime(), diff --git a/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositListItem.java b/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositListItem.java index cb09fe4e..c4c9036c 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositListItem.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/deposit/DepositListItem.java @@ -19,6 +19,8 @@ package haveno.desktop.main.funds.deposit; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; + +import common.types.Filter; import haveno.core.locale.Res; import haveno.core.trade.HavenoUtils; import haveno.core.util.coin.CoinFormatter; @@ -85,7 +87,7 @@ class DepositListItem { tooltip = new Tooltip(Res.get("shared.notUsedYet")); txConfidenceIndicator.setProgress(0); txConfidenceIndicator.setTooltip(tooltip); - MoneroTx tx = getTxWithFewestConfirmations(); + MoneroTx tx = getTxWithFewestConfirmations(cachedTxs); if (tx == null) { txConfidenceIndicator.setVisible(false); } else { @@ -132,20 +134,20 @@ class DepositListItem { return numTxOutputs; } - public long getNumConfirmationsSinceFirstUsed() { - MoneroTx tx = getTxWithFewestConfirmations(); + public long getNumConfirmationsSinceFirstUsed(List incomingTxs) { + MoneroTx tx = getTxWithFewestConfirmations(incomingTxs); return tx == null ? 0 : tx.getNumConfirmations(); } - private MoneroTxWallet getTxWithFewestConfirmations() { + private MoneroTxWallet getTxWithFewestConfirmations(List incomingTxs) { // get txs with incoming transfers to subaddress - List txs = xmrWalletService.getWallet() - .getTxs(new MoneroTxQuery() - .setTransferQuery(new MoneroTransferQuery() - .setIsIncoming(true) - .setSubaddressIndex(addressEntry.getSubaddressIndex()))); - + MoneroTxQuery query = new MoneroTxQuery() + .setTransferQuery(new MoneroTransferQuery() + .setIsIncoming(true) + .setSubaddressIndex(addressEntry.getSubaddressIndex())); + List txs = incomingTxs == null ? xmrWalletService.getWallet().getTxs(query) : Filter.apply(query, incomingTxs); + // get tx with fewest confirmations MoneroTxWallet highestTx = null; for (MoneroTxWallet tx : txs) if (highestTx == null || tx.getNumConfirmations() < highestTx.getNumConfirmations()) highestTx = tx; 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 0998797c..b9651b94 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 @@ -147,9 +147,12 @@ public class DepositView extends ActivatableView { setUsageColumnCellFactory(); setConfidenceColumnCellFactory(); + // prefetch all incoming txs to avoid query per subaddress + List incomingTxs = xmrWalletService.getIncomingTxs(); + addressColumn.setComparator(Comparator.comparing(DepositListItem::getAddressString)); balanceColumn.setComparator(Comparator.comparing(DepositListItem::getBalanceAsBI)); - confirmationsColumn.setComparator(Comparator.comparingLong(o -> o.getNumConfirmationsSinceFirstUsed())); + confirmationsColumn.setComparator(Comparator.comparingLong(o -> o.getNumConfirmationsSinceFirstUsed(incomingTxs))); usageColumn.setComparator(Comparator.comparingInt(DepositListItem::getNumTxOutputs)); tableView.getSortOrder().add(usageColumn); tableView.setItems(sortedList); diff --git a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionAwareOpenOffer.java b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionAwareOpenOffer.java index edbfcb90..734b6b34 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionAwareOpenOffer.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionAwareOpenOffer.java @@ -35,7 +35,7 @@ class TransactionAwareOpenOffer implements TransactionAwareTradable { String txId = transaction.getHash(); - return paymentTxId.equals(txId); + return txId.equals(paymentTxId); } public Tradable asTradable() { diff --git a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsListItem.java b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsListItem.java index f5d5122e..29de42b2 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsListItem.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsListItem.java @@ -34,6 +34,7 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import monero.wallet.model.MoneroIncomingTransfer; import monero.wallet.model.MoneroOutgoingTransfer; +import monero.wallet.model.MoneroTxQuery; import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroWalletListener; @@ -60,6 +61,8 @@ class TransactionsListItem { @Getter private boolean initialTxConfidenceVisibility = true; private final Supplier lazyFieldsSupplier; + private XmrWalletService xmrWalletService; + MoneroWalletListener walletListener; private static class LazyFields { TxConfidenceIndicator txConfidenceIndicator; @@ -82,6 +85,8 @@ class TransactionsListItem { TransactionAwareTradable transactionAwareTradable) { this.memo = tx.getNote(); this.txId = tx.getHash(); + this.xmrWalletService = xmrWalletService; + this.confirmations = tx.getNumConfirmations() == null ? 0 : tx.getNumConfirmations(); Optional optionalTradable = Optional.ofNullable(transactionAwareTradable) .map(TransactionAwareTradable::asTradable); @@ -182,18 +187,24 @@ class TransactionsListItem { }}); // listen for tx updates - // TODO: this only listens for new blocks, listen for double spend - xmrWalletService.addWalletListener(new MoneroWalletListener() { + walletListener = new MoneroWalletListener() { @Override public void onNewBlock(long height) { - MoneroTxWallet tx = xmrWalletService.getWallet().getTx(txId); + MoneroTxWallet tx = xmrWalletService.getWallet().getTxs(new MoneroTxQuery() + .setHash(txId) + .setInTxPool(confirmations > 0 ? false : null)).get(0); GUIUtil.updateConfidence(tx, lazy().tooltip, lazy().txConfidenceIndicator); confirmations = tx.getNumConfirmations(); } - }); + }; + xmrWalletService.addWalletListener(walletListener); } public void cleanup() { + if (walletListener != null) { + xmrWalletService.removeWalletListener(walletListener); + walletListener = null; + } } public TxConfidenceIndicator getTxConfidenceIndicator() { diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java index 9ea0fa78..cec65b92 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java @@ -67,7 +67,6 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import lombok.extern.slf4j.Slf4j; import monero.wallet.model.MoneroTxWallet; -import org.bitcoinj.core.Coin; import java.math.BigInteger; import java.util.Date; @@ -727,13 +726,13 @@ public class DisputeSummaryWindow extends Overlay { sellerPayoutAmount.equals(sellerSecurityDeposit)) { buyerGetsTradeAmountRadioButton.setSelected(true); } else if (buyerPayoutAmount.equals(tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit)) && - sellerPayoutAmount.equals(Coin.ZERO)) { + sellerPayoutAmount.equals(BigInteger.valueOf(0))) { buyerGetsAllRadioButton.setSelected(true); } else if (sellerPayoutAmount.equals(tradeAmount.add(sellerSecurityDeposit)) && buyerPayoutAmount.equals(buyerSecurityDeposit)) { sellerGetsTradeAmountRadioButton.setSelected(true); } else if (sellerPayoutAmount.equals(tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit)) - && buyerPayoutAmount.equals(Coin.ZERO)) { + && buyerPayoutAmount.equals(BigInteger.valueOf(0))) { sellerGetsAllRadioButton.setSelected(true); } else { customRadioButton.setSelected(true); 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 10cb3178..c2b6af46 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 @@ -448,6 +448,8 @@ public class PendingTradesViewModel extends ActivatableWithDataModel preferences.setUseStandbyMode(!avoidStandbyMode.isSelected())); + } else { + preferences.setUseStandbyMode(false); + } } private void activateAutoConfirmPreferences() { diff --git a/p2p/src/main/java/haveno/network/p2p/P2PService.java b/p2p/src/main/java/haveno/network/p2p/P2PService.java index 662e24e5..b94f1410 100644 --- a/p2p/src/main/java/haveno/network/p2p/P2PService.java +++ b/p2p/src/main/java/haveno/network/p2p/P2PService.java @@ -190,6 +190,7 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis } private void doShutDown() { + if (p2PDataStorage != null) { p2PDataStorage.shutDown(); } diff --git a/p2p/src/main/java/haveno/network/p2p/mailbox/MailboxMessageService.java b/p2p/src/main/java/haveno/network/p2p/mailbox/MailboxMessageService.java index e80c4557..fa688e19 100644 --- a/p2p/src/main/java/haveno/network/p2p/mailbox/MailboxMessageService.java +++ b/p2p/src/main/java/haveno/network/p2p/mailbox/MailboxMessageService.java @@ -64,7 +64,6 @@ import javax.inject.Singleton; import java.security.PublicKey; import java.time.Clock; import java.util.ArrayDeque; -import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.Date; @@ -345,11 +344,7 @@ public class MailboxMessageService implements HashMapChangedListener, PersistedD .map(e -> (ProtectedMailboxStorageEntry) e) .filter(e -> networkNode.getNodeAddress() != null) .collect(Collectors.toSet()); - if (entries.size() > 1) { - threadedBatchProcessMailboxEntries(entries); - } else if (entries.size() == 1) { - processSingleMailboxEntry(entries); - } + threadedBatchProcessMailboxEntries(entries); } @Override @@ -375,14 +370,6 @@ public class MailboxMessageService implements HashMapChangedListener, PersistedD p2PDataStorage.addHashMapChangedListener(this); } - private void processSingleMailboxEntry(Collection protectedMailboxStorageEntries) { - checkArgument(protectedMailboxStorageEntries.size() == 1); - var mailboxItems = new ArrayList<>(getMailboxItems(protectedMailboxStorageEntries)); - if (mailboxItems.size() == 1) { - handleMailboxItem(mailboxItems.get(0)); - } - } - // We run the batch processing of all mailbox messages we have received at startup in a thread to not block the UI. // For about 1000 messages decryption takes about 1 sec. private void threadedBatchProcessMailboxEntries(Collection protectedMailboxStorageEntries) { @@ -390,7 +377,7 @@ public class MailboxMessageService implements HashMapChangedListener, PersistedD long ts = System.currentTimeMillis(); ListenableFuture> future = executor.submit(() -> { var mailboxItems = getMailboxItems(protectedMailboxStorageEntries); - log.info("Batch processing of {} mailbox entries took {} ms", + log.trace("Batch processing of {} mailbox entries took {} ms", protectedMailboxStorageEntries.size(), System.currentTimeMillis() - ts); return mailboxItems; diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 848df095..89e2b4a8 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -520,10 +520,9 @@ message Arbitrator { string registration_signature = 4; bytes registration_pub_key = 5; PubKeyRing pub_key_ring = 6; - string xmr_address = 7; - string email_address = 8; - string info = 9; - map extra_data = 10; + string email_address = 7; + string info = 8; + map extra_data = 9; } message Mediator {