diff --git a/LICENSE b/LICENSE index 93a5d000f0..90fb6f4689 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 - Copyright (C) 2007 Free Software Foundation, Inc. + Copyright (C) 2007 Free Software Foundation, Inc. Copyright (C) 2020 Haveno Dex Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. @@ -644,7 +644,7 @@ the "copyright" line and a pointer to where the full notice is found. GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . + along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. @@ -659,4 +659,4 @@ specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see -. +. diff --git a/Makefile b/Makefile index 51d55af341..ad51f76809 100644 --- a/Makefile +++ b/Makefile @@ -70,9 +70,11 @@ monerod1-local: --log-level 0 \ --add-exclusive-node 127.0.0.1:48080 \ --add-exclusive-node 127.0.0.1:58080 \ + --max-connections-per-ip 10 \ --rpc-access-control-origins http://localhost:8080 \ --fixed-difficulty 500 \ --disable-rpc-ban \ + --rpc-max-connections-per-private-ip 100 \ monerod2-local: ./.localnet/monerod \ @@ -88,9 +90,11 @@ monerod2-local: --confirm-external-bind \ --add-exclusive-node 127.0.0.1:28080 \ --add-exclusive-node 127.0.0.1:58080 \ + --max-connections-per-ip 10 \ --rpc-access-control-origins http://localhost:8080 \ --fixed-difficulty 500 \ --disable-rpc-ban \ + --rpc-max-connections-per-private-ip 100 \ monerod3-local: ./.localnet/monerod \ @@ -106,9 +110,11 @@ monerod3-local: --confirm-external-bind \ --add-exclusive-node 127.0.0.1:28080 \ --add-exclusive-node 127.0.0.1:48080 \ + --max-connections-per-ip 10 \ --rpc-access-control-origins http://localhost:8080 \ --fixed-difficulty 500 \ --disable-rpc-ban \ + --rpc-max-connections-per-private-ip 100 \ #--proxy 127.0.0.1:49775 \ diff --git a/README.md b/README.md index 5c439f9a55..4e90b06ce3 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,10 @@ Main features: See the [FAQ on our website](https://haveno.exchange/faq/) for more information. +## Haveno Demo + +https://github.com/user-attachments/assets/eb6b3af0-78ce-46a7-bfa1-2aacd8649d47 + ## Installing Haveno Haveno can be installed on Linux, macOS, and Windows by using a third party installer and network. @@ -34,7 +38,7 @@ Haveno can be installed on Linux, macOS, and Windows by using a third party inst A test network is also available for users to make test trades using Monero's stagenet. See the [instructions](https://github.com/haveno-dex/haveno/blob/master/docs/installing.md) to build Haveno and connect to the test network. -Alternatively, you can [create your own mainnet network](create-mainnet.md). +Alternatively, you can [create your own mainnet network](https://github.com/haveno-dex/haveno/blob/master/docs/create-mainnet.md). Note that Haveno is being actively developed. If you find issues or bugs, please let us know. @@ -63,7 +67,7 @@ See the [developer guide](docs/developer-guide.md) to get started developing for See [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) for our styling guides. -If you are not able to contribute code and want to contribute development resources, [donations](#support) fund development bounties. +If you are not able to contribute code and want to contribute development resources, [donations](#support-and-sponsorships) fund development bounties. ## Bounties diff --git a/build.gradle b/build.gradle index 74e776cb2b..20ca924031 100644 --- a/build.gradle +++ b/build.gradle @@ -71,7 +71,7 @@ configure(subprojects) { loggingVersion = '1.2' lombokVersion = '1.18.30' mockitoVersion = '5.10.0' - netlayerVersion = '700ec94f0f' // Tor browser version 14.0.3 and tor binary version: 0.4.8.13 + netlayerVersion = 'd9c60be46d' // Tor browser version 14.0.7 and tor binary version: 0.4.8.14 protobufVersion = '3.19.1' protocVersion = protobufVersion pushyVersion = '0.13.2' @@ -457,14 +457,14 @@ configure(project(':core')) { doLast { // get monero binaries download url Map moneroBinaries = [ - 'linux-x86_64' : 'https://github.com/haveno-dex/monero/releases/download/release5/monero-bins-haveno-linux-x86_64.tar.gz', - 'linux-x86_64-sha256' : '92003b6d9104e8fe3c4dff292b782ed9b82b157aaff95200fda35e5c3dcb733a', - 'linux-aarch64' : 'https://github.com/haveno-dex/monero/releases/download/release5/monero-bins-haveno-linux-aarch64.tar.gz', - 'linux-aarch64-sha256' : '18b069c6c474ce18efea261c875a4d54022520e888712b2a56524d9c92f133b1', - 'mac' : 'https://github.com/haveno-dex/monero/releases/download/release5/monero-bins-haveno-mac.tar.gz', - 'mac-sha256' : 'd308352191cd5a9e5e3932ad15869e033e22e209de459f4fd6460b111377dae2', - 'windows' : 'https://github.com/haveno-dex/monero/releases/download/release5/monero-bins-haveno-windows.zip', - 'windows-sha256' : '9c9e1994d4738e2a89ca28bef343bcad460ea6c06e0dd40de8278ab3033bd6c7' + 'linux-x86_64' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-linux-x86_64.tar.gz', + 'linux-x86_64-sha256' : '44470a3cf2dd9be7f3371a8cc89a34cf9a7e88c442739d87ef9a0ec3ccb65208', + 'linux-aarch64' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-linux-aarch64.tar.gz', + 'linux-aarch64-sha256' : 'c9505524689b0d7a020b8d2fd449c3cb9f8fd546747f9bdcf36cac795179f71c', + 'mac' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-mac.tar.gz', + 'mac-sha256' : 'dea6eddefa09630cfff7504609bd5d7981316336c64e5458e242440694187df8', + 'windows' : 'https://github.com/haveno-dex/monero/releases/download/release6/monero-bins-haveno-windows.zip', + 'windows-sha256' : '284820e28c4770d7065fad7863e66fe0058053ca2372b78345d83c222edc572d' ] String osKey @@ -610,7 +610,7 @@ configure(project(':desktop')) { apply plugin: 'com.github.johnrengelman.shadow' apply from: 'package/package.gradle' - version = '1.0.18-SNAPSHOT' + version = '1.0.19-SNAPSHOT' jar.manifest.attributes( "Implementation-Title": project.name, diff --git a/common/src/main/java/haveno/common/ClockWatcher.java b/common/src/main/java/haveno/common/ClockWatcher.java index 7bd8454c78..ba77ca38ab 100644 --- a/common/src/main/java/haveno/common/ClockWatcher.java +++ b/common/src/main/java/haveno/common/ClockWatcher.java @@ -69,7 +69,7 @@ public class ClockWatcher { listeners.forEach(listener -> listener.onMissedSecondTick(missedMs)); if (missedMs > ClockWatcher.IDLE_TOLERANCE_MS) { - log.info("We have been in standby mode for {} sec", missedMs / 1000); + log.warn("We have been in standby mode for {} sec", missedMs / 1000); listeners.forEach(listener -> listener.onAwakeFromStandby(missedMs)); } } diff --git a/common/src/main/java/haveno/common/app/Version.java b/common/src/main/java/haveno/common/app/Version.java index d39016dc31..3dc04889b8 100644 --- a/common/src/main/java/haveno/common/app/Version.java +++ b/common/src/main/java/haveno/common/app/Version.java @@ -28,7 +28,7 @@ import static com.google.common.base.Preconditions.checkArgument; public class Version { // The application versions // We use semantic versioning with major, minor and patch - public static final String VERSION = "1.0.18"; + public static final String VERSION = "1.0.19"; /** * Holds a list of the tagged resource files for optimizing the getData requests. @@ -72,6 +72,25 @@ public class Version { return false; } + public static int compare(String version1, String version2) { + if (version1.equals(version2)) + return 0; + else if (getMajorVersion(version1) > getMajorVersion(version2)) + return 1; + else if (getMajorVersion(version1) < getMajorVersion(version2)) + return -1; + else if (getMinorVersion(version1) > getMinorVersion(version2)) + return 1; + else if (getMinorVersion(version1) < getMinorVersion(version2)) + return -1; + else if (getPatchVersion(version1) > getPatchVersion(version2)) + return 1; + else if (getPatchVersion(version1) < getPatchVersion(version2)) + return -1; + else + return 0; + } + private static int getSubVersion(String version, int index) { final String[] split = version.split("\\."); checkArgument(split.length == 3, "Version number must be in semantic version format (contain 2 '.'). version=" + version); @@ -91,8 +110,9 @@ public class Version { // For the switch to version 2, offers created with the old version will become invalid and have to be canceled. // For the switch to version 3, offers created with the old version can be migrated to version 3 just by opening // the Haveno app. - // VERSION = 0.0.1 -> TRADE_PROTOCOL_VERSION = 1 - public static final int TRADE_PROTOCOL_VERSION = 1; + // Version = 0.0.1 -> TRADE_PROTOCOL_VERSION = 1 + // Version = 1.0.19 -> TRADE_PROTOCOL_VERSION = 2 + public static final int TRADE_PROTOCOL_VERSION = 2; private static String p2pMessageVersion; public static String getP2PMessageVersion() { diff --git a/core/src/main/java/haveno/core/api/CoreOffersService.java b/core/src/main/java/haveno/core/api/CoreOffersService.java index a66388c040..f9c450b825 100644 --- a/core/src/main/java/haveno/core/api/CoreOffersService.java +++ b/core/src/main/java/haveno/core/api/CoreOffersService.java @@ -159,7 +159,7 @@ public class CoreOffersService { } OpenOffer getMyOffer(String id) { - return openOfferManager.getOpenOfferById(id) + return openOfferManager.getOpenOffer(id) .filter(open -> open.getOffer().isMyOffer(keyRing)) .orElseThrow(() -> new IllegalStateException(format("openoffer with id '%s' not found", id))); @@ -265,6 +265,7 @@ public class CoreOffersService { if (!seenKeyImages.add(keyImage)) { for (Offer offer2 : offers) { if (offer == offer2) continue; + if (offer2.getOfferPayload().getReserveTxKeyImages() == null) continue; if (offer2.getOfferPayload().getReserveTxKeyImages().contains(keyImage)) { log.warn("Key image {} belongs to multiple offers, seen in offer {} and {}", keyImage, offer.getId(), offer2.getId()); duplicateFundedOffers.add(offer2); diff --git a/core/src/main/java/haveno/core/api/CoreTradesService.java b/core/src/main/java/haveno/core/api/CoreTradesService.java index 14f10b4f00..9854167c71 100644 --- a/core/src/main/java/haveno/core/api/CoreTradesService.java +++ b/core/src/main/java/haveno/core/api/CoreTradesService.java @@ -47,7 +47,6 @@ import haveno.core.support.messages.ChatMessage; import haveno.core.support.traderchat.TradeChatSession; import haveno.core.support.traderchat.TraderChatManager; import haveno.core.trade.ClosedTradableManager; -import haveno.core.trade.Tradable; import haveno.core.trade.Trade; import haveno.core.trade.TradeManager; import haveno.core.trade.TradeUtil; @@ -223,8 +222,7 @@ class CoreTradesService { } private Optional getClosedTrade(String tradeId) { - Optional tradable = closedTradableManager.getTradeById(tradeId); - return tradable.filter((t) -> t instanceof Trade).map(value -> (Trade) value); + return closedTradableManager.getTradeById(tradeId); } List getTrades() { diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index 88d0117f8e..d686d64925 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -24,6 +24,7 @@ import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.config.BaseCurrencyNetwork; import haveno.common.config.Config; +import haveno.core.locale.Res; import haveno.core.trade.HavenoUtils; import haveno.core.user.Preferences; import haveno.core.xmr.model.EncryptedConnectionList; @@ -43,7 +44,6 @@ import java.util.Set; import org.apache.commons.lang3.exception.ExceptionUtils; -import javafx.beans.property.BooleanProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.LongProperty; import javafx.beans.property.ObjectProperty; @@ -51,7 +51,6 @@ import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.beans.property.ReadOnlyIntegerProperty; import javafx.beans.property.ReadOnlyLongProperty; import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleLongProperty; import javafx.beans.property.SimpleObjectProperty; @@ -91,7 +90,7 @@ public final class XmrConnectionService { private final LongProperty chainHeight = new SimpleLongProperty(0); private final DownloadListener downloadListener = new DownloadListener(); @Getter - private final BooleanProperty connectionServiceFallbackHandlerActive = new SimpleBooleanProperty(); + private final SimpleStringProperty connectionServiceFallbackHandler = new SimpleStringProperty(); @Getter private final StringProperty connectionServiceErrorMsg = new SimpleStringProperty(); private final LongProperty numUpdates = new SimpleLongProperty(0); @@ -261,6 +260,7 @@ public final class XmrConnectionService { private MoneroRpcConnection getBestConnection(Collection ignoredConnections) { accountService.checkAccountOpen(); + if (!fallbackApplied && lastUsedLocalSyncingNode() && !xmrLocalNode.isDetected()) return null; // user needs to explicitly allow fallback after syncing local node Set ignoredConnectionsSet = new HashSet<>(ignoredConnections); addLocalNodeIfIgnored(ignoredConnectionsSet); MoneroRpcConnection bestConnection = connectionManager.getBestAvailableConnection(ignoredConnectionsSet.toArray(new MoneroRpcConnection[0])); // checks connections @@ -604,9 +604,6 @@ public final class XmrConnectionService { if (coreContext.isApiUser()) connectionManager.setAutoSwitch(connectionList.getAutoSwitch()); else connectionManager.setAutoSwitch(true); // auto switch is always enabled on desktop ui - // start local node if applicable - maybeStartLocalNode(); - // update connection if (connectionManager.getConnection() == null || connectionManager.getAutoSwitch()) { MoneroRpcConnection bestConnection = getBestConnection(); @@ -619,9 +616,6 @@ public final class XmrConnectionService { MoneroRpcConnection connection = new MoneroRpcConnection(config.xmrNode, config.xmrNodeUsername, config.xmrNodePassword).setPriority(1); if (isProxyApplied(connection)) connection.setProxyUri(getProxyUri()); connectionManager.setConnection(connection); - - // start local node if applicable - maybeStartLocalNode(); } // register connection listener @@ -634,20 +628,8 @@ public final class XmrConnectionService { onConnectionChanged(connectionManager.getConnection()); } - private void maybeStartLocalNode() { - - // skip if seed node - if (HavenoUtils.isSeedNode()) return; - - // start local node if offline and used as last connection - if (connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored()) { - try { - log.info("Starting local node"); - xmrLocalNode.start(); - } catch (Exception e) { - log.error("Unable to start local monero node, error={}\n", e.getMessage(), e); - } - } + private boolean lastUsedLocalSyncingNode() { + return connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored(); } private void onConnectionChanged(MoneroRpcConnection currentConnection) { @@ -733,12 +715,17 @@ public final class XmrConnectionService { if (isShutDownStarted) return; // invoke fallback handling on startup error - boolean canFallback = isFixedConnection() || isCustomConnections(); + boolean canFallback = isFixedConnection() || isCustomConnections() || lastUsedLocalSyncingNode(); if (lastInfo == null && canFallback) { - if (!connectionServiceFallbackHandlerActive.get() && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) { - log.warn("Failed to fetch daemon info from custom connection on startup: " + e.getMessage()); + if (connectionServiceFallbackHandler.get() == null || connectionServiceFallbackHandler.equals("") && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) { lastFallbackInvocation = System.currentTimeMillis(); - connectionServiceFallbackHandlerActive.set(true); + if (lastUsedLocalSyncingNode()) { + log.warn("Failed to fetch daemon info from local connection on startup: " + e.getMessage()); + connectionServiceFallbackHandler.set(Res.get("connectionFallback.localNode")); + } else { + log.warn("Failed to fetch daemon info from custom connection on startup: " + e.getMessage()); + connectionServiceFallbackHandler.set(Res.get("connectionFallback.customNode")); + } } return; } diff --git a/core/src/main/java/haveno/core/api/XmrLocalNode.java b/core/src/main/java/haveno/core/api/XmrLocalNode.java index 5a424dad38..7295202c64 100644 --- a/core/src/main/java/haveno/core/api/XmrLocalNode.java +++ b/core/src/main/java/haveno/core/api/XmrLocalNode.java @@ -25,6 +25,8 @@ import haveno.core.trade.HavenoUtils; import haveno.core.user.Preferences; import haveno.core.xmr.XmrNodeSettings; import haveno.core.xmr.nodes.XmrNodes; +import haveno.core.xmr.nodes.XmrNodes.XmrNode; +import haveno.core.xmr.nodes.XmrNodesSetupPreferences; import haveno.core.xmr.wallet.XmrWalletService; import java.io.File; @@ -55,6 +57,7 @@ public class XmrLocalNode { private MoneroConnectionManager connectionManager; private final Config config; private final Preferences preferences; + private final XmrNodes xmrNodes; private final List listeners = new ArrayList<>(); // required arguments @@ -69,9 +72,12 @@ public class XmrLocalNode { } @Inject - public XmrLocalNode(Config config, Preferences preferences) { + public XmrLocalNode(Config config, + Preferences preferences, + XmrNodes xmrNodes) { this.config = config; this.preferences = preferences; + this.xmrNodes = xmrNodes; this.daemon = new MoneroDaemonRpc(getUri()); // initialize connection manager to listen to local connection @@ -101,7 +107,19 @@ public class XmrLocalNode { * Returns whether Haveno should ignore a local Monero node even if it is usable. */ public boolean shouldBeIgnored() { - return config.ignoreLocalXmrNode || preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM; + if (config.ignoreLocalXmrNode) return true; + + // determine if local node is configured + boolean hasConfiguredLocalNode = false; + for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) { + if (node.getAddress() != null && equalsUri("http://" + node.getAddress() + ":" + node.getPort())) { + hasConfiguredLocalNode = true; + break; + } + } + if (!hasConfiguredLocalNode) return true; + + return false; } public void addListener(XmrLocalNodeListener listener) { @@ -120,7 +138,11 @@ public class XmrLocalNode { } public boolean equalsUri(String uri) { - return HavenoUtils.isLocalHost(uri) && MoneroUtils.parseUri(uri).getPort() == HavenoUtils.getDefaultMoneroPort(); + try { + return HavenoUtils.isLocalHost(uri) && MoneroUtils.parseUri(uri).getPort() == HavenoUtils.getDefaultMoneroPort(); + } catch (Exception e) { + return false; + } } /** diff --git a/core/src/main/java/haveno/core/app/DomainInitialisation.java b/core/src/main/java/haveno/core/app/DomainInitialisation.java index 646d80d9dd..220fb05040 100644 --- a/core/src/main/java/haveno/core/app/DomainInitialisation.java +++ b/core/src/main/java/haveno/core/app/DomainInitialisation.java @@ -178,6 +178,9 @@ public class DomainInitialisation { closedTradableManager.onAllServicesInitialized(); failedTradesManager.onAllServicesInitialized(); + filterManager.setFilterWarningHandler(filterWarningHandler); + filterManager.onAllServicesInitialized(); + openOfferManager.onAllServicesInitialized(); balances.onAllServicesInitialized(); @@ -199,10 +202,6 @@ public class DomainInitialisation { priceFeedService.setCurrencyCodeOnInit(); priceFeedService.startRequestingPrices(); - filterManager.setFilterWarningHandler(filterWarningHandler); - filterManager.onAllServicesInitialized(); - - mobileNotificationService.onAllServicesInitialized(); myOfferTakenEvents.onAllServicesInitialized(); tradeEvents.onAllServicesInitialized(); diff --git a/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java b/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java index 0cf18224ba..fc6eb2d75c 100644 --- a/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java +++ b/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java @@ -86,7 +86,7 @@ public class HavenoHeadlessApp implements HeadlessApp { havenoSetup.setDisplaySecurityRecommendationHandler(key -> log.info("onDisplaySecurityRecommendationHandler")); havenoSetup.setWrongOSArchitectureHandler(msg -> log.error("onWrongOSArchitectureHandler. msg={}", msg)); havenoSetup.setRejectedTxErrorMessageHandler(errorMessage -> log.warn("setRejectedTxErrorMessageHandler. errorMessage={}", errorMessage)); - havenoSetup.setShowPopupIfInvalidBtcConfigHandler(() -> log.error("onShowPopupIfInvalidBtcConfigHandler")); + havenoSetup.setShowPopupIfInvalidXmrConfigHandler(() -> log.error("onShowPopupIfInvalidXmrConfigHandler")); havenoSetup.setRevolutAccountsUpdateHandler(revolutAccountList -> log.info("setRevolutAccountsUpdateHandler: revolutAccountList={}", revolutAccountList)); havenoSetup.setOsxKeyLoggerWarningHandler(() -> log.info("setOsxKeyLoggerWarningHandler")); havenoSetup.setQubesOSInfoHandler(() -> log.info("setQubesOSInfoHandler")); diff --git a/core/src/main/java/haveno/core/app/HavenoSetup.java b/core/src/main/java/haveno/core/app/HavenoSetup.java index eee13f3eec..4ac7b23512 100644 --- a/core/src/main/java/haveno/core/app/HavenoSetup.java +++ b/core/src/main/java/haveno/core/app/HavenoSetup.java @@ -158,7 +158,7 @@ public class HavenoSetup { rejectedTxErrorMessageHandler; @Setter @Nullable - private Consumer displayMoneroConnectionFallbackHandler; + private Consumer displayMoneroConnectionFallbackHandler; @Setter @Nullable private Consumer displayTorNetworkSettingsHandler; @@ -176,7 +176,7 @@ public class HavenoSetup { private Consumer displayPrivateNotificationHandler; @Setter @Nullable - private Runnable showPopupIfInvalidBtcConfigHandler; + private Runnable showPopupIfInvalidXmrConfigHandler; @Setter @Nullable private Consumer> revolutAccountsUpdateHandler; @@ -430,7 +430,7 @@ public class HavenoSetup { getXmrWalletSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout()); // listen for fallback handling - getConnectionServiceFallbackHandlerActive().addListener((observable, oldValue, newValue) -> { + getConnectionServiceFallbackHandler().addListener((observable, oldValue, newValue) -> { if (displayMoneroConnectionFallbackHandler == null) return; displayMoneroConnectionFallbackHandler.accept(newValue); }); @@ -461,7 +461,7 @@ public class HavenoSetup { havenoSetupListeners.forEach(HavenoSetupListener::onInitWallet); walletAppSetup.init(chainFileLockedExceptionHandler, showFirstPopupIfResyncSPVRequestedHandler, - showPopupIfInvalidBtcConfigHandler, + showPopupIfInvalidXmrConfigHandler, () -> {}, () -> {}); } @@ -734,8 +734,8 @@ public class HavenoSetup { return xmrConnectionService.getConnectionServiceErrorMsg(); } - public BooleanProperty getConnectionServiceFallbackHandlerActive() { - return xmrConnectionService.getConnectionServiceFallbackHandlerActive(); + public StringProperty getConnectionServiceFallbackHandler() { + return xmrConnectionService.getConnectionServiceFallbackHandler(); } public StringProperty getTopErrorMsg() { diff --git a/core/src/main/java/haveno/core/app/WalletAppSetup.java b/core/src/main/java/haveno/core/app/WalletAppSetup.java index d17c7ba366..7d37372afd 100644 --- a/core/src/main/java/haveno/core/app/WalletAppSetup.java +++ b/core/src/main/java/haveno/core/app/WalletAppSetup.java @@ -117,7 +117,7 @@ public class WalletAppSetup { void init(@Nullable Consumer chainFileLockedExceptionHandler, @Nullable Runnable showFirstPopupIfResyncSPVRequestedHandler, - @Nullable Runnable showPopupIfInvalidBtcConfigHandler, + @Nullable Runnable showPopupIfInvalidXmrConfigHandler, Runnable downloadCompleteHandler, Runnable walletInitializedHandler) { log.info("Initialize WalletAppSetup with monero-java v{}", MoneroUtils.getVersion()); @@ -199,8 +199,8 @@ public class WalletAppSetup { walletInitializedHandler.run(); }, exception -> { - if (exception instanceof InvalidHostException && showPopupIfInvalidBtcConfigHandler != null) { - showPopupIfInvalidBtcConfigHandler.run(); + if (exception instanceof InvalidHostException && showPopupIfInvalidXmrConfigHandler != null) { + showPopupIfInvalidXmrConfigHandler.run(); } else { walletServiceException.set(exception); } diff --git a/core/src/main/java/haveno/core/filter/FilterManager.java b/core/src/main/java/haveno/core/filter/FilterManager.java index cb7e0e9b21..2cebb66f3a 100644 --- a/core/src/main/java/haveno/core/filter/FilterManager.java +++ b/core/src/main/java/haveno/core/filter/FilterManager.java @@ -406,6 +406,10 @@ public class FilterManager { .anyMatch(e -> e.equals(address)); } + public String getDisableTradeBelowVersion() { + return getFilter() == null || getFilter().getDisableTradeBelowVersion() == null || getFilter().getDisableTradeBelowVersion().isEmpty() ? null : getFilter().getDisableTradeBelowVersion(); + } + public boolean requireUpdateToNewVersionForTrading() { if (getFilter() == null) { return false; diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 5df08a38ba..e68f90e484 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -97,6 +97,7 @@ import haveno.network.p2p.peers.PeerManager; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -134,7 +135,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe private static final long REPUBLISH_AGAIN_AT_STARTUP_DELAY_SEC = 30; private static final long REPUBLISH_INTERVAL_MS = TimeUnit.MINUTES.toMillis(30); private static final long REFRESH_INTERVAL_MS = OfferPayload.TTL / 2; - private static final int NUM_ATTEMPTS_THRESHOLD = 5; // process pending offer only on republish cycle after this many attempts + private static final int NUM_ATTEMPTS_THRESHOLD = 5; // process offer only on republish cycle after this many attempts private final CoreContext coreContext; private final KeyRing keyRing; @@ -236,7 +237,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe public void onAdded(Offer offer) { // cancel offer if reserved funds spent - Optional openOfferOptional = getOpenOfferById(offer.getId()); + Optional openOfferOptional = getOpenOffer(offer.getId()); if (openOfferOptional.isPresent() && openOfferOptional.get().getState() != OpenOffer.State.RESERVED && offer.isReservedFundsSpent()) { log.warn("Canceling open offer because reserved funds have been spent, offerId={}, state={}", offer.getId(), openOfferOptional.get().getState()); cancelOpenOffer(openOfferOptional.get(), null, null); @@ -474,19 +475,19 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // .forEach(openOffer -> OfferUtil.getInvalidMakerFeeTxErrorMessage(openOffer.getOffer(), btcWalletService) // .ifPresent(errorMsg -> invalidOffers.add(new Tuple2<>(openOffer, errorMsg)))); - // process pending offers - processPendingOffers(false, (transaction) -> {}, (errorMessage) -> { - log.warn("Error processing pending offers on bootstrap: " + errorMessage); + // processs offers + processOffers(false, (transaction) -> {}, (errorMessage) -> { + log.warn("Error processing offers on bootstrap: " + errorMessage); }); - // register to process pending offers on new block + // register to process offers on new block xmrWalletService.addWalletListener(new MoneroWalletListener() { @Override public void onNewBlock(long height) { - // process each pending offer on new block a few times, then rely on period republish - processPendingOffers(true, (transaction) -> {}, (errorMessage) -> { - log.warn("Error processing pending offers on new block {}: {}", height, errorMessage); + // process each offer on new block a few times, then rely on period republish + processOffers(true, (transaction) -> {}, (errorMessage) -> { + log.warn("Error processing offers on new block {}: {}", height, errorMessage); }); } }); @@ -554,13 +555,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe synchronized (processOffersLock) { CountDownLatch latch = new CountDownLatch(1); addOpenOffer(openOffer); - processPendingOffer(getOpenOffers(), openOffer, (transaction) -> { + processOffer(getOpenOffers(), openOffer, (transaction) -> { requestPersistence(); latch.countDown(); resultHandler.handleResult(transaction); }, (errorMessage) -> { if (!openOffer.isCanceled()) { - log.warn("Error processing pending offer {}: {}", openOffer.getId(), errorMessage); + log.warn("Error processing offer {}: {}", openOffer.getId(), errorMessage); doCancelOffer(openOffer, resetAddressEntriesOnError); } latch.countDown(); @@ -573,12 +574,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // Remove from offerbook public void removeOffer(Offer offer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - Optional openOfferOptional = getOpenOfferById(offer.getId()); + Optional openOfferOptional = getOpenOffer(offer.getId()); if (openOfferOptional.isPresent()) { cancelOpenOffer(openOfferOptional.get(), resultHandler, errorMessageHandler); } else { - log.warn("Offer was not found in our list of open offers. We still try to remove it from the offerbook."); - errorMessageHandler.handleErrorMessage("Offer was not found in our list of open offers. " + "We still try to remove it from the offerbook."); + String errorMsg = "Offer was not found in our list of open offers. We still try to remove it from the offerbook."; + log.warn(errorMsg); + errorMessageHandler.handleErrorMessage(errorMsg); offerBookService.removeOffer(offer.getOfferPayload(), () -> offer.setState(Offer.State.REMOVED), null); } } @@ -686,7 +688,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe OpenOffer.State originalState, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - Optional openOfferOptional = getOpenOfferById(editedOffer.getId()); + Optional openOfferOptional = getOpenOffer(editedOffer.getId()); if (openOfferOptional.isPresent()) { OpenOffer openOffer = openOfferOptional.get(); @@ -705,12 +707,21 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe addOpenOffer(editedOpenOffer); - if (editedOpenOffer.isAvailable()) - maybeRepublishOffer(editedOpenOffer); + // reset arbitrator signature if invalid + Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(editedOpenOffer.getOffer().getOfferPayload().getArbitratorSigner()); + if (arbitrator == null || !HavenoUtils.isArbitratorSignatureValid(editedOpenOffer.getOffer().getOfferPayload(), arbitrator)) { + editedOpenOffer.getOffer().getOfferPayload().setArbitratorSignature(null); + editedOpenOffer.getOffer().getOfferPayload().setArbitratorSigner(null); + } - offersToBeEdited.remove(openOffer.getId()); - requestPersistence(); - resultHandler.handleResult(); + // process offer which might sign and publish + processOffer(getOpenOffers(), editedOpenOffer, (transaction) -> { + offersToBeEdited.remove(openOffer.getId()); + requestPersistence(); + resultHandler.handleResult(); + }, (errorMsg) -> { + errorMessageHandler.handleErrorMessage(errorMsg); + }); } else { errorMessageHandler.handleErrorMessage("There is no offer with this id existing to be published."); } @@ -727,6 +738,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } else { resultHandler.handleResult(); } + requestPersistence(); } else { errorMessageHandler.handleErrorMessage("Editing of offer can't be canceled as it is not edited."); } @@ -736,7 +748,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe doCancelOffer(openOffer, true); } - // remove open offer which thaws its key images + // cancel open offer which thaws its key images private void doCancelOffer(@NotNull OpenOffer openOffer, boolean resetAddressEntries) { Offer offer = openOffer.getOffer(); offer.setState(Offer.State.REMOVED); @@ -750,7 +762,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // close open offer after key images spent public void closeOpenOffer(Offer offer) { - getOpenOfferById(offer.getId()).ifPresent(openOffer -> { + getOpenOffer(offer.getId()).ifPresent(openOffer -> { removeOpenOffer(openOffer); openOffer.setState(OpenOffer.State.CLOSED); xmrWalletService.resetAddressEntriesForOpenOffer(offer.getId()); @@ -813,14 +825,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return openOffers.getObservableList(); } - public Optional getOpenOfferById(String offerId) { + public Optional getOpenOffer(String offerId) { synchronized (openOffers) { return openOffers.stream().filter(e -> e.getId().equals(offerId)).findFirst(); } } public boolean hasOpenOffer(String offerId) { - return getOpenOfferById(offerId).isPresent(); + return getOpenOffer(offerId).isPresent(); } public Optional getSignedOfferById(String offerId) { @@ -881,30 +893,21 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe /////////////////////////////////////////////////////////////////////////////////////////// // Place offer helpers /////////////////////////////////////////////////////////////////////////////////////////// - private void processPendingOffers(boolean skipOffersWithTooManyAttempts, + private void processOffers(boolean skipOffersWithTooManyAttempts, TransactionResultHandler resultHandler, // TODO (woodser): transaction not needed with result handler ErrorMessageHandler errorMessageHandler) { ThreadUtils.execute(() -> { List errorMessages = new ArrayList(); synchronized (processOffersLock) { List openOffers = getOpenOffers(); - for (OpenOffer pendingOffer : openOffers) { - if (pendingOffer.getState() != OpenOffer.State.PENDING) continue; - if (skipOffersWithTooManyAttempts && pendingOffer.getNumProcessingAttempts() > NUM_ATTEMPTS_THRESHOLD) continue; // skip offers with too many attempts + removeOffersWithDuplicateKeyImages(openOffers); + for (OpenOffer offer : openOffers) { + if (skipOffersWithTooManyAttempts && offer.getNumProcessingAttempts() > NUM_ATTEMPTS_THRESHOLD) continue; // skip offers with too many attempts CountDownLatch latch = new CountDownLatch(1); - processPendingOffer(openOffers, pendingOffer, (transaction) -> { + processOffer(openOffers, offer, (transaction) -> { latch.countDown(); }, errorMessage -> { - if (!pendingOffer.isCanceled()) { - String warnMessage = "Error processing pending offer, offerId=" + pendingOffer.getId() + ", attempt=" + pendingOffer.getNumProcessingAttempts() + ": " + errorMessage; - errorMessages.add(warnMessage); - - // cancel offer if invalid - if (pendingOffer.getOffer().getState() == Offer.State.INVALID) { - log.warn("Canceling offer because it's invalid: {}", pendingOffer.getId()); - doCancelOffer(pendingOffer); - } - } + errorMessages.add(errorMessage); latch.countDown(); }); HavenoUtils.awaitLatch(latch); @@ -919,7 +922,29 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe }, THREAD_ID); } - private void processPendingOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + private void removeOffersWithDuplicateKeyImages(List openOffers) { + + // collect offers with duplicate key images + Set keyImages = new HashSet<>(); + Set offersToRemove = new HashSet<>(); + for (OpenOffer openOffer : openOffers) { + if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() == null) continue; + if (Collections.disjoint(keyImages, openOffer.getOffer().getOfferPayload().getReserveTxKeyImages())) { + keyImages.addAll(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages()); + } else { + offersToRemove.add(openOffer); + } + } + + // remove offers with duplicate key images + for (OpenOffer offerToRemove : offersToRemove) { + log.warn("Removing open offer which has duplicate key images with other open offers: {}", offerToRemove.getId()); + doCancelOffer(offerToRemove); + openOffers.remove(offerToRemove); + } + } + + private void processOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { // skip if already processing if (openOffer.isProcessing()) { @@ -929,23 +954,33 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // process offer openOffer.setProcessing(true); - doProcessPendingOffer(openOffers, openOffer, (transaction) -> { + doProcessOffer(openOffers, openOffer, (transaction) -> { openOffer.setProcessing(false); resultHandler.handleResult(transaction); }, (errorMsg) -> { openOffer.setProcessing(false); openOffer.setNumProcessingAttempts(openOffer.getNumProcessingAttempts() + 1); openOffer.getOffer().setErrorMessage(errorMsg); + if (!openOffer.isCanceled()) { + errorMsg = "Error processing offer, offerId=" + openOffer.getId() + ", attempt=" + openOffer.getNumProcessingAttempts() + ": " + errorMsg; + openOffer.getOffer().setErrorMessage(errorMsg); + + // cancel offer if invalid + if (openOffer.getOffer().getState() == Offer.State.INVALID) { + log.warn("Canceling offer because it's invalid: {}", openOffer.getId()); + doCancelOffer(openOffer); + } + } errorMessageHandler.handleErrorMessage(errorMsg); }); } - private void doProcessPendingOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + private void doProcessOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { new Thread(() -> { try { - // done processing if wallet not initialized - if (xmrWalletService.getWallet() == null) { + // done processing if canceled or wallet not initialized + if (openOffer.isCanceled() || xmrWalletService.getWallet() == null) { resultHandler.handleResult(null); return; } @@ -958,6 +993,33 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return; } + // validate non-pending state + if (!openOffer.isPending()) { + boolean isValid = true; + Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(openOffer.getOffer().getOfferPayload().getArbitratorSigner()); + if (openOffer.getOffer().getOfferPayload().getArbitratorSigner() == null) { + isValid = false; + } else if (arbitrator == null) { + log.warn("Offer {} signed by unavailable arbitrator, reposting", openOffer.getId()); + isValid = false; + } else if (!HavenoUtils.isArbitratorSignatureValid(openOffer.getOffer().getOfferPayload(), arbitrator)) { + log.warn("Offer {} has invalid arbitrator signature, reposting", openOffer.getId()); + isValid = false; + } + if ((openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null || openOffer.getOffer().getOfferPayload().getReserveTxKeyImages().isEmpty()) && (openOffer.getReserveTxHash() == null || openOffer.getReserveTxHash().isEmpty())) { + log.warn("Offer {} is missing reserve tx hash but has reserved key images, reposting", openOffer.getId()); + isValid = false; + } + if (isValid) { + resultHandler.handleResult(null); + return; + } else { + openOffer.getOffer().getOfferPayload().setArbitratorSignature(null); + openOffer.getOffer().getOfferPayload().setArbitratorSigner(null); + if (openOffer.isAvailable()) openOffer.setState(OpenOffer.State.PENDING); + } + } + // cancel offer if scheduled txs unavailable if (openOffer.getScheduledTxHashes() != null) { boolean scheduledTxsAvailable = true; @@ -975,6 +1037,12 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } } + // sign and post offer if already funded + if (openOffer.getReserveTxHash() != null) { + signAndPostOffer(openOffer, false, resultHandler, errorMessageHandler); + return; + } + // get amount needed to reserve offer BigInteger amountNeeded = openOffer.getOffer().getAmountNeeded(); @@ -987,43 +1055,30 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe setSplitOutputTx(openOffer, splitOutputTx); } - // if not found, create tx to split exact output - if (splitOutputTx == null) { - if (openOffer.getSplitOutputTxHash() != null) { - log.warn("Split output tx unexpectedly unavailable for offer, offerId={}, split output tx={}", openOffer.getId(), openOffer.getSplitOutputTxHash()); - setSplitOutputTx(openOffer, null); - } - try { - splitOrSchedule(openOffers, openOffer, amountNeeded); - } catch (Exception e) { - log.warn("Unable to split or schedule funds for offer {}: {}", openOffer.getId(), e.getMessage()); - openOffer.getOffer().setState(Offer.State.INVALID); - errorMessageHandler.handleErrorMessage(e.getMessage()); - return; - } - } else if (!splitOutputTx.isLocked()) { - - // otherwise sign and post offer if split output available - signAndPostOffer(openOffer, true, resultHandler, errorMessageHandler); + // if wallet has exact available balance, try to sign and post directly + if (xmrWalletService.getAvailableBalance().equals(amountNeeded)) { + signAndPostOffer(openOffer, true, resultHandler, (errorMessage) -> { + splitOrSchedule(splitOutputTx, openOffers, openOffer, amountNeeded, resultHandler, errorMessageHandler); + }); return; + } else { + splitOrSchedule(splitOutputTx, openOffers, openOffer, amountNeeded, resultHandler, errorMessageHandler); } } else { // sign and post offer if enough funds - boolean hasFundsReserved = openOffer.getReserveTxHash() != null; boolean hasSufficientBalance = xmrWalletService.getAvailableBalance().compareTo(amountNeeded) >= 0; - if (hasFundsReserved || hasSufficientBalance) { + if (hasSufficientBalance) { signAndPostOffer(openOffer, true, resultHandler, errorMessageHandler); return; } else if (openOffer.getScheduledTxHashes() == null) { scheduleWithEarliestTxs(openOffers, openOffer); + resultHandler.handleResult(null); + return; } } - - // handle result - resultHandler.handleResult(null); } catch (Exception e) { - if (!openOffer.isCanceled()) log.error("Error processing pending offer: {}\n", e.getMessage(), e); + if (!openOffer.isCanceled()) log.error("Error processing offer: {}\n", e.getMessage(), e); errorMessageHandler.handleErrorMessage(e.getMessage()); } }).start(); @@ -1087,13 +1142,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe if (output.isSpent() || output.isFrozen()) removeTxs.add(tx); } } - if (!hasExactAmount(tx, reserveAmount, preferredSubaddressIndex)) removeTxs.add(tx); + if (!hasExactOutput(tx, reserveAmount, preferredSubaddressIndex)) removeTxs.add(tx); } splitOutputTxs.removeAll(removeTxs); return splitOutputTxs; } - private boolean hasExactAmount(MoneroTxWallet tx, BigInteger amount, Integer preferredSubaddressIndex) { + private boolean hasExactOutput(MoneroTxWallet tx, BigInteger amount, Integer preferredSubaddressIndex) { boolean hasExactOutput = (tx.getOutputsWallet(new MoneroOutputQuery() .setAccountIndex(0) .setSubaddressIndex(preferredSubaddressIndex) @@ -1115,7 +1170,35 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return earliestUnscheduledTx; } - private void splitOrSchedule(List openOffers, OpenOffer openOffer, BigInteger offerReserveAmount) { + // if split tx not found and cannot reserve exact amount directly, create tx to split or reserve exact output + private void splitOrSchedule(MoneroTxWallet splitOutputTx, List openOffers, OpenOffer openOffer, BigInteger amountNeeded, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + if (splitOutputTx == null) { + if (openOffer.getSplitOutputTxHash() != null) { + log.warn("Split output tx unexpectedly unavailable for offer, offerId={}, split output tx={}", openOffer.getId(), openOffer.getSplitOutputTxHash()); + setSplitOutputTx(openOffer, null); + } + try { + splitOrScheduleAux(openOffers, openOffer, amountNeeded); + resultHandler.handleResult(null); + return; + } catch (Exception e) { + log.warn("Unable to split or schedule funds for offer {}: {}", openOffer.getId(), e.getMessage()); + openOffer.getOffer().setState(Offer.State.INVALID); + errorMessageHandler.handleErrorMessage(e.getMessage()); + return; + } + } else if (!splitOutputTx.isLocked()) { + + // otherwise sign and post offer if split output available + signAndPostOffer(openOffer, true, resultHandler, errorMessageHandler); + return; + } else { + resultHandler.handleResult(null); + return; + } + } + + private void splitOrScheduleAux(List openOffers, OpenOffer openOffer, BigInteger offerReserveAmount) { // handle sufficient available balance to split output boolean sufficientAvailableBalance = xmrWalletService.getAvailableBalance().compareTo(offerReserveAmount) >= 0; @@ -1294,18 +1377,17 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe transaction -> { // set offer state - openOffer.setState(OpenOffer.State.AVAILABLE); openOffer.setScheduledTxHashes(null); openOffer.setScheduledAmount(null); requestPersistence(); - resultHandler.handleResult(transaction); if (!stopped) { startPeriodicRepublishOffersTimer(); startPeriodicRefreshOffersTimer(); } else { log.debug("We have stopped already. We ignore that placeOfferProtocol.placeOffer.onResult call."); } + resultHandler.handleResult(transaction); }, errorMessageHandler); @@ -1355,6 +1437,40 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return; } + // verify max length of extra info + if (offer.getOfferPayload().getExtraInfo() != null && offer.getOfferPayload().getExtraInfo().length() > Restrictions.MAX_EXTRA_INFO_LENGTH) { + errorMessage = "Extra info is too long for offer " + request.offerId + ". Max length is " + Restrictions.MAX_EXTRA_INFO_LENGTH + " but got " + offer.getOfferPayload().getExtraInfo().length(); + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + + // verify the trade protocol version + if (request.getOfferPayload().getProtocolVersion() != Version.TRADE_PROTOCOL_VERSION) { + errorMessage = "Unsupported protocol version: " + request.getOfferPayload().getProtocolVersion(); + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + + // verify the min version number + if (filterManager.getDisableTradeBelowVersion() != null) { + if (Version.compare(request.getOfferPayload().getVersionNr(), filterManager.getDisableTradeBelowVersion()) < 0) { + errorMessage = "Offer version number is too low: " + request.getOfferPayload().getVersionNr() + " < " + filterManager.getDisableTradeBelowVersion(); + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + } + + // verify the max version number + if (Version.compare(request.getOfferPayload().getVersionNr(), Version.VERSION) > 0) { + errorMessage = "Offer version number is too high: " + request.getOfferPayload().getVersionNr() + " > " + Version.VERSION; + log.warn(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + // verify maker and taker fees boolean hasBuyerAsTakerWithoutDeposit = offer.getDirection() == OfferDirection.SELL && offer.isPrivateOffer() && offer.getChallengeHash() != null && offer.getChallengeHash().length() > 0 && offer.getTakerFeePct() == 0; if (hasBuyerAsTakerWithoutDeposit) { @@ -1557,6 +1673,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return; } + // Don't allow trade start if not connected to Monero node + if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) { + errorMessage = "We got a handleOfferAvailabilityRequest but we are not connected to a Monero node."; + log.info(errorMessage); + sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); + return; + } + if (stopped) { errorMessage = "We have stopped already. We ignore that handleOfferAvailabilityRequest call."; log.debug(errorMessage); @@ -1575,7 +1699,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } try { - Optional openOfferOptional = getOpenOfferById(request.offerId); + Optional openOfferOptional = getOpenOffer(request.offerId); AvailabilityResult availabilityResult; byte[] makerSignature = null; if (openOfferOptional.isPresent()) { @@ -1801,27 +1925,20 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe originalOfferPayload.getChallengeHash(), updatedExtraDataMap, protocolVersion, - originalOfferPayload.getArbitratorSigner(), - originalOfferPayload.getArbitratorSignature(), - originalOfferPayload.getReserveTxKeyImages(), + null, + null, + null, originalOfferPayload.getExtraInfo()); - // Save states from original data to use for the updated - Offer.State originalOfferState = originalOffer.getState(); - OpenOffer.State originalOpenOfferState = originalOpenOffer.getState(); + // cancel old offer + log.info("Canceling outdated offer id={}", originalOffer.getId()); + doCancelOffer(originalOpenOffer, false); - // remove old offer - originalOffer.setState(Offer.State.REMOVED); - originalOpenOffer.setState(OpenOffer.State.CANCELED); - removeOpenOffer(originalOpenOffer); - - // Create new Offer + // create new offer Offer updatedOffer = new Offer(updatedPayload); updatedOffer.setPriceFeedService(priceFeedService); - updatedOffer.setState(originalOfferState); OpenOffer updatedOpenOffer = new OpenOffer(updatedOffer, originalOpenOffer.getTriggerPrice()); - updatedOpenOffer.setState(originalOpenOfferState); addOpenOffer(updatedOpenOffer); requestPersistence(); @@ -1873,10 +1990,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } } - private void maybeRepublishOffer(OpenOffer openOffer) { - maybeRepublishOffer(openOffer, null); - } - private void maybeRepublishOffer(OpenOffer openOffer, @Nullable Runnable completeHandler) { ThreadUtils.execute(() -> { @@ -1886,81 +1999,54 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return; } - // determine if offer is valid - boolean isValid = true; - Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(openOffer.getOffer().getOfferPayload().getArbitratorSigner()); - if (arbitrator == null) { - log.warn("Offer {} signed by unavailable arbitrator, reposting", openOffer.getId()); - isValid = false; - } else if (!HavenoUtils.isArbitratorSignatureValid(openOffer.getOffer().getOfferPayload(), arbitrator)) { - log.warn("Offer {} has invalid arbitrator signature, reposting", openOffer.getId()); - isValid = false; - } - if ((openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null || openOffer.getOffer().getOfferPayload().getReserveTxKeyImages().isEmpty()) && (openOffer.getReserveTxHash() == null || openOffer.getReserveTxHash().isEmpty())) { - log.warn("Offer {} is missing reserve tx hash but has reserved key images, reposting", openOffer.getId()); - isValid = false; - } + // reprocess offer then publish + synchronized (processOffersLock) { + CountDownLatch latch = new CountDownLatch(1); + processOffer(getOpenOffers(), openOffer, (transaction) -> { + requestPersistence(); + latch.countDown(); - // if valid, re-add offer to book - if (isValid) { - offerBookService.addOffer(openOffer.getOffer(), - () -> { - if (!stopped) { - - // refresh means we send only the data needed to refresh the TTL (hash, signature and sequence no.) - if (periodicRefreshOffersTimer == null) { - startPeriodicRefreshOffersTimer(); - } - if (completeHandler != null) { - completeHandler.run(); - } - } - }, - errorMessage -> { - if (!stopped) { - log.error("Adding offer to P2P network failed. " + errorMessage); - stopRetryRepublishOffersTimer(); - retryRepublishOffersTimer = UserThread.runAfter(OpenOfferManager.this::republishOffers, - RETRY_REPUBLISH_DELAY_SEC); - if (completeHandler != null) completeHandler.run(); - } - }); - } else { - - // reset offer state to pending - openOffer.getOffer().getOfferPayload().setArbitratorSignature(null); - openOffer.getOffer().getOfferPayload().setArbitratorSigner(null); - openOffer.getOffer().setState(Offer.State.UNKNOWN); - openOffer.setState(OpenOffer.State.PENDING); - - // republish offer - synchronized (processOffersLock) { - CountDownLatch latch = new CountDownLatch(1); - processPendingOffer(getOpenOffers(), openOffer, (transaction) -> { - requestPersistence(); - latch.countDown(); + // skip if prevented from publishing + if (preventedFromPublishing(openOffer)) { if (completeHandler != null) completeHandler.run(); - }, (errorMessage) -> { - if (!openOffer.isCanceled()) { - log.warn("Error republishing offer {}: {}", openOffer.getId(), errorMessage); - openOffer.getOffer().setErrorMessage(errorMessage); + return; + } + + // publish offer to books + offerBookService.addOffer(openOffer.getOffer(), + () -> { + if (!stopped) { - // cancel offer if invalid - if (openOffer.getOffer().getState() == Offer.State.INVALID) { - log.warn("Canceling offer because it's invalid: {}", openOffer.getId()); - doCancelOffer(openOffer); - } - } - latch.countDown(); - if (completeHandler != null) completeHandler.run(); - }); - HavenoUtils.awaitLatch(latch); - } + // refresh means we send only the data needed to refresh the TTL (hash, signature and sequence no.) + if (periodicRefreshOffersTimer == null) { + startPeriodicRefreshOffersTimer(); + } + if (completeHandler != null) { + completeHandler.run(); + } + } + }, + errorMessage -> { + if (!stopped) { + log.error("Adding offer to P2P network failed. " + errorMessage); + stopRetryRepublishOffersTimer(); + retryRepublishOffersTimer = UserThread.runAfter(OpenOfferManager.this::republishOffers, + RETRY_REPUBLISH_DELAY_SEC); + if (completeHandler != null) completeHandler.run(); + } + }); + }, (errorMessage) -> { + log.warn("Error republishing offer {}: {}", openOffer.getId(), errorMessage); + latch.countDown(); + if (completeHandler != null) completeHandler.run(); + }); + HavenoUtils.awaitLatch(latch); } }, THREAD_ID); } private boolean preventedFromPublishing(OpenOffer openOffer) { + if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) return true; return openOffer.isDeactivated() || openOffer.isCanceled() || openOffer.getOffer().getOfferPayload().getArbitratorSigner() == null; } @@ -1983,25 +2069,27 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe if (periodicRefreshOffersTimer == null) periodicRefreshOffersTimer = UserThread.runPeriodically(() -> { if (!stopped) { - int size = openOffers.size(); - //we clone our list as openOffers might change during our delayed call - final ArrayList openOffersList = new ArrayList<>(openOffers.getList()); - for (int i = 0; i < size; i++) { - // we delay to avoid reaching throttle limits - // roughly 4 offers per second - - long delay = 300; - final long minDelay = (i + 1) * delay; - final long maxDelay = (i + 2) * delay; - final OpenOffer openOffer = openOffersList.get(i); - UserThread.runAfterRandomDelay(() -> { - // we need to check if in the meantime the offer has been removed - boolean contained = false; - synchronized (openOffers) { - contained = openOffers.contains(openOffer); - } - if (contained) maybeRefreshOffer(openOffer, 0, 1); - }, minDelay, maxDelay, TimeUnit.MILLISECONDS); + synchronized (openOffers) { + int size = openOffers.size(); + //we clone our list as openOffers might change during our delayed call + final ArrayList openOffersList = new ArrayList<>(openOffers.getList()); + for (int i = 0; i < size; i++) { + // we delay to avoid reaching throttle limits + // roughly 4 offers per second + + long delay = 300; + final long minDelay = (i + 1) * delay; + final long maxDelay = (i + 2) * delay; + final OpenOffer openOffer = openOffersList.get(i); + UserThread.runAfterRandomDelay(() -> { + // we need to check if in the meantime the offer has been removed + boolean contained = false; + synchronized (openOffers) { + contained = openOffers.contains(openOffer); + } + if (contained) maybeRefreshOffer(openOffer, 0, 1); + }, minDelay, maxDelay, TimeUnit.MILLISECONDS); + } } } else { log.debug("We have stopped already. We ignore that periodicRefreshOffersTimer.run call."); diff --git a/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java b/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java index 0b22d9e40e..68d5f9da4f 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java @@ -23,7 +23,7 @@ import haveno.common.handlers.ErrorMessageHandler; import haveno.common.taskrunner.TaskRunner; import haveno.core.locale.Res; import haveno.core.offer.messages.SignOfferResponse; -import haveno.core.offer.placeoffer.tasks.AddToOfferBook; +import haveno.core.offer.placeoffer.tasks.MaybeAddToOfferBook; import haveno.core.offer.placeoffer.tasks.MakerProcessSignOfferResponse; import haveno.core.offer.placeoffer.tasks.MakerReserveOfferFunds; import haveno.core.offer.placeoffer.tasks.MakerSendSignOfferRequest; @@ -135,7 +135,7 @@ public class PlaceOfferProtocol { ); taskRunner.addTasks( MakerProcessSignOfferResponse.class, - AddToOfferBook.class + MaybeAddToOfferBook.class ); taskRunner.run(); 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 0d9271a41e..e873d1e561 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 @@ -87,6 +87,9 @@ public class MakerReserveOfferFunds extends Task { try { //if (true) throw new RuntimeException("Pretend error"); reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex); + } catch (IllegalStateException e) { + log.warn("Illegal state creating reserve tx, offerId={}, error={}", openOffer.getShortId(), i + 1, e.getMessage()); + throw e; } catch (Exception e) { log.warn("Error creating reserve tx, offerId={}, attempt={}/{}, error={}", openOffer.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); model.getXmrWalletService().handleWalletError(e, sourceConnection); 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 2037b51d09..3644492735 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 @@ -77,7 +77,7 @@ public class MakerSendSignOfferRequest extends Task { offer.getOfferPayload().getReserveTxKeyImages(), returnAddress); - // send request to least used arbitrators until success + // send request to random arbitrators until success sendSignOfferRequests(request, () -> { complete(); }, (errorMessage) -> { diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/AddToOfferBook.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MaybeAddToOfferBook.java similarity index 58% rename from core/src/main/java/haveno/core/offer/placeoffer/tasks/AddToOfferBook.java rename to core/src/main/java/haveno/core/offer/placeoffer/tasks/MaybeAddToOfferBook.java index c5c3cf4f46..8e3e3c23bc 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/AddToOfferBook.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MaybeAddToOfferBook.java @@ -20,13 +20,14 @@ package haveno.core.offer.placeoffer.tasks; import haveno.common.taskrunner.Task; import haveno.common.taskrunner.TaskRunner; import haveno.core.offer.Offer; +import haveno.core.offer.OpenOffer; import haveno.core.offer.placeoffer.PlaceOfferModel; import static com.google.common.base.Preconditions.checkNotNull; -public class AddToOfferBook extends Task { +public class MaybeAddToOfferBook extends Task { - public AddToOfferBook(TaskRunner taskHandler, PlaceOfferModel model) { + public MaybeAddToOfferBook(TaskRunner taskHandler, PlaceOfferModel model) { super(taskHandler, model); } @@ -35,17 +36,22 @@ public class AddToOfferBook extends Task { try { runInterceptHook(); checkNotNull(model.getSignOfferResponse().getSignedOfferPayload().getArbitratorSignature(), "Offer's arbitrator signature is null: " + model.getOpenOffer().getOffer().getId()); - model.getOfferBookService().addOffer(new Offer(model.getSignOfferResponse().getSignedOfferPayload()), - () -> { - model.setOfferAddedToOfferBook(true); - complete(); - }, - errorMessage -> { - model.getOpenOffer().getOffer().setErrorMessage("Could not add offer to offerbook.\n" + - "Please check your network connection and try again."); - - failed(errorMessage); - }); + if (model.getOpenOffer().isPending() || model.getOpenOffer().isAvailable()) { + model.getOfferBookService().addOffer(new Offer(model.getSignOfferResponse().getSignedOfferPayload()), + () -> { + model.getOpenOffer().setState(OpenOffer.State.AVAILABLE); + model.setOfferAddedToOfferBook(true); + complete(); + }, + errorMessage -> { + model.getOpenOffer().getOffer().setErrorMessage("Could not add offer to offerbook.\n" + + "Please check your network connection and try again."); + failed(errorMessage); + }); + } else { + complete(); + return; + } } catch (Throwable t) { model.getOpenOffer().getOffer().setErrorMessage("An error occurred.\n" + "Error message:\n" diff --git a/core/src/main/java/haveno/core/payment/PaymentAccount.java b/core/src/main/java/haveno/core/payment/PaymentAccount.java index 8dda8fdedd..14dd88482b 100644 --- a/core/src/main/java/haveno/core/payment/PaymentAccount.java +++ b/core/src/main/java/haveno/core/payment/PaymentAccount.java @@ -36,6 +36,7 @@ package haveno.core.payment; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; import haveno.common.proto.ProtoUtil; import haveno.common.proto.persistable.PersistablePayload; import haveno.common.util.Utilities; @@ -341,12 +342,29 @@ public abstract class PaymentAccount implements PersistablePayload { // ---------------------------- SERIALIZATION ----------------------------- public String toJson() { - Map jsonMap = new HashMap(); - if (paymentAccountPayload != null) jsonMap.putAll(gsonBuilder.create().fromJson(paymentAccountPayload.toJson(), (Type) Object.class)); + Gson gson = gsonBuilder.create(); + Map jsonMap = new HashMap<>(); + + if (paymentAccountPayload != null) { + String payloadJson = paymentAccountPayload.toJson(); + Map payloadMap = gson.fromJson(payloadJson, new TypeToken>() {}.getType()); + + for (Map.Entry entry : payloadMap.entrySet()) { + Object value = entry.getValue(); + if (value instanceof List) { + List list = (List) value; + String joinedString = list.stream().map(Object::toString).collect(Collectors.joining(",")); + entry.setValue(joinedString); + } + } + + jsonMap.putAll(payloadMap); + } + jsonMap.put("accountName", getAccountName()); jsonMap.put("accountId", getId()); if (paymentAccountPayload != null) jsonMap.put("salt", getSaltAsHex()); - return gsonBuilder.create().toJson(jsonMap); + return gson.toJson(jsonMap); } /** @@ -388,12 +406,7 @@ public abstract class PaymentAccount implements PersistablePayload { PaymentAccountForm form = new PaymentAccountForm(PaymentAccountForm.FormId.valueOf(paymentMethod.getId())); for (PaymentAccountFormField.FieldId fieldId : getInputFieldIds()) { PaymentAccountFormField field = getEmptyFormField(fieldId); - Object value = jsonMap.get(HavenoUtils.toCamelCase(field.getId().toString())); - if (value instanceof List) { // TODO: list should already be serialized to comma delimited string in PaymentAccount.toJson() (PaymentAccountTypeAdapter?) - field.setValue(String.join(",", (List) value)); - } else { - field.setValue((String) value); - } + field.setValue((String) jsonMap.get(HavenoUtils.toCamelCase(field.getId().toString()))); form.getFields().add(field); } return form; diff --git a/core/src/main/java/haveno/core/provider/ProvidersRepository.java b/core/src/main/java/haveno/core/provider/ProvidersRepository.java index 8357bb7f08..08736e0f76 100644 --- a/core/src/main/java/haveno/core/provider/ProvidersRepository.java +++ b/core/src/main/java/haveno/core/provider/ProvidersRepository.java @@ -52,7 +52,8 @@ public class ProvidersRepository { private static final String DEFAULT_LOCAL_NODE = "http://localhost:8078/"; private static final List DEFAULT_NODES = Arrays.asList( "http://elaxlgigphpicy5q7pi5wkz2ko2vgjbq4576vic7febmx4xcxvk6deqd.onion/", // Haveno - "http://lrrgpezvdrbpoqvkavzobmj7dr2otxc5x6wgktrw337bk6mxsvfp5yid.onion/" // Cake + "http://lrrgpezvdrbpoqvkavzobmj7dr2otxc5x6wgktrw337bk6mxsvfp5yid.onion/", // Cake + "http://2c6y3sqmknakl3fkuwh4tjhxb2q5isr53dnfcqs33vt3y7elujc6tyad.onion/" // boldsuck ); private final Config config; diff --git a/core/src/main/java/haveno/core/support/SupportManager.java b/core/src/main/java/haveno/core/support/SupportManager.java index 10cbfdafaf..4bb8e86d82 100644 --- a/core/src/main/java/haveno/core/support/SupportManager.java +++ b/core/src/main/java/haveno/core/support/SupportManager.java @@ -232,10 +232,12 @@ public abstract class SupportManager { getAllChatMessages(ackMessage.getSourceId()).stream() .filter(msg -> msg.getUid().equals(ackMessage.getSourceUid())) .forEach(msg -> { - if (ackMessage.isSuccess()) - msg.setAcknowledged(true); - else - msg.setAckError(ackMessage.getErrorMessage()); + UserThread.execute(() -> { + if (ackMessage.isSuccess()) + msg.setAcknowledged(true); + else + msg.setAckError(ackMessage.getErrorMessage()); + }); }); requestPersistence(); } 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 64de6b3f2d..047fe85456 100644 --- a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java @@ -399,13 +399,6 @@ public abstract class DisputeManager> extends Sup chatMessage.setSystemMessage(true); dispute.addAndPersistChatMessage(chatMessage); - // export latest multisig hex - try { - trade.exportMultisigHex(); - } catch (Exception e) { - log.error("Failed to export multisig hex", e); - } - // create dispute opened message NodeAddress agentNodeAddress = getAgentNodeAddress(dispute); DisputeOpenedMessage disputeOpenedMessage = new DisputeOpenedMessage(dispute, @@ -578,8 +571,9 @@ public abstract class DisputeManager> extends Sup trade.advanceState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); } - // update multisig hex - if (message.getUpdatedMultisigHex() != null) sender.setUpdatedMultisigHex(message.getUpdatedMultisigHex()); + // update opener's multisig hex + TradePeer opener = sender == trade.getArbitrator() ? trade.getTradePeer() : sender; + if (message.getOpenerUpdatedMultisigHex() != null) opener.setUpdatedMultisigHex(message.getOpenerUpdatedMultisigHex()); // add chat message with price info if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0); @@ -605,7 +599,7 @@ public abstract class DisputeManager> extends Sup if (trade.isArbitrator()) { TradePeer senderPeer = sender == trade.getMaker() ? trade.getTaker() : trade.getMaker(); if (senderPeer != trade.getMaker() && senderPeer != trade.getTaker()) throw new RuntimeException("Sender peer is not maker or taker, address=" + senderPeer.getNodeAddress()); - sendDisputeOpenedMessageToPeer(dispute, contract, senderPeer.getPubKeyRing(), trade.getSelf().getUpdatedMultisigHex()); + sendDisputeOpenedMessageToPeer(dispute, contract, senderPeer.getPubKeyRing(), opener.getUpdatedMultisigHex()); } tradeManager.requestPersistence(); errorMessage = null; diff --git a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java index 50be387c76..6ef9cd69ed 100644 --- a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java +++ b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java @@ -62,7 +62,6 @@ import haveno.core.support.dispute.messages.DisputeClosedMessage; import haveno.core.support.dispute.messages.DisputeOpenedMessage; import haveno.core.support.messages.ChatMessage; import haveno.core.support.messages.SupportMessage; -import haveno.core.trade.BuyerTrade; import haveno.core.trade.ClosedTradableManager; import haveno.core.trade.Contract; import haveno.core.trade.HavenoUtils; @@ -464,14 +463,6 @@ public final class ArbitrationManager extends DisputeManager tradeManager.requestPersistence(); } } else { - Optional openOfferOptional = openOfferManager.getOpenOfferById(tradeId); + Optional openOfferOptional = openOfferManager.getOpenOffer(tradeId); openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer())); } sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null); diff --git a/core/src/main/java/haveno/core/support/dispute/messages/DisputeOpenedMessage.java b/core/src/main/java/haveno/core/support/dispute/messages/DisputeOpenedMessage.java index fb6fb2fc19..a11c6b1175 100644 --- a/core/src/main/java/haveno/core/support/dispute/messages/DisputeOpenedMessage.java +++ b/core/src/main/java/haveno/core/support/dispute/messages/DisputeOpenedMessage.java @@ -34,7 +34,7 @@ import java.util.Optional; public final class DisputeOpenedMessage extends DisputeMessage { private final Dispute dispute; private final NodeAddress senderNodeAddress; - private final String updatedMultisigHex; + private final String openerUpdatedMultisigHex; private final PaymentSentMessage paymentSentMessage; public DisputeOpenedMessage(Dispute dispute, @@ -67,7 +67,7 @@ public final class DisputeOpenedMessage extends DisputeMessage { super(messageVersion, uid, supportType); this.dispute = dispute; this.senderNodeAddress = senderNodeAddress; - this.updatedMultisigHex = updatedMultisigHex; + this.openerUpdatedMultisigHex = updatedMultisigHex; this.paymentSentMessage = paymentSentMessage; } @@ -78,7 +78,7 @@ public final class DisputeOpenedMessage extends DisputeMessage { .setDispute(dispute.toProtoMessage()) .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) .setType(SupportType.toProtoMessage(supportType)) - .setUpdatedMultisigHex(updatedMultisigHex); + .setOpenerUpdatedMultisigHex(openerUpdatedMultisigHex); Optional.ofNullable(paymentSentMessage).ifPresent(e -> builder.setPaymentSentMessage(paymentSentMessage.toProtoNetworkEnvelope().getPaymentSentMessage())); return getNetworkEnvelopeBuilder().setDisputeOpenedMessage(builder).build(); } @@ -91,7 +91,7 @@ public final class DisputeOpenedMessage extends DisputeMessage { proto.getUid(), messageVersion, SupportType.fromProto(proto.getType()), - ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()), + ProtoUtil.stringOrNullFromProto(proto.getOpenerUpdatedMultisigHex()), proto.hasPaymentSentMessage() ? PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), messageVersion) : null); } @@ -108,7 +108,7 @@ public final class DisputeOpenedMessage extends DisputeMessage { ",\n DisputeOpenedMessage.uid='" + uid + '\'' + ",\n messageVersion=" + messageVersion + ",\n supportType=" + supportType + - ",\n updatedMultisigHex=" + updatedMultisigHex + + ",\n openerUpdatedMultisigHex=" + openerUpdatedMultisigHex + ",\n paymentSentMessage=" + paymentSentMessage + "\n} " + super.toString(); } diff --git a/core/src/main/java/haveno/core/support/dispute/refund/RefundManager.java b/core/src/main/java/haveno/core/support/dispute/refund/RefundManager.java index fa3503f625..034eac6d5a 100644 --- a/core/src/main/java/haveno/core/support/dispute/refund/RefundManager.java +++ b/core/src/main/java/haveno/core/support/dispute/refund/RefundManager.java @@ -196,7 +196,7 @@ public final class RefundManager extends DisputeManager { tradeManager.requestPersistence(); } } else { - Optional openOfferOptional = openOfferManager.getOpenOfferById(tradeId); + Optional openOfferOptional = openOfferManager.getOpenOffer(tradeId); openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer())); } sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null); @@ -205,7 +205,7 @@ public final class RefundManager extends DisputeManager { if (tradeManager.getOpenTrade(tradeId).isPresent()) { tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.REFUND_REQUEST_CLOSED); } else { - Optional openOfferOptional = openOfferManager.getOpenOfferById(tradeId); + Optional openOfferOptional = openOfferManager.getOpenOffer(tradeId); openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer())); } diff --git a/core/src/main/java/haveno/core/trade/ClosedTradableManager.java b/core/src/main/java/haveno/core/trade/ClosedTradableManager.java index 0a48fc5188..cac8e9e261 100644 --- a/core/src/main/java/haveno/core/trade/ClosedTradableManager.java +++ b/core/src/main/java/haveno/core/trade/ClosedTradableManager.java @@ -150,9 +150,9 @@ public class ClosedTradableManager implements PersistedDataHost { } } - public Optional getTradeById(String id) { + public Optional getTradeById(String id) { synchronized (closedTradables) { - return closedTradables.stream().filter(e -> e instanceof Trade && e.getId().equals(id)).findFirst(); + return getClosedTrades().stream().filter(e -> e.getId().equals(id)).findFirst(); } } diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 6ad8e7aef3..114edcabe5 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -143,6 +143,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { private static final long DELETE_AFTER_NUM_BLOCKS = 2; // if deposit requested but not published private static final long EXTENDED_RPC_TIMEOUT = 600000; // 10 minutes private static final long DELETE_AFTER_MS = TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS; + private static final int NUM_CONFIRMATIONS_FOR_SCHEDULED_IMPORT = 10; protected final Object pollLock = new Object(); protected static final Object importMultisigLock = new Object(); private boolean pollInProgress; @@ -194,7 +195,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { SELLER_SENT_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED), SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED), SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED), - SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED); + SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED), + BUYER_RECEIVED_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED); @NotNull public Phase getPhase() { @@ -602,12 +604,12 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } - // notified from TradeProtocol of ack messages - public void onAckMessage(AckMessage ackMessage, NodeAddress sender) { + // notified from TradeProtocol of ack messages + public void onAckMessage(AckMessage ackMessage, NodeAddress sender) { for (TradeListener listener : new ArrayList(tradeListeners)) { // copy array to allow listener invocation to unregister listener without concurrent modification exception listener.onAckMessage(ackMessage, sender); } - } + } /////////////////////////////////////////////////////////////////////////////////////////// @@ -617,8 +619,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { public void initialize(ProcessModelServiceProvider serviceProvider) { if (isInitialized) throw new IllegalStateException(getClass().getSimpleName() + " " + getId() + " is already initialized"); - // done if payout unlocked and marked complete - if (isPayoutUnlocked() && isCompleted()) { + // skip initialization if trade is complete + // starting in v1.0.19, seller resends payment received message until acked or stored in mailbox + if (isFinished()) { clearAndShutDown(); return; } @@ -633,16 +636,20 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { ThreadUtils.execute(() -> onConnectionChanged(connection), getId()); }); - // reset buyer's payment sent state if no ack receive - if (this instanceof BuyerTrade && getState().ordinal() >= Trade.State.BUYER_CONFIRMED_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 states if no ack receive + if (!isPayoutPublished()) { - // reset seller's payment received state if no ack receive - if (this instanceof SellerTrade && getState().ordinal() >= Trade.State.SELLER_CONFIRMED_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.BUYER_SENT_PAYMENT_SENT_MSG); - setState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); + // reset buyer's payment sent state if no ack receive + if (this instanceof BuyerTrade && getState().ordinal() >= Trade.State.BUYER_CONFIRMED_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 seller's payment received state if no ack receive + if (this instanceof SellerTrade && getState().ordinal() >= Trade.State.SELLER_CONFIRMED_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.BUYER_SENT_PAYMENT_SENT_MSG); + resetToPaymentSentState(); + } } // handle trade state events @@ -732,15 +739,20 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { xmrWalletService.addWalletListener(idlePayoutSyncer); } - // TODO: buyer's payment sent message state property became unsynced if shut down while awaiting ack from seller. fixed in v1.0.19 so this check can be removed? + // TODO: buyer's payment sent message state property became unsynced if shut down while awaiting ack from seller. fixed mismatch in v1.0.19, but can this check can be removed? if (isBuyer()) { MessageState expectedState = getPaymentSentMessageState(); - if (expectedState != null && expectedState != processModel.getPaymentSentMessageStateProperty().get()) { - log.warn("Updating unexpected payment sent message state for {} {}, expected={}, actual={}", getClass().getSimpleName(), getId(), expectedState, processModel.getPaymentSentMessageStateProperty().get()); - processModel.getPaymentSentMessageStateProperty().set(expectedState); + if (expectedState != null && expectedState != getSeller().getPaymentSentMessageStateProperty().get()) { + log.warn("Updating unexpected payment sent message state for {} {}, expected={}, actual={}", getClass().getSimpleName(), getId(), expectedState, processModel.getPaymentSentMessageStatePropertySeller().get()); + getSeller().getPaymentSentMessageStateProperty().set(expectedState); } } + // handle confirmations + walletHeight.addListener((observable, oldValue, newValue) -> { + importMultisigHexIfScheduled(); + }); + // trade is initialized isInitialized = true; @@ -765,11 +777,30 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } - // start polling if deposit requested - if (isDepositRequested()) tryInitPolling(); + // init syncing if deposit requested + if (isDepositRequested()) { + tryInitSyncing(); + } isFullyInitialized = true; } + public boolean isFinished() { + return isPayoutUnlocked() && isCompleted() && !getProtocol().needsToResendPaymentReceivedMessages(); + } + + public void resetToPaymentSentState() { + setState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); + for (TradePeer peer : getAllPeers()) peer.setPaymentReceivedMessage(null); + setPayoutTxHex(null); + } + + public void reprocessApplicableMessages() { + if (!isDepositRequested() || isPayoutUnlocked() || isCompleted()) return; + getProtocol().maybeReprocessPaymentSentMessage(false); + getProtocol().maybeReprocessPaymentReceivedMessage(false); + HavenoUtils.arbitrationManager.maybeReprocessDisputeClosedMessage(this, false); + } + public void awaitInitialized() { while (!isFullyInitialized) HavenoUtils.waitFor(100); // TODO: use proper notification and refactor isInitialized, fullyInitialized, and arbitrator idling } @@ -1077,6 +1108,26 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } + public void scheduleImportMultisigHex() { + processModel.setImportMultisigHexScheduled(true); + requestPersistence(); + } + + private void importMultisigHexIfScheduled() { + if (!isInitialized || isShutDownStarted) return; + if (!isDepositsConfirmed() || getMaker().getDepositTx() == null) return; + if (walletHeight.get() - getMaker().getDepositTx().getHeight() < NUM_CONFIRMATIONS_FOR_SCHEDULED_IMPORT) return; + ThreadUtils.execute(() -> { + if (!isInitialized || isShutDownStarted) return; + synchronized (getLock()) { + if (processModel.isImportMultisigHexScheduled()) { + processModel.setImportMultisigHexScheduled(false); + ThreadUtils.submitToPool(() -> importMultisigHex()); + } + } + }, getId()); + } + public void importMultisigHex() { synchronized (walletLock) { synchronized (HavenoUtils.getDaemonLock()) { // lock on daemon because import calls full refresh @@ -1089,10 +1140,10 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } catch (IllegalArgumentException | IllegalStateException e) { throw e; } catch (Exception e) { + log.warn("Failed to import multisig hex, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); handleWalletError(e, sourceConnection); doPollWallet(); if (isPayoutPublished()) break; - log.warn("Failed to import multisig hex, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } @@ -1141,6 +1192,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (removed) wallet.importMultisigHex(multisigHexes.toArray(new String[0])); if (wallet.isMultisigImportNeeded()) throw new IllegalStateException(errorMessage); } + + // remove scheduled import + processModel.setImportMultisigHexScheduled(false); } catch (MoneroError e) { // import multisig hex individually if one is invalid @@ -1335,7 +1389,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { MoneroTxSet describedTxSet = wallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex)); if (describedTxSet.getTxs() == null || describedTxSet.getTxs().size() != 1) throw new IllegalArgumentException("Bad payout tx"); // TODO (woodser): test nack MoneroTxWallet payoutTx = describedTxSet.getTxs().get(0); - if (payoutTxId == null) updatePayout(payoutTx); // update payout tx if not signed + if (payoutTxId == null) updatePayout(payoutTx); // update payout tx if id currently unknown // verify payout tx has exactly 2 destinations if (payoutTx.getOutgoingTransfer() == null || payoutTx.getOutgoingTransfer().getDestinations() == null || payoutTx.getOutgoingTransfer().getDestinations().size() != 2) throw new IllegalArgumentException("Payout tx does not have exactly two destinations"); @@ -1366,6 +1420,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { BigInteger expectedSellerPayout = sellerDepositAmount.subtract(tradeAmount).subtract(txCostSplit); if (!sellerPayoutDestination.getAmount().equals(expectedSellerPayout)) throw new IllegalArgumentException("Seller destination amount is not deposit amount - trade amount - 1/2 tx costs, " + sellerPayoutDestination.getAmount() + " vs " + expectedSellerPayout); + // update payout tx + updatePayout(payoutTx); + // check connection boolean doSign = sign && getPayoutTxHex() == null; if (doSign || publish) verifyDaemonConnection(); @@ -1374,28 +1431,36 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (doSign) { // sign tx + String signedPayoutTxHex; try { MoneroMultisigSignResult result = wallet.signMultisigTxHex(payoutTxHex); if (result.getSignedMultisigTxHex() == null) throw new IllegalArgumentException("Error signing payout tx, signed multisig hex is null"); - setPayoutTxHex(result.getSignedMultisigTxHex()); + signedPayoutTxHex = result.getSignedMultisigTxHex(); } catch (Exception e) { throw new IllegalStateException(e); } + // verify miner fee is within tolerance unless outdated offer version + if (getOffer().getOfferPayload().getProtocolVersion() >= 2) { + + // 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? + log.info("Creating fee estimate tx for {} {}", getClass().getSimpleName(), getId()); + saveWallet(); // save wallet before creating fee estimate tx + 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); + } + + // set signed payout tx hex + setPayoutTxHex(signedPayoutTxHex); + // describe result describedTxSet = wallet.describeMultisigTxSet(getPayoutTxHex()); payoutTx = describedTxSet.getTxs().get(0); updatePayout(payoutTx); - - // 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? - log.info("Creating fee estimate tx for {} {}", getClass().getSimpleName(), getId()); - saveWallet(); // save wallet before creating fee estimate tx - 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); } // save trade state @@ -1506,7 +1571,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { peer.setUpdatedMultisigHex(null); peer.setDisputeClosedMessage(null); peer.setPaymentSentMessage(null); - peer.setPaymentReceivedMessage(null); + if (peer.isPaymentReceivedMessageReceived()) peer.setPaymentReceivedMessage(null); } } @@ -1604,7 +1669,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } // unreserve maker's open offer - Optional openOffer = processModel.getOpenOfferManager().getOpenOfferById(this.getId()); + Optional openOffer = processModel.getOpenOfferManager().getOpenOffer(this.getId()); if (this instanceof MakerTrade && openOffer.isPresent()) { processModel.getOpenOfferManager().unreserveOpenOffer(openOffer.get()); } @@ -1618,15 +1683,16 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // done if wallet already deleted if (!walletExists()) return; - // move to failed trades - processModel.getTradeManager().onMoveInvalidTradeToFailedTrades(this); - // set error height if (processModel.getTradeProtocolErrorHeight() == 0) { log.warn("Scheduling to remove trade if unfunded for {} {} from height {}", getClass().getSimpleName(), getId(), xmrConnectionService.getLastInfo().getHeight()); - processModel.setTradeProtocolErrorHeight(xmrConnectionService.getLastInfo().getHeight()); + processModel.setTradeProtocolErrorHeight(xmrConnectionService.getLastInfo().getHeight()); // height denotes scheduled error handling } + // move to failed trades + processModel.getTradeManager().onMoveInvalidTradeToFailedTrades(this); + requestPersistence(); + // listen for deposits published to restore trade protocolErrorStateSubscription = EasyBind.subscribe(stateProperty(), state -> { if (isDepositsPublished()) { @@ -1680,10 +1746,14 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { }); } + public boolean isProtocolErrorHandlingScheduled() { + return processModel.getTradeProtocolErrorHeight() > 0; + } + private void restoreDepositsPublishedTrade() { // close open offer - if (this instanceof MakerTrade && processModel.getOpenOfferManager().getOpenOfferById(getId()).isPresent()) { + if (this instanceof MakerTrade && processModel.getOpenOfferManager().getOpenOffer(getId()).isPresent()) { log.info("Closing open offer because {} {} was restored after protocol error", getClass().getSimpleName(), getShortId()); processModel.getOpenOfferManager().closeOpenOffer(checkNotNull(getOffer())); } @@ -1868,10 +1938,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { getSeller().setPayoutTxFee(splitTxFee); getBuyer().setPayoutAmount(getBuyer().getSecurityDeposit().subtract(getBuyer().getPayoutTxFee()).add(getAmount())); getSeller().setPayoutAmount(getSeller().getSecurityDeposit().subtract(getSeller().getPayoutTxFee())); - } else if (getDisputeState().isClosed()) { + } else { DisputeResult disputeResult = getDisputeResult(); - if (disputeResult == null) log.warn("Dispute result is not set for {} {}", getClass().getSimpleName(), getId()); - else { + if (disputeResult != null) { BigInteger[] buyerSellerPayoutTxFees = ArbitrationManager.getBuyerSellerPayoutTxCost(disputeResult, payoutTx.getFee()); getBuyer().setPayoutTxFee(buyerSellerPayoutTxFees[0]); getSeller().setPayoutTxFee(buyerSellerPayoutTxFees[1]); @@ -2015,9 +2084,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { throw new IllegalArgumentException("Trade is not buyer, seller, or arbitrator"); } - public MessageState getPaymentSentMessageState() { + private MessageState getPaymentSentMessageState() { if (isPaymentReceived()) return MessageState.ACKNOWLEDGED; - if (processModel.getPaymentSentMessageStateProperty().get() == MessageState.ACKNOWLEDGED) return MessageState.ACKNOWLEDGED; + if (getSeller().getPaymentSentMessageStateProperty().get() == MessageState.ACKNOWLEDGED) return MessageState.ACKNOWLEDGED; switch (state) { case BUYER_SENT_PAYMENT_SENT_MSG: return MessageState.SENT; @@ -2156,7 +2225,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } public boolean isPaymentSent() { - return getState().getPhase().ordinal() >= Phase.PAYMENT_SENT.ordinal(); + return getState().getPhase().ordinal() >= Phase.PAYMENT_SENT.ordinal() && getState() != State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG; } public boolean hasPaymentReceivedMessage() { @@ -2174,7 +2243,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } public boolean isPaymentReceived() { - return getState().getPhase().ordinal() >= Phase.PAYMENT_RECEIVED.ordinal(); + return getState().getPhase().ordinal() >= Phase.PAYMENT_RECEIVED.ordinal() && getState() != State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG; } public boolean isPayoutPublished() { @@ -2345,7 +2414,12 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { return tradeAmountTransferred(); } - public boolean tradeAmountTransferred() { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private boolean tradeAmountTransferred() { return isPaymentReceived() || (getDisputeResult() != null && getDisputeResult().getWinner() == DisputeResult.Winner.SELLER); } @@ -2361,11 +2435,6 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Private - /////////////////////////////////////////////////////////////////////////////////////////// - // lazy initialization private ObjectProperty getAmountProperty() { if (tradeAmountProperty == null) @@ -2410,11 +2479,12 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // sync and reprocess messages on new thread if (isInitialized && connection != null && !Boolean.FALSE.equals(xmrConnectionService.isConnected())) { - ThreadUtils.execute(() -> tryInitPolling(), getId()); + ThreadUtils.execute(() -> tryInitSyncing(), getId()); } } } - private void tryInitPolling() { + + private void tryInitSyncing() { if (isShutDownStarted) return; // set known deposit txs @@ -2423,23 +2493,18 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // start polling if (!isIdling()) { - tryInitPollingAux(); + doTryInitSyncing(); } else { long startSyncingInMs = ThreadLocalRandom.current().nextLong(0, getPollPeriod()); // random time to start polling UserThread.runAfter(() -> ThreadUtils.execute(() -> { - if (!isShutDownStarted) tryInitPollingAux(); + if (!isShutDownStarted) doTryInitSyncing(); }, getId()), startSyncingInMs / 1000l); } } - private void tryInitPollingAux() { + private void doTryInitSyncing() { if (!wasWalletSynced) trySyncWallet(true); updatePollPeriod(); - - // reprocess pending payout messages - this.getProtocol().maybeReprocessPaymentReceivedMessage(false); - HavenoUtils.arbitrationManager.maybeReprocessDisputeClosedMessage(this, false); - startPolling(); } @@ -2790,7 +2855,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (!isShutDownStarted) wallet = getWallet(); restartInProgress = false; pollWallet(); - if (!isShutDownStarted) ThreadUtils.execute(() -> tryInitPolling(), getId()); + if (!isShutDownStarted) ThreadUtils.execute(() -> tryInitSyncing(), getId()); } private void setStateDepositsSeen() { diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index a3cca84912..c98978ae4c 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -450,8 +450,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi return; } - // skip if marked as failed - if (failedTradesManager.getObservableList().contains(trade)) { + // skip if failed and error handling not scheduled + if (failedTradesManager.getObservableList().contains(trade) && !trade.isProtocolErrorHandlingScheduled()) { log.warn("Skipping initialization of failed trade {} {}", trade.getClass().getSimpleName(), trade.getId()); tradesToSkip.add(trade); return; @@ -460,8 +460,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi // initialize trade initPersistedTrade(trade); - // remove trade if protocol didn't initialize - if (getOpenTradeByUid(trade.getUid()).isPresent() && !trade.isDepositsPublished()) { + // record if protocol didn't initialize + if (!trade.isDepositsPublished()) { uninitializedTrades.add(trade); } } catch (Exception e) { @@ -556,7 +556,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi if (request.getMakerNodeAddress().equals(p2PService.getNetworkNode().getNodeAddress())) { // get open offer - Optional openOfferOptional = openOfferManager.getOpenOfferById(request.getOfferId()); + Optional openOfferOptional = openOfferManager.getOpenOffer(request.getOfferId()); if (!openOfferOptional.isPresent()) return; OpenOffer openOffer = openOfferOptional.get(); if (openOffer.getState() != OpenOffer.State.AVAILABLE) return; @@ -747,7 +747,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } private void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) { - log.info("TradeManager handling InitMultisigRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); + log.info("TradeManager handling InitMultisigRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); try { Validator.nonEmptyStringOf(request.getOfferId()); @@ -766,7 +766,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } private void handleSignContractRequest(SignContractRequest request, NodeAddress sender) { - log.info("TradeManager handling SignContractRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); + log.info("TradeManager handling SignContractRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); try { Validator.nonEmptyStringOf(request.getOfferId()); @@ -923,8 +923,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi requestPersistence(); }, errorMessage -> { log.warn("Taker error during trade initialization: " + errorMessage); - xmrWalletService.resetAddressEntriesForOpenOffer(trade.getId()); // TODO: move to maybe remove on error trade.onProtocolError(); + xmrWalletService.resetAddressEntriesForOpenOffer(trade.getId()); // TODO: move this into protocol error handling errorMessageHandler.handleErrorMessage(errorMessage); }); @@ -1285,6 +1285,12 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } } + public boolean hasFailedScheduledTrade(String offerId) { + synchronized (failedTradesManager) { + return failedTradesManager.getTradeById(offerId).isPresent() && failedTradesManager.getTradeById(offerId).get().isProtocolErrorHandlingScheduled(); + } + } + public Optional getOpenTradeByUid(String tradeUid) { synchronized (tradableList) { return tradableList.stream().filter(e -> e.getUid().equals(tradeUid)).findFirst(); @@ -1315,7 +1321,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } public Optional getClosedTrade(String tradeId) { - return closedTradableManager.getClosedTrades().stream().filter(e -> e.getId().equals(tradeId)).findFirst(); + return closedTradableManager.getTradeById(tradeId); } public Optional getFailedTrade(String tradeId) { diff --git a/core/src/main/java/haveno/core/trade/protocol/ArbitratorProtocol.java b/core/src/main/java/haveno/core/trade/protocol/ArbitratorProtocol.java index 98b3f1ab0d..b25c6c68d1 100644 --- a/core/src/main/java/haveno/core/trade/protocol/ArbitratorProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/ArbitratorProtocol.java @@ -43,7 +43,7 @@ public class ArbitratorProtocol extends DisputeProtocol { /////////////////////////////////////////////////////////////////////////////////////////// public void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer, ErrorMessageHandler errorMessageHandler) { - System.out.println("ArbitratorProtocol.handleInitTradeRequest()"); + log.info(TradeProtocol.LOG_HIGHLIGHT + "handleInitTradeRequest() for {} {}", trade.getClass().getSimpleName(), trade.getShortId()); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); @@ -78,7 +78,7 @@ public class ArbitratorProtocol extends DisputeProtocol { } public void handleDepositRequest(DepositRequest request, NodeAddress sender) { - System.out.println("ArbitratorProtocol.handleDepositRequest() " + trade.getId()); + log.info(TradeProtocol.LOG_HIGHLIGHT + "handleDepositRequest() for {} {}", trade.getClass().getSimpleName(), trade.getShortId()); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); diff --git a/core/src/main/java/haveno/core/trade/protocol/BuyerAsMakerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/BuyerAsMakerProtocol.java index 160e1bee6c..9dc1b64405 100644 --- a/core/src/main/java/haveno/core/trade/protocol/BuyerAsMakerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/BuyerAsMakerProtocol.java @@ -60,8 +60,8 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol public void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer, ErrorMessageHandler errorMessageHandler) { - System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()"); - ThreadUtils.execute(() -> { + log.info(TradeProtocol.LOG_HIGHLIGHT + "handleInitTradeRequest() for {} {} from {}", trade.getClass().getSimpleName(), trade.getShortId(), peer); + ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); this.errorMessageHandler = errorMessageHandler; diff --git a/core/src/main/java/haveno/core/trade/protocol/BuyerAsTakerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/BuyerAsTakerProtocol.java index 7a5a899e87..927997e611 100644 --- a/core/src/main/java/haveno/core/trade/protocol/BuyerAsTakerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/BuyerAsTakerProtocol.java @@ -68,7 +68,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol @Override public void onTakeOffer(TradeResultHandler tradeResultHandler, ErrorMessageHandler errorMessageHandler) { - System.out.println(getClass().getSimpleName() + ".onTakeOffer()"); + log.info(TradeProtocol.LOG_HIGHLIGHT + "onTakerOffer for {} {}", getClass().getSimpleName(), trade.getShortId()); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); @@ -99,7 +99,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol @Override public void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer) { - System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()"); + log.info(TradeProtocol.LOG_HIGHLIGHT + "handleInitTradeRequest() for {} {} from {}", trade.getClass().getSimpleName(), trade.getShortId(), peer); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); diff --git a/core/src/main/java/haveno/core/trade/protocol/BuyerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/BuyerProtocol.java index 06e1eead7c..4302f6db6f 100644 --- a/core/src/main/java/haveno/core/trade/protocol/BuyerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/BuyerProtocol.java @@ -119,7 +119,7 @@ public class BuyerProtocol extends DisputeProtocol { /////////////////////////////////////////////////////////////////////////////////////////// public void onPaymentSent(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - System.out.println("BuyerProtocol.onPaymentSent()"); + log.info(TradeProtocol.LOG_HIGHLIGHT + "BuyerProtocol.onPaymentSent() for {} {}", trade.getClass().getSimpleName(), trade.getShortId()); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); diff --git a/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java b/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java index 54aaa65d37..ae6a27e7f0 100644 --- a/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java +++ b/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java @@ -44,6 +44,7 @@ import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.filter.FilterManager; import haveno.core.network.MessageState; import haveno.core.offer.Offer; +import haveno.core.offer.OfferDirection; import haveno.core.offer.OpenOfferManager; import haveno.core.payment.PaymentAccount; import haveno.core.payment.payload.PaymentAccountPayload; @@ -73,6 +74,9 @@ import org.bitcoinj.core.Coin; import org.bitcoinj.core.Transaction; import javax.annotation.Nullable; + +import java.util.Arrays; +import java.util.List; import java.util.Optional; // Fields marked as transient are only used during protocol execution which are based on directMessages so we do not @@ -90,6 +94,7 @@ public class ProcessModel implements Model, PersistablePayload { transient private ProcessModelServiceProvider provider; transient private TradeManager tradeManager; transient private Offer offer; + transient public Throwable error; // Added in v1.4.0 // MessageState of the last message sent from the seller to the buyer in the take offer process. @@ -158,15 +163,14 @@ public class ProcessModel implements Model, PersistablePayload { @Getter @Setter private long tradeProtocolErrorHeight; - - // We want to indicate the user the state of the message delivery of the - // PaymentSentMessage. As well we do an automatic re-send in case it was not ACKed yet. - // To enable that even after restart we persist the state. + @Getter @Setter - private ObjectProperty paymentSentMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); - @Setter - private ObjectProperty paymentSentMessageStatePropertyArbitrator = new SimpleObjectProperty<>(MessageState.UNDEFINED); + private boolean importMultisigHexScheduled; private ObjectProperty paymentAccountDecryptedProperty = new SimpleObjectProperty<>(false); + @Deprecated + private ObjectProperty paymentSentMessageStatePropertySeller = new SimpleObjectProperty<>(MessageState.UNDEFINED); + @Deprecated + private ObjectProperty paymentSentMessageStatePropertyArbitrator = new SimpleObjectProperty<>(MessageState.UNDEFINED); public ProcessModel(String offerId, String accountId, PubKeyRing pubKeyRing) { this(offerId, accountId, pubKeyRing, new TradePeer(), new TradePeer(), new TradePeer()); @@ -188,6 +192,31 @@ public class ProcessModel implements Model, PersistablePayload { this.offer = offer; this.provider = provider; this.tradeManager = tradeManager; + for (TradePeer peer : getTradePeers()) { + peer.applyTransient(tradeManager); + } + + // migrate deprecated fields to new model for v1.0.19 + if (paymentSentMessageStatePropertySeller.get() != MessageState.UNDEFINED && getSeller().getPaymentSentMessageStateProperty().get() == MessageState.UNDEFINED) { + getSeller().getPaymentSentMessageStateProperty().set(paymentSentMessageStatePropertySeller.get()); + tradeManager.requestPersistence(); + } + if (paymentSentMessageStatePropertyArbitrator.get() != MessageState.UNDEFINED && getArbitrator().getPaymentSentMessageStateProperty().get() == MessageState.UNDEFINED) { + getArbitrator().getPaymentSentMessageStateProperty().set(paymentSentMessageStatePropertyArbitrator.get()); + tradeManager.requestPersistence(); + } + } + + private List getTradePeers() { + return Arrays.asList(maker, taker, arbitrator); + } + + private TradePeer getBuyer() { + return offer.getDirection() == OfferDirection.BUY ? maker : taker; + } + + private TradePeer getSeller() { + return offer.getDirection() == OfferDirection.BUY ? taker : maker; } @@ -203,11 +232,12 @@ public class ProcessModel implements Model, PersistablePayload { .setPubKeyRing(pubKeyRing.toProtoMessage()) .setUseSavingsWallet(useSavingsWallet) .setFundsNeededForTrade(fundsNeededForTrade) - .setPaymentSentMessageState(paymentSentMessageStateProperty.get().name()) + .setPaymentSentMessageStateSeller(paymentSentMessageStatePropertySeller.get().name()) .setPaymentSentMessageStateArbitrator(paymentSentMessageStatePropertyArbitrator.get().name()) .setBuyerPayoutAmountFromMediation(buyerPayoutAmountFromMediation) .setSellerPayoutAmountFromMediation(sellerPayoutAmountFromMediation) - .setTradeProtocolErrorHeight(tradeProtocolErrorHeight); + .setTradeProtocolErrorHeight(tradeProtocolErrorHeight) + .setImportMultisigHexScheduled(importMultisigHexScheduled); Optional.ofNullable(maker).ifPresent(e -> builder.setMaker((protobuf.TradePeer) maker.toProtoMessage())); Optional.ofNullable(taker).ifPresent(e -> builder.setTaker((protobuf.TradePeer) taker.toProtoMessage())); Optional.ofNullable(arbitrator).ifPresent(e -> builder.setArbitrator((protobuf.TradePeer) arbitrator.toProtoMessage())); @@ -231,6 +261,7 @@ public class ProcessModel implements Model, PersistablePayload { processModel.setBuyerPayoutAmountFromMediation(proto.getBuyerPayoutAmountFromMediation()); processModel.setSellerPayoutAmountFromMediation(proto.getSellerPayoutAmountFromMediation()); processModel.setTradeProtocolErrorHeight(proto.getTradeProtocolErrorHeight()); + processModel.setImportMultisigHexScheduled(proto.getImportMultisigHexScheduled()); // nullable processModel.setPayoutTxSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getPayoutTxSignature())); @@ -240,14 +271,13 @@ public class ProcessModel implements Model, PersistablePayload { processModel.setTradeFeeAddress(ProtoUtil.stringOrNullFromProto(proto.getTradeFeeAddress())); processModel.setMultisigAddress(ProtoUtil.stringOrNullFromProto(proto.getMultisigAddress())); - String paymentSentMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getPaymentSentMessageState()); - MessageState paymentSentMessageState = ProtoUtil.enumFromProto(MessageState.class, paymentSentMessageStateString); - processModel.setPaymentSentMessageState(paymentSentMessageState); - + // deprecated fields need to be read in order to migrate to new fields + String paymentSentMessageStateSellerString = ProtoUtil.stringOrNullFromProto(proto.getPaymentSentMessageStateSeller()); + MessageState paymentSentMessageStateSeller = ProtoUtil.enumFromProto(MessageState.class, paymentSentMessageStateSellerString); + processModel.paymentSentMessageStatePropertySeller.set(paymentSentMessageStateSeller); String paymentSentMessageStateArbitratorString = ProtoUtil.stringOrNullFromProto(proto.getPaymentSentMessageStateArbitrator()); MessageState paymentSentMessageStateArbitrator = ProtoUtil.enumFromProto(MessageState.class, paymentSentMessageStateArbitratorString); - processModel.setPaymentSentMessageStateArbitrator(paymentSentMessageStateArbitrator); - + processModel.paymentSentMessageStatePropertyArbitrator.set(paymentSentMessageStateArbitrator); return processModel; } @@ -274,32 +304,8 @@ public class ProcessModel implements Model, PersistablePayload { return getP2PService().getAddress(); } - void setPaymentSentAckMessage(AckMessage ackMessage) { - MessageState messageState = ackMessage.isSuccess() ? - MessageState.ACKNOWLEDGED : - MessageState.FAILED; - setPaymentSentMessageState(messageState); - } - - void setPaymentSentAckMessageArbitrator(AckMessage ackMessage) { - MessageState messageState = ackMessage.isSuccess() ? - MessageState.ACKNOWLEDGED : - MessageState.FAILED; - setPaymentSentMessageStateArbitrator(messageState); - } - - public void setPaymentSentMessageState(MessageState paymentSentMessageStateProperty) { - this.paymentSentMessageStateProperty.set(paymentSentMessageStateProperty); - if (tradeManager != null) { - tradeManager.requestPersistence(); - } - } - - public void setPaymentSentMessageStateArbitrator(MessageState paymentSentMessageStateProperty) { - this.paymentSentMessageStatePropertyArbitrator.set(paymentSentMessageStateProperty); - if (tradeManager != null) { - tradeManager.requestPersistence(); - } + public boolean isPaymentReceivedMessagesReceived() { + return getArbitrator().isPaymentReceivedMessageReceived() && getBuyer().isPaymentReceivedMessageReceived(); } void setDepositTxSentAckMessage(AckMessage ackMessage) { diff --git a/core/src/main/java/haveno/core/trade/protocol/SellerAsMakerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/SellerAsMakerProtocol.java index 15d92ff785..9219d0ad7d 100644 --- a/core/src/main/java/haveno/core/trade/protocol/SellerAsMakerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/SellerAsMakerProtocol.java @@ -65,7 +65,7 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc public void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer, ErrorMessageHandler errorMessageHandler) { - System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()"); + log.info(TradeProtocol.LOG_HIGHLIGHT + "handleInitTradeRequest() for {} {} from {}", trade.getClass().getSimpleName(), trade.getShortId(), peer); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); diff --git a/core/src/main/java/haveno/core/trade/protocol/SellerAsTakerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/SellerAsTakerProtocol.java index 2332ca2003..f4914efe60 100644 --- a/core/src/main/java/haveno/core/trade/protocol/SellerAsTakerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/SellerAsTakerProtocol.java @@ -68,7 +68,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc @Override public void onTakeOffer(TradeResultHandler tradeResultHandler, ErrorMessageHandler errorMessageHandler) { - System.out.println(getClass().getSimpleName() + ".onTakeOffer()"); + log.info(TradeProtocol.LOG_HIGHLIGHT + "onTakerOffer for {} {}", getClass().getSimpleName(), trade.getShortId()); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); @@ -99,7 +99,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc @Override public void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer) { - System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()"); + log.info(TradeProtocol.LOG_HIGHLIGHT + "handleInitTradeRequest() for {} {} from {}", trade.getClass().getSimpleName(), trade.getShortId(), peer); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); diff --git a/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java index 4de8fa0d6a..2a01c5a2ca 100644 --- a/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java @@ -53,6 +53,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class SellerProtocol extends DisputeProtocol { + enum SellerEvent implements FluentProtocol.Event { STARTUP, DEPOSIT_TXS_CONFIRMED, @@ -69,31 +70,37 @@ public class SellerProtocol extends DisputeProtocol { // re-send payment received message if payout not published ThreadUtils.execute(() -> { - if (trade.isShutDownStarted() || trade.isPayoutPublished()) return; + if (!needsToResendPaymentReceivedMessages()) return; synchronized (trade.getLock()) { - if (trade.isShutDownStarted() || trade.isPayoutPublished()) return; - if (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && !trade.isPayoutPublished()) { - latchTrade(); - given(anyPhase(Trade.Phase.PAYMENT_RECEIVED) - .with(SellerEvent.STARTUP)) - .setup(tasks( - SellerSendPaymentReceivedMessageToBuyer.class, - SellerSendPaymentReceivedMessageToArbitrator.class) - .using(new TradeTaskRunner(trade, - () -> { - unlatchTrade(); - }, - (errorMessage) -> { - log.warn("Error sending PaymentReceivedMessage on startup: " + errorMessage); - unlatchTrade(); - }))) - .executeTasks(); - awaitTradeLatch(); - } + if (!needsToResendPaymentReceivedMessages()) return; + latchTrade(); + given(anyPhase(Trade.Phase.PAYMENT_RECEIVED) + .with(SellerEvent.STARTUP)) + .setup(tasks( + SellerSendPaymentReceivedMessageToBuyer.class, + SellerSendPaymentReceivedMessageToArbitrator.class) + .using(new TradeTaskRunner(trade, + () -> { + unlatchTrade(); + }, + (errorMessage) -> { + log.warn("Error sending PaymentReceivedMessage on startup: " + errorMessage); + unlatchTrade(); + }))) + .executeTasks(); + awaitTradeLatch(); } }, trade.getId()); } + public boolean needsToResendPaymentReceivedMessages() { + return !trade.isShutDownStarted() && trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && !trade.getProcessModel().isPaymentReceivedMessagesReceived() && resendPaymentReceivedMessagesEnabled(); + } + + private boolean resendPaymentReceivedMessagesEnabled() { + return trade.getOffer().getOfferPayload().getProtocolVersion() >= 2; + } + @Override protected void onTradeMessage(TradeMessage message, NodeAddress peer) { super.onTradeMessage(message, peer); @@ -115,7 +122,7 @@ public class SellerProtocol extends DisputeProtocol { /////////////////////////////////////////////////////////////////////////////////////////// public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - log.info("SellerProtocol.onPaymentReceived()"); + log.info(TradeProtocol.LOG_HIGHLIGHT + "SellerProtocol.onPaymentReceived() for {} {}", trade.getClass().getSimpleName(), trade.getShortId()); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { latchTrade(); @@ -137,7 +144,7 @@ public class SellerProtocol extends DisputeProtocol { resultHandler.handleResult(); }, (errorMessage) -> { log.warn("Error confirming payment received, reverting state to {}, error={}", Trade.State.BUYER_SENT_PAYMENT_SENT_MSG, errorMessage); - trade.setState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); + trade.resetToPaymentSentState(); handleTaskRunnerFault(event, errorMessage); }))) .run(() -> trade.advanceState(Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT)) diff --git a/core/src/main/java/haveno/core/trade/protocol/TradePeer.java b/core/src/main/java/haveno/core/trade/protocol/TradePeer.java index eeef2d4daf..11c035a329 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradePeer.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradePeer.java @@ -24,12 +24,17 @@ import haveno.common.crypto.PubKeyRing; import haveno.common.proto.ProtoUtil; import haveno.common.proto.persistable.PersistablePayload; import haveno.core.account.witness.AccountAgeWitness; +import haveno.core.network.MessageState; import haveno.core.payment.payload.PaymentAccountPayload; import haveno.core.proto.CoreProtoResolver; import haveno.core.support.dispute.messages.DisputeClosedMessage; +import haveno.core.trade.TradeManager; import haveno.core.trade.messages.PaymentReceivedMessage; import haveno.core.trade.messages.PaymentSentMessage; +import haveno.network.p2p.AckMessage; import haveno.network.p2p.NodeAddress; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; @@ -57,6 +62,7 @@ public final class TradePeer implements PersistablePayload { @Nullable transient private byte[] preparedDepositTx; transient private MoneroTxWallet depositTx; + transient private TradeManager tradeManager; // Persistable mutable @Nullable @@ -96,7 +102,6 @@ public final class TradePeer implements PersistablePayload { @Getter private DisputeClosedMessage disputeClosedMessage; - // added in v 0.6 @Nullable private byte[] accountAgeWitnessNonce; @@ -142,13 +147,32 @@ public final class TradePeer implements PersistablePayload { private long payoutAmount; @Nullable private String updatedMultisigHex; - @Getter + @Deprecated + private boolean depositsConfirmedMessageAcked; + + // We want to indicate the user the state of the message delivery of the payment + // confirmation messages. We do an automatic re-send in case it was not ACKed yet. + // To enable that even after restart we persist the state. @Setter - boolean depositsConfirmedMessageAcked; + private ObjectProperty depositsConfirmedMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); + @Setter + private ObjectProperty paymentSentMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); + @Setter + private ObjectProperty paymentReceivedMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); public TradePeer() { } + public void applyTransient(TradeManager tradeManager) { + this.tradeManager = tradeManager; + + // migrate deprecated fields to new model for v1.0.19 + if (depositsConfirmedMessageAcked && depositsConfirmedMessageStateProperty.get() == MessageState.UNDEFINED) { + depositsConfirmedMessageStateProperty.set(MessageState.ACKNOWLEDGED); + tradeManager.requestPersistence(); + } + } + public BigInteger getDepositTxFee() { return BigInteger.valueOf(depositTxFee); } @@ -181,6 +205,60 @@ public final class TradePeer implements PersistablePayload { this.payoutAmount = payoutAmount.longValueExact(); } + void setDepositsConfirmedAckMessage(AckMessage ackMessage) { + MessageState messageState = ackMessage.isSuccess() ? + MessageState.ACKNOWLEDGED : + MessageState.FAILED; + setDepositsConfirmedMessageState(messageState); + } + + void setPaymentSentAckMessage(AckMessage ackMessage) { + MessageState messageState = ackMessage.isSuccess() ? + MessageState.ACKNOWLEDGED : + MessageState.FAILED; + setPaymentSentMessageState(messageState); + } + + void setPaymentReceivedAckMessage(AckMessage ackMessage) { + MessageState messageState = ackMessage.isSuccess() ? + MessageState.ACKNOWLEDGED : + MessageState.FAILED; + setPaymentReceivedMessageState(messageState); + } + + public void setDepositsConfirmedMessageState(MessageState depositsConfirmedMessageStateProperty) { + this.depositsConfirmedMessageStateProperty.set(depositsConfirmedMessageStateProperty); + if (tradeManager != null) { + tradeManager.requestPersistence(); + } + } + + public void setPaymentSentMessageState(MessageState paymentSentMessageStateProperty) { + this.paymentSentMessageStateProperty.set(paymentSentMessageStateProperty); + if (tradeManager != null) { + tradeManager.requestPersistence(); + } + } + + public void setPaymentReceivedMessageState(MessageState paymentReceivedMessageStateProperty) { + this.paymentReceivedMessageStateProperty.set(paymentReceivedMessageStateProperty); + if (tradeManager != null) { + tradeManager.requestPersistence(); + } + } + + public boolean isDepositsConfirmedMessageAcked() { + return depositsConfirmedMessageStateProperty.get() == MessageState.ACKNOWLEDGED; + } + + public boolean isPaymentSentMessageAcked() { + return paymentSentMessageStateProperty.get() == MessageState.ACKNOWLEDGED; + } + + public boolean isPaymentReceivedMessageReceived() { + return paymentReceivedMessageStateProperty.get() == MessageState.ACKNOWLEDGED || paymentReceivedMessageStateProperty.get() == MessageState.STORED_IN_MAILBOX; + } + @Override public Message toProtoMessage() { final protobuf.TradePeer.Builder builder = protobuf.TradePeer.newBuilder(); @@ -221,6 +299,9 @@ public final class TradePeer implements PersistablePayload { Optional.ofNullable(payoutTxFee).ifPresent(e -> builder.setPayoutTxFee(payoutTxFee)); Optional.ofNullable(payoutAmount).ifPresent(e -> builder.setPayoutAmount(payoutAmount)); builder.setDepositsConfirmedMessageAcked(depositsConfirmedMessageAcked); + builder.setDepositsConfirmedMessageState(depositsConfirmedMessageStateProperty.get().name()); + builder.setPaymentSentMessageState(paymentSentMessageStateProperty.get().name()); + builder.setPaymentReceivedMessageState(paymentReceivedMessageStateProperty.get().name()); builder.setCurrentDate(currentDate); return builder.build(); @@ -270,6 +351,19 @@ public final class TradePeer implements PersistablePayload { tradePeer.setUnsignedPayoutTxHex(ProtoUtil.stringOrNullFromProto(proto.getUnsignedPayoutTxHex())); tradePeer.setPayoutTxFee(BigInteger.valueOf(proto.getPayoutTxFee())); tradePeer.setPayoutAmount(BigInteger.valueOf(proto.getPayoutAmount())); + + String depositsConfirmedMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getDepositsConfirmedMessageState()); + MessageState depositsConfirmedMessageState = ProtoUtil.enumFromProto(MessageState.class, depositsConfirmedMessageStateString); + tradePeer.setDepositsConfirmedMessageState(depositsConfirmedMessageState); + + String paymentSentMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getPaymentSentMessageState()); + MessageState paymentSentMessageState = ProtoUtil.enumFromProto(MessageState.class, paymentSentMessageStateString); + tradePeer.setPaymentSentMessageState(paymentSentMessageState); + + String paymentReceivedMessageStateString = ProtoUtil.stringOrNullFromProto(proto.getPaymentReceivedMessageState()); + MessageState paymentReceivedMessageState = ProtoUtil.enumFromProto(MessageState.class, paymentReceivedMessageStateString); + tradePeer.setPaymentReceivedMessageState(paymentReceivedMessageState); + return tradePeer; } } 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 684dfb7c62..74a50f44f4 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -96,6 +96,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D private static final String TIMEOUT_REACHED = "Timeout reached."; public static final int MAX_ATTEMPTS = 5; // max attempts to create txs and other wallet functions public static final long REPROCESS_DELAY_MS = 5000; + public static final String LOG_HIGHLIGHT = "\u001B[36m"; // cyan protected final ProcessModel processModel; protected final Trade trade; @@ -106,6 +107,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D protected ErrorMessageHandler errorMessageHandler; private boolean depositsConfirmedTasksCalled; + private int reprocessPaymentSentMessageCount; private int reprocessPaymentReceivedMessageCount; /////////////////////////////////////////////////////////////////////////////////////////// @@ -124,12 +126,12 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D protected void onTradeMessage(TradeMessage message, NodeAddress peerNodeAddress) { log.info("Received {} as TradeMessage from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peerNodeAddress, message.getOfferId(), message.getUid()); - ThreadUtils.execute(() -> handle(message, peerNodeAddress), trade.getId()); + handle(message, peerNodeAddress); } protected void onMailboxMessage(TradeMessage message, NodeAddress peerNodeAddress) { log.info("Received {} as MailboxMessage from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peerNodeAddress, message.getOfferId(), message.getUid()); - ThreadUtils.execute(() -> handle(message, peerNodeAddress), trade.getId()); + handle(message, peerNodeAddress); } private void handle(TradeMessage message, NodeAddress peerNodeAddress) { @@ -163,7 +165,6 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } } else if (networkEnvelope instanceof AckMessage) { onAckMessage((AckMessage) networkEnvelope, peer); - trade.onAckMessage((AckMessage) networkEnvelope, peer); // notify trade listeners } } @@ -208,11 +209,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D onMailboxMessage(tradeMessage, mailboxMessage.getSenderNodeAddress()); } else if (mailboxMessage instanceof AckMessage) { AckMessage ackMessage = (AckMessage) mailboxMessage; - if (!trade.isCompleted()) { - // We only apply the msg if we have not already completed the trade - onAckMessage(ackMessage, mailboxMessage.getSenderNodeAddress()); - } - // In any case we remove the msg + onAckMessage(ackMessage, mailboxMessage.getSenderNodeAddress()); processModel.getP2PService().getMailboxMessageService().removeMailboxMsg(ackMessage); log.info("Remove {} from the P2P network.", ackMessage.getClass().getSimpleName()); } @@ -240,7 +237,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D protected void onInitialized() { // listen for direct messages unless completed - if (!trade.isCompleted()) processModel.getP2PService().addDecryptedDirectMessageListener(this); + if (!trade.isFinished()) processModel.getP2PService().addDecryptedDirectMessageListener(this); // initialize trade synchronized (trade.getLock()) { @@ -250,6 +247,9 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D MailboxMessageService mailboxMessageService = processModel.getP2PService().getMailboxMessageService(); if (!trade.isCompleted()) mailboxMessageService.addDecryptedMailboxListener(this); handleMailboxCollection(mailboxMessageService.getMyDecryptedMailboxMessages()); + + // reprocess applicable messages + trade.reprocessApplicableMessages(); } // send deposits confirmed message if applicable @@ -279,24 +279,46 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D }, trade.getId()); } - public void maybeReprocessPaymentReceivedMessage(boolean reprocessOnError) { + public boolean needsToResendPaymentReceivedMessages() { + return false; // seller protocol overrides + } + + public void maybeReprocessPaymentSentMessage(boolean reprocessOnError) { if (trade.isShutDownStarted()) return; ThreadUtils.execute(() -> { + if (trade.isShutDownStarted()) return; synchronized (trade.getLock()) { // skip if no need to reprocess - if (trade.isSeller() || trade.getSeller().getPaymentReceivedMessage() == null || (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && trade.isPayoutPublished())) { + if (trade.isShutDownStarted() || trade.isBuyer() || trade.getBuyer().getPaymentSentMessage() == null || trade.getState().ordinal() >= Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal()) { return; } - log.warn("Reprocessing payment received message for {} {}", trade.getClass().getSimpleName(), trade.getId()); + log.warn("Reprocessing PaymentSentMessage for {} {}", trade.getClass().getSimpleName(), trade.getId()); + handle(trade.getBuyer().getPaymentSentMessage(), trade.getBuyer().getPaymentSentMessage().getSenderNodeAddress(), reprocessOnError); + } + }, trade.getId()); + } + + public void maybeReprocessPaymentReceivedMessage(boolean reprocessOnError) { + if (trade.isShutDownStarted()) return; + ThreadUtils.execute(() -> { + if (trade.isShutDownStarted()) return; + synchronized (trade.getLock()) { + + // skip if no need to reprocess + if (trade.isShutDownStarted() || trade.isSeller() || trade.getSeller().getPaymentReceivedMessage() == null || (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && trade.isPayoutPublished())) { + return; + } + + log.warn("Reprocessing PaymentReceivedMessage for {} {}", trade.getClass().getSimpleName(), trade.getId()); handle(trade.getSeller().getPaymentReceivedMessage(), trade.getSeller().getPaymentReceivedMessage().getSenderNodeAddress(), reprocessOnError); } }, trade.getId()); } public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) { - System.out.println(getClass().getSimpleName() + ".handleInitMultisigRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); + log.info(LOG_HIGHLIGHT + "handleInitMultisigRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + sender); trade.addInitProgressStep(); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { @@ -333,7 +355,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) { - System.out.println(getClass().getSimpleName() + ".handleSignContractRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); + log.info(LOG_HIGHLIGHT + "handleSignContractRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + sender); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { @@ -376,7 +398,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) { - System.out.println(getClass().getSimpleName() + ".handleSignContractResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); + log.info(LOG_HIGHLIGHT + "handleSignContractResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + sender); trade.addInitProgressStep(); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { @@ -422,7 +444,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } public void handleDepositResponse(DepositResponse response, NodeAddress sender) { - System.out.println(getClass().getSimpleName() + ".handleDepositResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); + log.info(LOG_HIGHLIGHT + "handleDepositResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + sender); trade.addInitProgressStep(); ThreadUtils.execute(() -> { synchronized (trade.getLock()) { @@ -452,7 +474,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } public void handle(DepositsConfirmedMessage message, NodeAddress sender) { - System.out.println(getClass().getSimpleName() + ".handle(DepositsConfirmedMessage) from " + sender + " for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); + log.info(LOG_HIGHLIGHT + "handle(DepositsConfirmedMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + sender); if (!trade.isInitialized() || trade.isShutDown()) return; ThreadUtils.execute(() -> { synchronized (trade.getLock()) { @@ -481,12 +503,33 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D // received by seller and arbitrator protected void handle(PaymentSentMessage message, NodeAddress peer) { - System.out.println(getClass().getSimpleName() + ".handle(PaymentSentMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); - if (!trade.isInitialized() || trade.isShutDown()) return; + handle(message, peer, true); + } + + // received by seller and arbitrator + protected void handle(PaymentSentMessage message, NodeAddress peer, boolean reprocessOnError) { + log.info(LOG_HIGHLIGHT + "handle(PaymentSentMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + peer); + + // ignore if not seller or arbitrator if (!(trade instanceof SellerTrade || trade instanceof ArbitratorTrade)) { log.warn("Ignoring PaymentSentMessage since not seller or arbitrator"); return; } + + // validate signature + try { + HavenoUtils.verifyPaymentSentMessage(trade, message); + } catch (Throwable t) { + log.warn("Ignoring PaymentSentMessage with invalid signature for {} {}, error={}", trade.getClass().getSimpleName(), trade.getId(), t.getMessage()); + return; + } + + // save message for reprocessing + trade.getBuyer().setPaymentSentMessage(message); + trade.requestPersistence(); + + // process message on trade thread + if (!trade.isInitialized() || trade.isShutDownStarted()) return; ThreadUtils.execute(() -> { // We are more tolerant with expected phase and allow also DEPOSITS_PUBLISHED as it can be the case // that the wallet is still syncing and so the DEPOSITS_CONFIRMED state to yet triggered when we received @@ -494,7 +537,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D // TODO A better fix would be to add a listener for the wallet sync state and process // the mailbox msg once wallet is ready and trade state set. synchronized (trade.getLock()) { - if (!trade.isInitialized() || trade.isShutDown()) return; + if (!trade.isInitialized() || trade.isShutDownStarted()) return; if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_SENT.ordinal()) { log.warn("Received another PaymentSentMessage which was already processed for {} {}, ACKing", trade.getClass().getSimpleName(), trade.getId()); handleTaskRunnerSuccess(peer, message); @@ -509,7 +552,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D return; } latchTrade(); - expect(anyPhase(Trade.Phase.DEPOSITS_CONFIRMED, Trade.Phase.DEPOSITS_UNLOCKED) + expect(anyPhase() .with(message) .from(peer)) .setup(tasks( @@ -521,7 +564,19 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D handleTaskRunnerSuccess(peer, message); }, (errorMessage) -> { - handleTaskRunnerFault(peer, message, errorMessage); + log.warn("Error processing payment sent message: " + errorMessage); + processModel.getTradeManager().requestPersistence(); + + // schedule to reprocess message unless deleted + if (trade.getBuyer().getPaymentSentMessage() != null) { + UserThread.runAfter(() -> { + reprocessPaymentSentMessageCount++; + maybeReprocessPaymentSentMessage(reprocessOnError); + }, trade.getReprocessDelayInSeconds(reprocessPaymentSentMessageCount)); + } else { + handleTaskRunnerFault(peer, message, errorMessage); // otherwise send nack + } + unlatchTrade(); }))) .executeTasks(true); awaitTradeLatch(); @@ -535,15 +590,31 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } private void handle(PaymentReceivedMessage message, NodeAddress peer, boolean reprocessOnError) { - System.out.println(getClass().getSimpleName() + ".handle(PaymentReceivedMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); - if (!trade.isInitialized() || trade.isShutDown()) return; + log.info(LOG_HIGHLIGHT + "handle(PaymentReceivedMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + peer); + + // ignore if not buyer or arbitrator + if (!(trade instanceof BuyerTrade || trade instanceof ArbitratorTrade)) { + log.warn("Ignoring PaymentReceivedMessage since not buyer or arbitrator"); + return; + } + + // validate signature + try { + HavenoUtils.verifyPaymentReceivedMessage(trade, message); + } catch (Throwable t) { + log.warn("Ignoring PaymentReceivedMessage with invalid signature for {} {}, error={}", trade.getClass().getSimpleName(), trade.getId(), t.getMessage()); + return; + } + + // save message for reprocessing + trade.getSeller().setPaymentReceivedMessage(message); + trade.requestPersistence(); + + // process message on trade thread + if (!trade.isInitialized() || trade.isShutDownStarted()) return; ThreadUtils.execute(() -> { - if (!(trade instanceof BuyerTrade || trade instanceof ArbitratorTrade)) { - log.warn("Ignoring PaymentReceivedMessage since not buyer or arbitrator"); - return; - } synchronized (trade.getLock()) { - if (!trade.isInitialized() || trade.isShutDown()) return; + if (!trade.isInitialized() || trade.isShutDownStarted()) return; latchTrade(); Validator.checkTradeId(processModel.getOfferId(), message); processModel.setTradeMessage(message); @@ -649,48 +720,84 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D private void onAckMessage(AckMessage ackMessage, NodeAddress sender) { - // handle ack for PaymentSentMessage, which automatically re-sends if not ACKed in a certain time - if (ackMessage.getSourceMsgClassName().equals(PaymentSentMessage.class.getSimpleName())) { - if (trade.getTradePeer(sender) == trade.getSeller()) { - processModel.setPaymentSentAckMessage(ackMessage); - trade.setStateIfValidTransitionTo(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG); - processModel.getTradeManager().requestPersistence(); - } else if (trade.getTradePeer(sender) == trade.getArbitrator()) { - processModel.setPaymentSentAckMessageArbitrator(ackMessage); - } else if (!ackMessage.isSuccess()) { - String err = "Received AckMessage with error state for " + ackMessage.getSourceMsgClassName() + " from "+ sender + " with tradeId " + trade.getId() + " and errorMessage=" + ackMessage.getErrorMessage(); - log.warn(err); - return; // log error and ignore nack if not seller - } + // ignore if trade is completely finished + if (trade.isFinished()) return; + + // get trade peer + TradePeer peer = trade.getTradePeer(sender); + if (peer == null) { + if (ackMessage.getSourceUid().equals(HavenoUtils.getDeterministicId(trade, DepositsConfirmedMessage.class, trade.getArbitrator().getNodeAddress()))) peer = trade.getArbitrator(); + else if (ackMessage.getSourceUid().equals(HavenoUtils.getDeterministicId(trade, DepositsConfirmedMessage.class, trade.getMaker().getNodeAddress()))) peer = trade.getMaker(); + else if (ackMessage.getSourceUid().equals(HavenoUtils.getDeterministicId(trade, DepositsConfirmedMessage.class, trade.getTaker().getNodeAddress()))) peer = trade.getTaker(); + } + if (peer == null) { + if (ackMessage.isSuccess()) log.warn("Received AckMessage from unknown peer for {}, sender={}, trade={} {}, messageUid={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid()); + else log.warn("Received AckMessage with error state from unknown peer for {}, sender={}, trade={} {}, messageUid={}, errorMessage={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.getErrorMessage()); + return; } - if (ackMessage.isSuccess()) { - log.info("Received AckMessage for {}, sender={}, trade={} {}, messageUid={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid()); + // update sender's node address + if (!peer.getNodeAddress().equals(sender)) { + log.info("Updating peer's node address from {} to {} using ACK message to {}", peer.getNodeAddress(), sender, ackMessage.getSourceMsgClassName()); + peer.setNodeAddress(sender); + } - // handle ack for DepositsConfirmedMessage, which automatically re-sends if not ACKed in a certain time - if (ackMessage.getSourceMsgClassName().equals(DepositsConfirmedMessage.class.getSimpleName())) { - TradePeer peer = trade.getTradePeer(sender); - if (peer == null) { - - // get the applicable peer based on the sourceUid - if (ackMessage.getSourceUid().equals(HavenoUtils.getDeterministicId(trade, DepositsConfirmedMessage.class, trade.getArbitrator().getNodeAddress()))) peer = trade.getArbitrator(); - else if (ackMessage.getSourceUid().equals(HavenoUtils.getDeterministicId(trade, DepositsConfirmedMessage.class, trade.getMaker().getNodeAddress()))) peer = trade.getMaker(); - else if (ackMessage.getSourceUid().equals(HavenoUtils.getDeterministicId(trade, DepositsConfirmedMessage.class, trade.getTaker().getNodeAddress()))) peer = trade.getTaker(); - } - if (peer == null) log.warn("Received AckMesage for DepositsConfirmedMessage for unknown peer: " + sender); - else peer.setDepositsConfirmedMessageAcked(true); - } - } else { - log.warn("Received AckMessage with error state for {}, sender={}, trade={} {}, messageUid={}, errorMessage={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.getErrorMessage()); - - // set trade state on deposit request nack - if (ackMessage.getSourceMsgClassName().equals(DepositRequest.class.getSimpleName())) { + // set trade state on deposit request nack + if (ackMessage.getSourceMsgClassName().equals(DepositRequest.class.getSimpleName())) { + if (!ackMessage.isSuccess()) { trade.setStateIfValidTransitionTo(Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED); processModel.getTradeManager().requestPersistence(); } + } + // handle ack for DepositsConfirmedMessage, which automatically re-sends if not ACKed in a certain time + if (ackMessage.getSourceMsgClassName().equals(DepositsConfirmedMessage.class.getSimpleName())) { + peer.setDepositsConfirmedAckMessage(ackMessage); + processModel.getTradeManager().requestPersistence(); + } + + // handle ack for PaymentSentMessage, which automatically re-sends if not ACKed in a certain time + if (ackMessage.getSourceMsgClassName().equals(PaymentSentMessage.class.getSimpleName())) { + if (trade.getTradePeer(sender) == trade.getSeller()) { + trade.getSeller().setPaymentSentAckMessage(ackMessage); + if (ackMessage.isSuccess()) trade.setStateIfValidTransitionTo(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG); + else trade.setState(Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG); + processModel.getTradeManager().requestPersistence(); + } else if (trade.getTradePeer(sender) == trade.getArbitrator()) { + trade.getArbitrator().setPaymentSentAckMessage(ackMessage); + processModel.getTradeManager().requestPersistence(); + } else { + log.warn("Received AckMessage from unexpected peer for {}, sender={}, trade={} {}, messageUid={}, success={}, errorMsg={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.isSuccess(), ackMessage.getErrorMessage()); + return; + } + } + + // handle ack for PaymentReceivedMessage, which automatically re-sends if not ACKed in a certain time + if (ackMessage.getSourceMsgClassName().equals(PaymentReceivedMessage.class.getSimpleName())) { + if (trade.getTradePeer(sender) == trade.getBuyer()) { + trade.getBuyer().setPaymentReceivedAckMessage(ackMessage); + if (ackMessage.isSuccess()) trade.setStateIfValidTransitionTo(Trade.State.BUYER_RECEIVED_PAYMENT_RECEIVED_MSG); + else trade.setState(Trade.State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG); + processModel.getTradeManager().requestPersistence(); + } else if (trade.getTradePeer(sender) == trade.getArbitrator()) { + trade.getArbitrator().setPaymentReceivedAckMessage(ackMessage); + processModel.getTradeManager().requestPersistence(); + } else { + log.warn("Received AckMessage from unexpected peer for {}, sender={}, trade={} {}, messageUid={}, success={}, errorMsg={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.isSuccess(), ackMessage.getErrorMessage()); + return; + } + } + + // generic handling + if (ackMessage.isSuccess()) { + log.info("Received AckMessage for {}, sender={}, trade={} {}, messageUid={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid()); + } else { + log.warn("Received AckMessage with error state for {}, sender={}, trade={} {}, messageUid={}, errorMessage={}", ackMessage.getSourceMsgClassName(), sender, trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.getErrorMessage()); handleError(ackMessage.getErrorMessage()); } + + // notify trade listeners + trade.onAckMessage(ackMessage, sender); } protected void sendAckMessage(NodeAddress peer, TradeMessage message, boolean result, @Nullable String errorMessage) { diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java index be9d528f35..3a7fc7ace9 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ArbitratorProcessDepositRequest.java @@ -44,7 +44,6 @@ import java.util.UUID; @Slf4j public class ArbitratorProcessDepositRequest extends TradeTask { - private Throwable error; private boolean depositResponsesSent; @SuppressWarnings({"unused"}) @@ -68,7 +67,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask { processDepositRequest(); complete(); } catch (Throwable t) { - this.error = t; + trade.getProcessModel().error = t; log.error("Error processing deposit request for trade {}: {}\n", trade.getId(), t.getMessage(), t); trade.setStateIfValidTransitionTo(Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED); failed(t); @@ -188,7 +187,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask { trade.stateProperty().addListener((obs, oldState, newState) -> { if (oldState == newState) return; if (newState == Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED) { - sendDepositResponsesOnce(error == null ? "Arbitrator failed to publish deposit txs within timeout for trade " + trade.getId() : error.getMessage()); + sendDepositResponsesOnce(trade.getProcessModel().error == null ? "Arbitrator failed to publish deposit txs within timeout for trade " + trade.getId() : trade.getProcessModel().error.getMessage()); } else if (newState.ordinal() >= Trade.State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS.ordinal()) { sendDepositResponsesOnce(null); } @@ -230,7 +229,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask { } private void sendDepositResponse(NodeAddress nodeAddress, PubKeyRing pubKeyRing, DepositResponse response) { - log.info("Sending deposit response to trader={}; offerId={}, error={}", nodeAddress, trade.getId(), error); + log.info("Sending deposit response to trader={}; offerId={}, error={}", nodeAddress, trade.getId(), trade.getProcessModel().error); processModel.getP2PService().sendEncryptedDirectMessage(nodeAddress, pubKeyRing, response, new SendDirectMessageListener() { @Override public void onArrived() { diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java index 42206c8de0..86bb957577 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java @@ -66,7 +66,7 @@ import lombok.extern.slf4j.Slf4j; public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask { private ChangeListener listener; private Timer timer; - private static final int MAX_RESEND_ATTEMPTS = 10; + private static final int MAX_RESEND_ATTEMPTS = 20; private int delayInMin = 10; private int resendCounter = 0; @@ -142,26 +142,26 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask @Override protected void setStateSent() { - if (trade.getState().ordinal() < Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal()) trade.setStateIfValidTransitionTo(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); + getReceiver().setPaymentSentMessageState(MessageState.SENT); tryToSendAgainLater(); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateArrived() { - trade.setStateIfValidTransitionTo(Trade.State.BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG); + getReceiver().setPaymentSentMessageState(MessageState.ARRIVED); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateStoredInMailbox() { - trade.setStateIfValidTransitionTo(Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG); + getReceiver().setPaymentSentMessageState(MessageState.STORED_IN_MAILBOX); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateFault() { - trade.setStateIfValidTransitionTo(Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG); + getReceiver().setPaymentSentMessageState(MessageState.FAILED); processModel.getTradeManager().requestPersistence(); } @@ -170,7 +170,7 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask timer.stop(); } if (listener != null) { - processModel.getPaymentSentMessageStateProperty().removeListener(listener); + trade.getSeller().getPaymentReceivedMessageStateProperty().removeListener(listener); } } @@ -185,7 +185,6 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask return; } - log.info("We will send the message again to the peer after a delay of {} min.", delayInMin); if (timer != null) { timer.stop(); } @@ -194,21 +193,30 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask if (resendCounter == 0) { listener = (observable, oldValue, newValue) -> onMessageStateChange(newValue); - processModel.getPaymentSentMessageStateProperty().addListener(listener); - onMessageStateChange(processModel.getPaymentSentMessageStateProperty().get()); + getReceiver().getPaymentSentMessageStateProperty().addListener(listener); + onMessageStateChange(getReceiver().getPaymentSentMessageStateProperty().get()); } - delayInMin = delayInMin * 2; + // first re-send is after 2 minutes, then increase the delay exponentially + if (resendCounter == 0) { + int shortDelay = 2; + log.info("We will send the message again to the peer after a delay of {} min.", shortDelay); + timer = UserThread.runAfter(this::run, shortDelay, TimeUnit.MINUTES); + } else { + log.info("We will send the message again to the peer after a delay of {} min.", delayInMin); + timer = UserThread.runAfter(this::run, delayInMin, TimeUnit.MINUTES); + delayInMin = (int) ((double) delayInMin * 1.5); + } resendCounter++; } private void onMessageStateChange(MessageState newValue) { - if (newValue == MessageState.ACKNOWLEDGED) { - trade.setStateIfValidTransitionTo(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG); - processModel.getTradeManager().requestPersistence(); + if (isAckedByReceiver()) { cleanup(); } } - protected abstract boolean isAckedByReceiver(); + protected boolean isAckedByReceiver() { + return getReceiver().isPaymentSentMessageAcked(); + } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToArbitrator.java b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToArbitrator.java index cc4113e342..9fea701200 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToArbitrator.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToArbitrator.java @@ -18,7 +18,6 @@ package haveno.core.trade.protocol.tasks; import haveno.common.taskrunner.TaskRunner; -import haveno.core.network.MessageState; import haveno.core.trade.Trade; import haveno.core.trade.protocol.TradePeer; import lombok.EqualsAndHashCode; @@ -39,26 +38,7 @@ public class BuyerSendPaymentSentMessageToArbitrator extends BuyerSendPaymentSen @Override protected void setStateSent() { + super.setStateSent(); complete(); // don't wait for message to arbitrator } - - @Override - protected void setStateFault() { - // state only updated on seller message - } - - @Override - protected void setStateStoredInMailbox() { - // state only updated on seller message - } - - @Override - protected void setStateArrived() { - // state only updated on seller message - } - - @Override - protected boolean isAckedByReceiver() { - return trade.getProcessModel().getPaymentSentMessageStatePropertyArbitrator().get() == MessageState.ACKNOWLEDGED; - } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToSeller.java b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToSeller.java index caf402be0a..57ca170455 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToSeller.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessageToSeller.java @@ -18,7 +18,6 @@ package haveno.core.trade.protocol.tasks; import haveno.common.taskrunner.TaskRunner; -import haveno.core.network.MessageState; import haveno.core.trade.Trade; import haveno.core.trade.messages.TradeMessage; import haveno.core.trade.protocol.TradePeer; @@ -40,25 +39,25 @@ public class BuyerSendPaymentSentMessageToSeller extends BuyerSendPaymentSentMes @Override protected void setStateSent() { - trade.getProcessModel().setPaymentSentMessageState(MessageState.SENT); + if (trade.getState().ordinal() < Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal()) trade.setStateIfValidTransitionTo(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); super.setStateSent(); } @Override protected void setStateArrived() { - trade.getProcessModel().setPaymentSentMessageState(MessageState.ARRIVED); + trade.setStateIfValidTransitionTo(Trade.State.BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG); super.setStateArrived(); } @Override protected void setStateStoredInMailbox() { - trade.getProcessModel().setPaymentSentMessageState(MessageState.STORED_IN_MAILBOX); + trade.setStateIfValidTransitionTo(Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG); super.setStateStoredInMailbox(); } @Override protected void setStateFault() { - trade.getProcessModel().setPaymentSentMessageState(MessageState.FAILED); + trade.setStateIfValidTransitionTo(Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG); super.setStateFault(); } @@ -69,9 +68,4 @@ public class BuyerSendPaymentSentMessageToSeller extends BuyerSendPaymentSentMes appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage); complete(); } - - @Override - protected boolean isAckedByReceiver() { - return trade.getState().ordinal() >= Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG.ordinal(); - } } 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 e1c4cce5cc..1d2170cb53 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 @@ -87,7 +87,7 @@ public class MaybeSendSignContractRequest extends TradeTask { Integer subaddressIndex = null; boolean reserveExactAmount = false; if (trade instanceof MakerTrade) { - reserveExactAmount = processModel.getOpenOfferManager().getOpenOfferById(trade.getId()).get().isReserveExactAmount(); + reserveExactAmount = processModel.getOpenOfferManager().getOpenOffer(trade.getId()).get().isReserveExactAmount(); if (reserveExactAmount) subaddressIndex = model.getXmrWalletService().getAddressEntry(trade.getId(), XmrAddressEntry.Context.OFFER_FUNDING).get().getSubaddressIndex(); } 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 c11df74fae..7e0c85af2d 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java @@ -18,7 +18,6 @@ package haveno.core.trade.protocol.tasks; -import haveno.common.ThreadUtils; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; import haveno.core.trade.messages.DepositsConfirmedMessage; @@ -63,17 +62,7 @@ public class ProcessDepositsConfirmedMessage extends TradeTask { // update multisig hex if (sender.getUpdatedMultisigHex() == null) { sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex()); - - // try to import multisig hex (retry later) - if (!trade.isPayoutPublished()) { - ThreadUtils.submitToPool(() -> { - try { - trade.importMultisigHex(); - } catch (Exception e) { - log.warn("Error importing multisig hex on deposits confirmed for trade " + trade.getId() + ": " + e.getMessage() + "\n", e); - } - }); - } + trade.scheduleImportMultisigHex(); } // persist 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 2ce29828a4..5d55bbaea7 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 @@ -72,6 +72,7 @@ public class ProcessPaymentReceivedMessage extends TradeTask { // update to the latest peer address of our peer if message is correct trade.getSeller().setNodeAddress(processModel.getTempTradePeerNodeAddress()); if (trade.getSeller().getNodeAddress().equals(trade.getBuyer().getNodeAddress())) trade.getBuyer().setNodeAddress(null); // tests can reuse addresses + trade.requestPersistence(); // ack and complete if already processed if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_RECEIVED.ordinal() && trade.isPayoutPublished()) { @@ -80,8 +81,13 @@ public class ProcessPaymentReceivedMessage extends TradeTask { return; } - // save message for reprocessing - trade.getSeller().setPaymentReceivedMessage(message); + // cannot process until wallet sees deposits unlocked + if (!trade.isDepositsUnlocked()) { + trade.syncAndPollWallet(); + if (!trade.isDepositsUnlocked()) { + throw new RuntimeException("Cannot process PaymentReceivedMessage until wallet sees that deposits are unlocked for " + trade.getClass().getSimpleName() + " " + trade.getId()); + } + } // set state trade.getSeller().setUpdatedMultisigHex(message.getUpdatedMultisigHex()); @@ -128,14 +134,6 @@ public class ProcessPaymentReceivedMessage extends TradeTask { private void processPayoutTx(PaymentReceivedMessage message) { - // adapt from 1.0.6 to 1.0.7 which changes field usage - // TODO: remove after future updates to allow old trades to clear - if (trade.getPayoutTxHex() != null && trade.getBuyer().getPaymentSentMessage() != null && trade.getPayoutTxHex().equals(trade.getBuyer().getPaymentSentMessage().getPayoutTxHex())) { - log.warn("Nullifying payout tx hex after 1.0.7 update {} {}", trade.getClass().getSimpleName(), trade.getShortId()); - if (trade instanceof BuyerTrade) trade.getSelf().setUnsignedPayoutTxHex(trade.getPayoutTxHex()); - trade.setPayoutTxHex(null); - } - // update wallet trade.importMultisigHex(); trade.syncAndPollWallet(); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentSentMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentSentMessage.java index 93d1ce520a..1f99d64806 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 @@ -46,9 +46,17 @@ public class ProcessPaymentSentMessage extends TradeTask { // update latest peer address trade.getBuyer().setNodeAddress(processModel.getTempTradePeerNodeAddress()); + trade.requestPersistence(); + + // cannot process until wallet sees deposits confirmed + if (!trade.isDepositsConfirmed()) { + trade.syncAndPollWallet(); + if (!trade.isDepositsConfirmed()) { + throw new RuntimeException("Cannot process PaymentSentMessage until wallet sees that deposits are confirmed for " + trade.getClass().getSimpleName() + " " + trade.getId()); + } + } // update state from message - trade.getBuyer().setPaymentSentMessage(message); trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex()); trade.getSeller().setAccountAgeWitness(message.getSellerAccountAgeWitness()); String counterCurrencyTxId = message.getCounterCurrencyTxId(); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java index 1452104e8f..a146fa3419 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java @@ -43,13 +43,6 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask { // handle first time preparation if (trade.getArbitrator().getPaymentReceivedMessage() == null) { - // adapt from 1.0.6 to 1.0.7 which changes field usage - // TODO: remove after future updates to allow old trades to clear - if (trade.getPayoutTxHex() != null && trade.getPayoutTxHex().equals(trade.getBuyer().getPaymentSentMessage().getPayoutTxHex())) { - log.warn("Nullifying payout tx hex after 1.0.7 update {} {}", trade.getClass().getSimpleName(), trade.getShortId()); - trade.setPayoutTxHex(null); - } - // synchronize on lock for wallet operations synchronized (trade.getWalletLock()) { synchronized (HavenoUtils.getWalletFunctionLock()) { diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java index f08fe87946..202d4c8c79 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java @@ -35,11 +35,15 @@ package haveno.core.trade.protocol.tasks; import com.google.common.base.Charsets; + +import haveno.common.Timer; +import haveno.common.UserThread; import haveno.common.crypto.PubKeyRing; import haveno.common.crypto.Sig; import haveno.common.taskrunner.TaskRunner; import haveno.core.account.sign.SignedWitness; import haveno.core.account.witness.AccountAgeWitnessService; +import haveno.core.network.MessageState; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.trade.messages.PaymentReceivedMessage; @@ -47,15 +51,23 @@ import haveno.core.trade.messages.TradeMailboxMessage; import haveno.core.trade.protocol.TradePeer; import haveno.core.util.JsonUtil; import haveno.network.p2p.NodeAddress; +import javafx.beans.value.ChangeListener; import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; import static com.google.common.base.Preconditions.checkArgument; +import java.util.concurrent.TimeUnit; + @Slf4j @EqualsAndHashCode(callSuper = true) public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessageTask { - SignedWitness signedWitness = null; + private SignedWitness signedWitness = null; + private ChangeListener listener; + private Timer timer; + private static final int MAX_RESEND_ATTEMPTS = 20; + private int delayInMin = 10; + private int resendCounter = 0; public SellerSendPaymentReceivedMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); @@ -77,6 +89,13 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag protected void run() { try { runInterceptHook(); + + // skip if already received + if (isReceived()) { + if (!isCompleted()) complete(); + return; + } + super.run(); } catch (Throwable t) { failed(t); @@ -134,29 +153,85 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag @Override protected void setStateSent() { - trade.advanceState(Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG); log.info("{} sent: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness); + getReceiver().setPaymentReceivedMessageState(MessageState.SENT); + tryToSendAgainLater(); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateFault() { - trade.advanceState(Trade.State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG); log.error("{} failed: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness); + getReceiver().setPaymentReceivedMessageState(MessageState.FAILED); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateStoredInMailbox() { - trade.advanceState(Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG); log.info("{} stored in mailbox: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness); + getReceiver().setPaymentReceivedMessageState(MessageState.STORED_IN_MAILBOX); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateArrived() { - trade.advanceState(Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG); log.info("{} arrived: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness); + getReceiver().setPaymentReceivedMessageState(MessageState.ARRIVED); processModel.getTradeManager().requestPersistence(); } + + private void cleanup() { + if (timer != null) { + timer.stop(); + } + if (listener != null) { + trade.getBuyer().getPaymentReceivedMessageStateProperty().removeListener(listener); + } + } + + private void tryToSendAgainLater() { + + // skip if already received + if (isReceived()) return; + + if (resendCounter >= MAX_RESEND_ATTEMPTS) { + cleanup(); + log.warn("We never received an ACK message when sending the PaymentReceivedMessage to the peer. We stop trying to send the message."); + return; + } + + if (timer != null) { + timer.stop(); + } + + timer = UserThread.runAfter(this::run, delayInMin, TimeUnit.MINUTES); + + if (resendCounter == 0) { + listener = (observable, oldValue, newValue) -> onMessageStateChange(newValue); + getReceiver().getPaymentReceivedMessageStateProperty().addListener(listener); + onMessageStateChange(getReceiver().getPaymentReceivedMessageStateProperty().get()); + } + + // first re-send is after 2 minutes, then increase the delay exponentially + if (resendCounter == 0) { + int shortDelay = 2; + log.info("We will send the message again to the peer after a delay of {} min.", shortDelay); + timer = UserThread.runAfter(this::run, shortDelay, TimeUnit.MINUTES); + } else { + log.info("We will send the message again to the peer after a delay of {} min.", delayInMin); + timer = UserThread.runAfter(this::run, delayInMin, TimeUnit.MINUTES); + delayInMin = (int) ((double) delayInMin * 1.5); + } + resendCounter++; + } + + private void onMessageStateChange(MessageState newValue) { + if (isReceived()) { + cleanup(); + } + } + + protected boolean isReceived() { + return getReceiver().isPaymentReceivedMessageReceived(); + } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessageToBuyer.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessageToBuyer.java index 7228b40307..212dcb22f4 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessageToBuyer.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessageToBuyer.java @@ -37,6 +37,30 @@ public class SellerSendPaymentReceivedMessageToBuyer extends SellerSendPaymentRe return trade.getBuyer(); } + @Override + protected void setStateSent() { + trade.advanceState(Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG); + super.setStateSent(); + } + + @Override + protected void setStateFault() { + trade.advanceState(Trade.State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG); + super.setStateFault(); + } + + @Override + protected void setStateStoredInMailbox() { + trade.advanceState(Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG); + super.setStateStoredInMailbox(); + } + + @Override + protected void setStateArrived() { + trade.advanceState(Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG); + super.setStateArrived(); + } + // continue execution on fault so payment received message is sent to arbitrator @Override protected void onFault(String errorMessage, TradeMessage message) { diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessage.java index 8be8f00da5..ba20d74351 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessage.java @@ -23,6 +23,7 @@ import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.crypto.PubKeyRing; import haveno.common.taskrunner.TaskRunner; +import haveno.core.network.MessageState; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; import haveno.core.trade.messages.DepositsConfirmedMessage; @@ -37,7 +38,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTask { private Timer timer; - private static final int MAX_RESEND_ATTEMPTS = 10; + private static final int MAX_RESEND_ATTEMPTS = 20; private int delayInMin = 10; private int resendCounter = 0; @@ -52,8 +53,8 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas try { runInterceptHook(); - // skip if already acked by receiver - if (isAckedByReceiver()) { + // skip if already acked or payout published + if (isAckedByReceiver() || trade.isPayoutPublished()) { if (!isCompleted()) complete(); return; } @@ -64,11 +65,17 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas } } - @Override - protected abstract NodeAddress getReceiverNodeAddress(); + protected abstract TradePeer getReceiver(); @Override - protected abstract PubKeyRing getReceiverPubKeyRing(); + protected NodeAddress getReceiverNodeAddress() { + return getReceiver().getNodeAddress(); + } + + @Override + protected PubKeyRing getReceiverPubKeyRing() { + return getReceiver().getPubKeyRing(); + } @Override protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) { @@ -97,23 +104,24 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas @Override protected void setStateSent() { + getReceiver().setDepositsConfirmedMessageState(MessageState.SENT); tryToSendAgainLater(); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateArrived() { - // no additional handling + getReceiver().setDepositsConfirmedMessageState(MessageState.ARRIVED); } @Override protected void setStateStoredInMailbox() { - // no additional handling + getReceiver().setDepositsConfirmedMessageState(MessageState.STORED_IN_MAILBOX); } @Override protected void setStateFault() { - // no additional handling + getReceiver().setDepositsConfirmedMessageState(MessageState.FAILED); } private void cleanup() { @@ -137,7 +145,7 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas timer.stop(); } - // first re-send is after 2 minutes, then double the delay each iteration + // first re-send is after 2 minutes, then increase the delay exponentially if (resendCounter == 0) { int shortDelay = 2; log.info("We will send the message again to the peer after a delay of {} min.", shortDelay); @@ -145,13 +153,12 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas } else { log.info("We will send the message again to the peer after a delay of {} min.", delayInMin); timer = UserThread.runAfter(this::run, delayInMin, TimeUnit.MINUTES); - delayInMin = delayInMin * 2; + delayInMin = (int) ((double) delayInMin * 1.5); } resendCounter++; } private boolean isAckedByReceiver() { - TradePeer peer = trade.getTradePeer(getReceiverNodeAddress()); - return peer.isDepositsConfirmedMessageAcked(); + return getReceiver().isDepositsConfirmedMessageAcked(); } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToArbitrator.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToArbitrator.java index baaa6ae987..ae8a171aa8 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToArbitrator.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToArbitrator.java @@ -17,10 +17,9 @@ package haveno.core.trade.protocol.tasks; -import haveno.common.crypto.PubKeyRing; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; -import haveno.network.p2p.NodeAddress; +import haveno.core.trade.protocol.TradePeer; import lombok.extern.slf4j.Slf4j; /** @@ -34,12 +33,7 @@ public class SendDepositsConfirmedMessageToArbitrator extends SendDepositsConfir } @Override - public NodeAddress getReceiverNodeAddress() { - return trade.getArbitrator().getNodeAddress(); - } - - @Override - public PubKeyRing getReceiverPubKeyRing() { - return trade.getArbitrator().getPubKeyRing(); + protected TradePeer getReceiver() { + return trade.getArbitrator(); } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToBuyer.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToBuyer.java index 5795ce8947..bf1212c9a8 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToBuyer.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToBuyer.java @@ -17,10 +17,9 @@ package haveno.core.trade.protocol.tasks; -import haveno.common.crypto.PubKeyRing; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; -import haveno.network.p2p.NodeAddress; +import haveno.core.trade.protocol.TradePeer; import lombok.extern.slf4j.Slf4j; /** @@ -34,12 +33,7 @@ public class SendDepositsConfirmedMessageToBuyer extends SendDepositsConfirmedMe } @Override - public NodeAddress getReceiverNodeAddress() { - return trade.getBuyer().getNodeAddress(); - } - - @Override - public PubKeyRing getReceiverPubKeyRing() { - return trade.getBuyer().getPubKeyRing(); + protected TradePeer getReceiver() { + return trade.getBuyer(); } } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToSeller.java b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToSeller.java index efdf9a99cd..4ea097fd2b 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToSeller.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/SendDepositsConfirmedMessageToSeller.java @@ -17,10 +17,9 @@ package haveno.core.trade.protocol.tasks; -import haveno.common.crypto.PubKeyRing; import haveno.common.taskrunner.TaskRunner; import haveno.core.trade.Trade; -import haveno.network.p2p.NodeAddress; +import haveno.core.trade.protocol.TradePeer; import lombok.extern.slf4j.Slf4j; /** @@ -34,12 +33,7 @@ public class SendDepositsConfirmedMessageToSeller extends SendDepositsConfirmedM } @Override - public NodeAddress getReceiverNodeAddress() { - return trade.getSeller().getNodeAddress(); - } - - @Override - public PubKeyRing getReceiverPubKeyRing() { - return trade.getSeller().getPubKeyRing(); + protected TradePeer getReceiver() { + return trade.getSeller(); } } 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 e6c71032f4..aa0fc9dfe9 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 @@ -70,6 +70,9 @@ public class TakerReserveTradeFunds extends TradeTask { MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection(); try { reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, takerFee, sendAmount, securityDeposit, returnAddress, false, null); + } catch (IllegalStateException e) { + log.warn("Illegal state creating reserve tx, offerId={}, error={}", trade.getShortId(), i + 1, e.getMessage()); + throw e; } catch (Exception e) { log.warn("Error creating reserve tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); trade.getXmrWalletService().handleWalletError(e, sourceConnection); diff --git a/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java b/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java index 97217b0fe4..a8fa1ade26 100644 --- a/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java +++ b/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java @@ -75,8 +75,6 @@ public class XmrNodes { new XmrNode(MoneroNodesOption.PROVIDED, null, null, "127.0.0.1", 38081, 1, "@local"), new XmrNode(MoneroNodesOption.PROVIDED, null, null, "127.0.0.1", 39081, 1, "@local"), new XmrNode(MoneroNodesOption.PROVIDED, null, null, "45.63.8.26", 38081, 2, "@haveno"), - new XmrNode(MoneroNodesOption.PROVIDED, null, null, "stagenet.community.rino.io", 38081, 3, "@RINOwallet"), - new XmrNode(MoneroNodesOption.PUBLIC, null, null, "stagenet.melo.tools", 38081, 3, null), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node.sethforprivacy.com", 38089, 3, null), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node2.sethforprivacy.com", 38089, 3, null), new XmrNode(MoneroNodesOption.PUBLIC, null, "plowsof3t5hogddwabaeiyrno25efmzfxyro2vligremt7sxpsclfaid.onion", null, 38089, 3, null) @@ -85,7 +83,6 @@ public class XmrNodes { return Arrays.asList( new XmrNode(MoneroNodesOption.PUBLIC, null, null, "127.0.0.1", 18081, 1, "@local"), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "xmr-node.cakewallet.com", 18081, 2, "@cakewallet"), - new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node.community.rino.io", 18081, 2, "@RINOwallet"), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "nodes.hashvault.pro", 18080, 2, "@HashVault"), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "p2pmd.xmrvsbeast.com", 18080, 2, "@xmrvsbeast"), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node.monerodevs.org", 18089, 2, "@monerodevs.org"), 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 c993ebd181..1bb1e3500e 100644 --- a/core/src/main/java/haveno/core/xmr/setup/MoneroWalletRpcManager.java +++ b/core/src/main/java/haveno/core/xmr/setup/MoneroWalletRpcManager.java @@ -135,7 +135,7 @@ public class MoneroWalletRpcManager { // stop process String pid = walletRpc.getProcess() == null ? null : String.valueOf(walletRpc.getProcess().pid()); - log.info("Stopping MoneroWalletRpc path={}, port={}, pid={}", path, port, pid); + log.info("Stopping MoneroWalletRpc path={}, port={}, pid={}, force={}", path, port, pid, force); walletRpc.stopProcess(force); } diff --git a/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java b/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java index b270762d3b..aefb92c41a 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java +++ b/core/src/main/java/haveno/core/xmr/wallet/Restrictions.java @@ -30,6 +30,7 @@ public class Restrictions { public static final double MAX_SECURITY_DEPOSIT_PCT = 0.5; public static BigInteger MIN_TRADE_AMOUNT = HavenoUtils.xmrToAtomicUnits(0.1); public static BigInteger MIN_SECURITY_DEPOSIT = HavenoUtils.xmrToAtomicUnits(0.1); + public static int MAX_EXTRA_INFO_LENGTH = 1500; // At mediation we require a min. payout to the losing party to keep incentive for the trader to accept the // mediated payout. For Refund agent cases we do not have that restriction. 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 0592fbdd59..7bfc37f8e2 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -68,7 +68,6 @@ import java.util.stream.Stream; import javafx.beans.property.LongProperty; import javafx.beans.property.ReadOnlyDoubleProperty; import javafx.beans.value.ChangeListener; -import lombok.Getter; import monero.common.MoneroError; import monero.common.MoneroRpcConnection; import monero.common.MoneroRpcError; @@ -145,8 +144,7 @@ public class XmrWalletService extends XmrWalletBase { private TradeManager tradeManager; private ExecutorService syncWalletThreadPool = Executors.newFixedThreadPool(10); // TODO: adjust based on connection type - @Getter - public final Object lock = new Object(); + private final Object lock = new Object(); private TaskLooper pollLooper; private boolean pollInProgress; private Long pollPeriodMs; @@ -247,7 +245,11 @@ public class XmrWalletService extends XmrWalletBase { @Override public void saveWallet() { - saveWallet(!(Utilities.isWindows() && wallet != null)); + saveWallet(shouldBackup(wallet)); + } + + private boolean shouldBackup(MoneroWallet wallet) { + return wallet != null && !Utilities.isWindows(); // TODO: cannot backup on windows because file is locked } public void saveWallet(boolean backup) { @@ -389,7 +391,7 @@ public class XmrWalletService extends XmrWalletBase { MoneroError err = null; String path = wallet.getPath(); try { - if (save) saveWallet(wallet, true); + if (save) saveWallet(wallet, shouldBackup(wallet)); wallet.close(); } catch (MoneroError e) { err = e; @@ -736,7 +738,7 @@ public class XmrWalletService extends XmrWalletBase { MoneroDaemonRpc daemon = getDaemon(); MoneroWallet wallet = getWallet(); MoneroTx tx = null; - synchronized (daemon) { + synchronized (lock) { try { // verify tx not submitted to pool @@ -765,7 +767,7 @@ public class XmrWalletService extends XmrWalletBase { BigInteger minerFeeEstimate = getFeeEstimate(tx.getWeight()); double minerFeeDiff = tx.getFee().subtract(minerFeeEstimate).abs().doubleValue() / minerFeeEstimate.doubleValue(); if (minerFeeDiff > MINER_FEE_TOLERANCE) throw new RuntimeException("Miner fee is not within " + (MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + minerFeeEstimate + " but was " + tx.getFee() + ", diff%=" + minerFeeDiff); - log.info("Trade tx fee {} is within tolerance, diff%={}", tx.getFee(), minerFeeDiff); + log.info("Trade miner fee {} is within tolerance, diff%={}", tx.getFee(), minerFeeDiff); // verify proof to fee address BigInteger actualTradeFee = BigInteger.ZERO; @@ -783,7 +785,7 @@ public class XmrWalletService extends XmrWalletBase { // verify trade fee amount if (!actualTradeFee.equals(tradeFeeAmount)) { if (equalsWithinFractionError(actualTradeFee, tradeFeeAmount)) { - log.warn("Trade tx fee amount is within fraction error, expected " + tradeFeeAmount + " but was " + actualTradeFee); + log.warn("Trade fee amount is within fraction error, expected " + tradeFeeAmount + " but was " + actualTradeFee); } else { throw new RuntimeException("Invalid trade fee amount, expected " + tradeFeeAmount + " but was " + actualTradeFee); } @@ -922,7 +924,7 @@ public class XmrWalletService extends XmrWalletBase { } // shut down threads - synchronized (getLock()) { + synchronized (lock) { List shutDownThreads = new ArrayList<>(); shutDownThreads.add(() -> ThreadUtils.shutDown(THREAD_ID)); ThreadUtils.awaitTasks(shutDownThreads); @@ -1014,6 +1016,13 @@ public class XmrWalletService extends XmrWalletBase { public synchronized void resetAddressEntriesForOpenOffer(String offerId) { log.info("resetAddressEntriesForOpenOffer offerId={}", offerId); + + // skip if failed trade is scheduled for processing // TODO: do not call this function in this case? + if (tradeManager.hasFailedScheduledTrade(offerId)) { + log.warn("Refusing to reset address entries because trade is scheduled for deletion with offerId={}", offerId); + return; + } + swapAddressEntryToAvailable(offerId, XmrAddressEntry.Context.OFFER_FUNDING); // swap trade payout to available if applicable @@ -1162,7 +1171,7 @@ public class XmrWalletService extends XmrWalletBase { public Stream getAddressEntriesForAvailableBalanceStream() { Stream available = getFundedAvailableAddressEntries().stream(); available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.ARBITRATOR).stream()); - available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.OFFER_FUNDING).stream().filter(entry -> !tradeManager.getOpenOfferManager().getOpenOfferById(entry.getOfferId()).isPresent())); + available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.OFFER_FUNDING).stream().filter(entry -> !tradeManager.getOpenOfferManager().getOpenOffer(entry.getOfferId()).isPresent())); available = Stream.concat(available, getAddressEntries(XmrAddressEntry.Context.TRADE_PAYOUT).stream().filter(entry -> tradeManager.getTrade(entry.getOfferId()) == null || tradeManager.getTrade(entry.getOfferId()).isPayoutUnlocked())); return available.filter(addressEntry -> getBalanceForSubaddress(addressEntry.getSubaddressIndex()).compareTo(BigInteger.ZERO) > 0); } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index d8111b7b9c..3a6b666102 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -134,7 +134,7 @@ shared.noDateAvailable=No date available shared.noDetailsAvailable=No details available shared.notUsedYet=Not used yet shared.date=Date -shared.sendFundsDetailsWithFee=Sending: {0}\nTo receiving address: {1}.\nRequired mining fee is: {2}\n\nThe recipient will receive: {3}\n\nAre you sure you want to withdraw this amount? +shared.sendFundsDetailsWithFee=Sending: {0}\n\nTo receiving address: {1}\n\nAdditional miner fee: {2}\n\nAre you sure you want to send this amount? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=Copy to clipboard @@ -495,6 +495,7 @@ createOffer.triggerPrice.tooltip=As protection against drastic price movements y deactivates the offer if the market price reaches that value. createOffer.triggerPrice.invalid.tooLow=Value must be higher than {0} createOffer.triggerPrice.invalid.tooHigh=Value must be lower than {0} +createOffer.extraInfo.invalid.tooLong=Must not exceed {0} characters. # new entries createOffer.placeOfferButton=Review: Place offer to {0} monero @@ -672,7 +673,8 @@ portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. N # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. -portfolio.pending.step1.info=Deposit transaction has been published.\n{0} need to wait for 10 confirmations (about 20 minutes) before the payment can start. +portfolio.pending.step1.info.you=Deposit transaction has been published.\nYou need to wait for 10 confirmations (about 20 minutes) before the payment can start. +portfolio.pending.step1.info.buyer=Deposit transaction has been published.\nThe XMR buyer needs to wait for 10 confirmations (about 20 minutes) before the payment can start. portfolio.pending.step1.warn=The deposit transaction is not confirmed yet. This usually takes about 20 minutes, but could be more if the network is congested. portfolio.pending.step1.openForDispute=The deposit transaction is still not confirmed. \ If you have been waiting for much longer than 20 minutes, contact Haveno support. @@ -962,7 +964,7 @@ portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee trans portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\n\ Without this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. \ You can make a request to be reimbursed the trade fee here: \ - [HYPERLINK:https://github.com/bisq-network/support/issues]\n\n\ + [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\n\ Feel free to move this trade to failed trades. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, \ but funds have been locked in the deposit transaction.\n\n\ @@ -972,7 +974,7 @@ portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=T (with seller receiving full trade amount back as well). \ This way, there is no security risk, and only trade fees are lost. \n\n\ You can request a reimbursement for lost trade fees here: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing \ but funds have been locked in the deposit transaction.\n\n\ If the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open \ @@ -981,18 +983,18 @@ portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx= their security deposits (with seller receiving full trade amount back as well). \ Otherwise the trade amount should go to the buyer. \n\n\ You can request a reimbursement for lost trade fees here: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\n\ Error: {0}\n\n\ It might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation \ ticket to get advice from Haveno mediators. \n\n\ If the error was critical and the trade cannot be completed, you might have lost your trade fee. \ Request a reimbursement for lost trade fees here: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\n\ The trade cannot be completed and you might \ have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\n\ Do you want to move the trade to failed trades?\n\n\ @@ -2056,7 +2058,8 @@ closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} of total trade amoun walletPasswordWindow.headline=Enter password to unlock connectionFallback.headline=Connection error -connectionFallback.msg=Error connecting to your custom Monero node(s).\n\nDo you want to try the next best available Monero node? +connectionFallback.customNode=Error connecting to your custom Monero node(s).\n\nDo you want to use the next best available Monero node? +connectionFallback.localNode=Error connecting to your last used local node.\n\nDo you want to use the next best available Monero node? torNetworkSettingWindow.header=Tor networks settings torNetworkSettingWindow.noBridges=Don't use bridges @@ -3201,14 +3204,16 @@ DOMESTIC_WIRE_TRANSFER=Domestic Wire Transfer # suppress inspection "UnusedProperty" BSQ_SWAP=BSQ Swap -# Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" OK_PAY=OKPay # suppress inspection "UnusedProperty" CASH_APP=Cash App # suppress inspection "UnusedProperty" VENMO=Venmo +# suppress inspection "UnusedProperty" PAYPAL=PayPal +# suppress inspection "UnusedProperty" +PAYSAFE=Paysafe # suppress inspection "UnusedProperty" UPHOLD_SHORT=Uphold @@ -3304,9 +3309,10 @@ OK_PAY_SHORT=OKPay CASH_APP_SHORT=Cash App # suppress inspection "UnusedProperty" VENMO_SHORT=Venmo +# suppress inspection "UnusedProperty" PAYPAL_SHORT=PayPal # suppress inspection "UnusedProperty" -PAYSAFE=Paysafe +PAYSAFE_SHORT=Paysafe #################################################################### diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index 5a2a065535..bc1841259c 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -134,7 +134,7 @@ shared.noDateAvailable=Žádné datum není k dispozici shared.noDetailsAvailable=Detaily nejsou k dispozici shared.notUsedYet=Ještě nepoužito shared.date=Datum -shared.sendFundsDetailsWithFee=Odesílání: {0}nNa přijímací adresu: {1}.nPožadován těžební poplatek: {2}\n\nPříjemce dostane: {3}\n\nJste si jisti, že chcete vyplatit tuto částku? +shared.sendFundsDetailsWithFee=Odesílání: {0}\n\nNa přijímací adresu: {1}\n\nDalší poplatek pro těžaře: {2}\n\nJste si jisti, že chcete vyplatit tuto částku? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno zjistil, že tato transakce by vytvořila drobné mince, které jsou pod limitem drobných mincí (a není to povoleno pravidly pro konsenzus Monero). Místo toho budou tyto drobné mince ({0} satoshi {1}) přidány k poplatku za těžbu.\n\n\n shared.copyToClipboard=Kopírovat do schránky @@ -672,7 +672,8 @@ portfolio.pending.autoConf.state.ERROR=Došlo k chybě při požadavku na služb # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=Služba se vrátila se selháním. Není možné automatické potvrzení. -portfolio.pending.step1.info=Vkladová transakce byla zveřejněna.\n{0} před zahájením platby musíte počkat na alespoň jedno potvrzení na blockchainu. +portfolio.pending.step1.info.you=Transakce vkladu byla publikována.\nMusíte počkat na 10 potvrzení (přibližně 20 minut), než bude platba zahájena. +portfolio.pending.step1.info.buyer=Transakce vkladu byla publikována.\nKupující XMR musí počkat na 10 potvrzení (asi 20 minut), než bude platba zahájena. portfolio.pending.step1.warn=Vkladová transakce není stále potvrzena. K tomu někdy dochází ve vzácných případech, kdy byl poplatek za financování jednoho obchodníka z externí peněženky příliš nízký. portfolio.pending.step1.openForDispute=Vkladová transakce není stále potvrzena. \ Pokud jste čekali mnohem déle než 20 minut, můžete poádat o pomoc podporu Haveno. @@ -962,7 +963,7 @@ portfolio.pending.failedTrade.maker.missingTakerFeeTx=Chybí poplatek příjemce portfolio.pending.failedTrade.missingDepositTx=Vkladová transakce (transakce 2-of-2 multisig) chybí.\n\n\ Bez této tx nelze obchod dokončit. Nebyly uzamčeny žádné prostředky, ale byl zaplacen váš obchodní poplatek. \ Zde můžete požádat o vrácení obchodního poplatku: \ - [HYPERLINK:https://github.com/bisq-network/support/issues]\n\n\ + [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\n\ Klidně můžete přesunout tento obchod do neúspěšných obchodů. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=Zpožděná výplatní transakce chybí, ale prostředky byly uzamčeny v vkladové transakci.\n\n\ @@ -972,7 +973,7 @@ portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=Z (přičemž prodejce také obdrží plnou částku obchodu). \ Tímto způsobem nehrozí žádné bezpečnostní riziko a jsou ztraceny pouze obchodní poplatky.\n\n\ O vrácení ztracených obchodních poplatků můžete požádat zde: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=Zpožděná výplatní transakce chybí, \ ale prostředky byly v depozitní transakci uzamčeny.\n\n\ Pokud kupujícímu chybí také odložená výplatní transakce, bude poučen, aby platbu NEPOSLAL a místo toho otevřel \ @@ -981,18 +982,18 @@ portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx= svých bezpečnostních vkladů (přičemž prodejce také obdrží plnou částku obchodu). \ Jinak by částka obchodu měla jít kupujícímu.\n\n\ O vrácení ztracených obchodních poplatků můžete požádat zde: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=Během provádění obchodního protokolu došlo k chybě.\n\n Chyba: {0}\n\n\ Je možné, že tato chyba není kritická a obchod lze dokončit normálně. Pokud si nejste jisti, otevřete si mediační úkol \ a získejte radu od mediátorů Haveno.\n\n\ Pokud byla chyba kritická a obchod nelze dokončit, možná jste ztratili obchodní poplatek. \ O vrácení ztracených obchodních poplatků požádejte zde: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.missingContract=Obchodní kontrakt není stanoven.\n\n\ Obchod nelze dokončit a možná jste ztratili poplatek \ za obchodování. Pokud ano, můžete požádat o vrácení ztracených obchodních poplatků zde: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=Obchodní protokol narazil na některé problémy.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=Obchodní protokol narazil na vážný problém.\n\n{0}\n\n\ Chcete obchod přesunout do neúspěšných obchodů?\n\n\ @@ -2056,7 +2057,7 @@ closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} z celkového objemu walletPasswordWindow.headline=Pro odemknutí zadejte heslo connectionFallback.headline=Chyba připojení -connectionFallback.msg=Chyba při připojování k vlastním uzlům Monero.\n\nChcete vyzkoušet další nejlepší dostupný uzel Monero? +connectionFallback.customNode=Chyba při připojování k vlastním uzlům Monero.\n\nChcete vyzkoušet další nejlepší dostupný uzel Monero? torNetworkSettingWindow.header=Nastavení sítě Tor torNetworkSettingWindow.noBridges=Nepoužívat most (bridge) diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index c19f183f47..ce1d2c2a2a 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -125,6 +125,7 @@ shared.noDateAvailable=Kein Datum verfügbar shared.noDetailsAvailable=Keine Details vorhanden shared.notUsedYet=Noch ungenutzt shared.date=Datum +shared.sendFundsDetailsWithFee=Senden: {0}\n\nAn die Empfangsadresse: {1}\n\nZusätzliche Miner-Gebühr: {2}\n\nSind Sie sicher, dass Sie diesen Betrag senden möchten? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Diese Transaktion würde ein Wechselgeld erzeugen das unterhalb des Dust-Grenzwerts liegt (und daher von den Monero-Konsensregeln nicht erlaubt wäre). Stattdessen wird dieser Dust ({0} Satoshi{1}) der Mining-Gebühr hinzugefügt.\n\n\n shared.copyToClipboard=In Zwischenablage kopieren @@ -615,7 +616,8 @@ portfolio.pending.autoConf.state.ERROR=An einer Service-Abfrage ist ein Fehler a # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=Eine Service-Abfrage ist ausgefallen. Eine Automatische Bestätigung ist nicht mehr möglich. -portfolio.pending.step1.info=Die Kautionstransaktion wurde veröffentlicht.\n{0} muss auf wenigstens eine Blockchain-Bestätigung warten, bevor die Zahlung beginnt. +portfolio.pending.step1.info.you=Die Einzahlungstransaktion wurde veröffentlicht.\nSie müssen 10 Bestätigungen abwarten (etwa 20 Minuten), bevor die Zahlung beginnen kann. +portfolio.pending.step1.info.buyer=Die Einzahlungstransaktion wurde veröffentlicht.\nDer XMR-Käufer muss 10 Bestätigungen abwarten (ca. 20 Minuten), bevor die Zahlung gestartet werden kann. portfolio.pending.step1.warn=Die Kautionstransaktion ist noch nicht bestätigt. Dies geschieht manchmal in seltenen Fällen, wenn die Finanzierungsgebühr aus der externen Wallet eines Traders zu niedrig war. portfolio.pending.step1.openForDispute=Die Kautionstransaktion ist noch nicht bestätigt. Sie können länger warten oder den Vermittler um Hilfe bitten. @@ -818,11 +820,11 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=Sie haben bereits akzept portfolio.pending.failedTrade.taker.missingTakerFeeTx=Die Transaktion der Abnehmer-Gebühr fehlt.\n\nOhne diese tx kann der Handel nicht abgeschlossen werden. Keine Gelder wurden gesperrt und keine Handelsgebühr wurde bezahlt. Sie können diesen Handel zu den fehlgeschlagenen Händeln verschieben. portfolio.pending.failedTrade.maker.missingTakerFeeTx=Die Transaktion der Abnehmer-Gebühr fehlt.\n\nOhne diese tx kann der Handel nicht abgeschlossen werden. Keine Gelder wurden gesperrt. Ihr Angebot ist für andere Händler weiterhin verfügbar. Sie haben die Ersteller-Gebühr also nicht verloren. Sie können diesen Handel zu den fehlgeschlagenen Händeln verschieben. -portfolio.pending.failedTrade.missingDepositTx=Die Einzahlungstransaktion (die 2-of-2 Multisig-Transaktion) fehlt.\n\nOhne diese tx kann der Handel nicht abgeschlossen werden. Keine Gelder wurden gesperrt aber die Handels-Gebühr wurde bezahlt. Sie können eine Anfrage für eine Rückerstattung der Handels-Gebühr hier einreichen: [HYPERLINK:https://github.com/bisq-network/support/issues]\n\nSie können diesen Handel gerne zu den fehlgeschlagenen Händeln verschieben. -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=Die verzögerte Auszahlungstransaktion fehlt, aber die Gelder wurden in der Einzahlungstransaktion gesperrt.\n\nBitte schicken Sie KEINE Geld-(Traditional-) oder Crypto-Zahlungen an den XMR Verkäufer, weil ohne die verzögerte Auszahlungstransaktion später kein Schlichtungsverfahren eröffnet werden kann. Stattdessen öffnen Sie ein Vermittlungs-Ticket mit Cmd/Strg+o. Der Vermittler sollte vorschlagen, dass beide Handelspartner ihre vollständige Sicherheitskaution zurückerstattet bekommen (und der Verkäufer auch seinen Handels-Betrag). Durch diese Vorgehensweise entsteht kein Sicherheitsrisiko und es geht ausschließlich die Handelsgebühr verloren.\n\nSie können eine Rückerstattung der verlorenen gegangenen Handelsgebühren hier erbitten: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=Die verzögerte Auszahlungstransaktion fehlt, aber die Gelder wurden in der Einzahlungstransaktion gesperrt.\n\nWenn dem Käufer die verzögerte Auszahlungstransaktion auch fehlt, wird er dazu aufgefordert die Bezahlung NICHT zu schicken und stattdessen ein Vermittlungs-Ticket zu eröffnen. Sie sollten auch ein Vermittlungs-Ticket mit Cmd/Strg+o öffnen.\n\nWenn der Käufer die Zahlung noch nicht geschickt hat, sollte der Vermittler vorschlagen, dass beide Handelspartner ihre Sicherheitskaution vollständig zurückerhalten (und der Verkäufer auch den Handels-Betrag). Anderenfalls sollte der Handels-Betrag an den Käufer gehen.\n\nSie können eine Rückerstattung der verlorenen gegangenen Handelsgebühren hier erbitten: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.errorMsgSet=Während der Ausführung des Handel-Protokolls ist ein Fehler aufgetreten.\n\nFehler: {0}\n\nEs kann sein, dass dieser Fehler nicht gravierend ist und der Handel ganz normal abgeschlossen werden kann. Wenn Sie sich unsicher sind, öffnen Sie ein Vermittlungs-Ticket um den Rat eines Haveno Vermittlers zu erhalten.\n\nWenn der Fehler gravierend war, kann der Handel nicht abgeschlossen werden und Sie haben vielleicht die Handelsgebühr verloren. Sie können eine Rückerstattung der verlorenen gegangenen Handelsgebühren hier erbitten: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=Der Handelsvertrag ist nicht festgelegt.\n\nDer Handel kann nicht abgeschlossen werden und Sie haben möglicherweise die Handelsgebühr verloren. Sollte das der Fall sein, können Sie eine Rückerstattung der verlorenen gegangenen Handelsgebühren hier beantragen: [HYPERLINK:https://github.com/bisq-network/support/issues] +portfolio.pending.failedTrade.missingDepositTx=Die Einzahlungstransaktion (die 2-of-2 Multisig-Transaktion) fehlt.\n\nOhne diese tx kann der Handel nicht abgeschlossen werden. Keine Gelder wurden gesperrt aber die Handels-Gebühr wurde bezahlt. Sie können eine Anfrage für eine Rückerstattung der Handels-Gebühr hier einreichen: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nSie können diesen Handel gerne zu den fehlgeschlagenen Händeln verschieben. +portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=Die verzögerte Auszahlungstransaktion fehlt, aber die Gelder wurden in der Einzahlungstransaktion gesperrt.\n\nBitte schicken Sie KEINE Geld-(Traditional-) oder Crypto-Zahlungen an den XMR Verkäufer, weil ohne die verzögerte Auszahlungstransaktion später kein Schlichtungsverfahren eröffnet werden kann. Stattdessen öffnen Sie ein Vermittlungs-Ticket mit Cmd/Strg+o. Der Vermittler sollte vorschlagen, dass beide Handelspartner ihre vollständige Sicherheitskaution zurückerstattet bekommen (und der Verkäufer auch seinen Handels-Betrag). Durch diese Vorgehensweise entsteht kein Sicherheitsrisiko und es geht ausschließlich die Handelsgebühr verloren.\n\nSie können eine Rückerstattung der verlorenen gegangenen Handelsgebühren hier erbitten: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=Die verzögerte Auszahlungstransaktion fehlt, aber die Gelder wurden in der Einzahlungstransaktion gesperrt.\n\nWenn dem Käufer die verzögerte Auszahlungstransaktion auch fehlt, wird er dazu aufgefordert die Bezahlung NICHT zu schicken und stattdessen ein Vermittlungs-Ticket zu eröffnen. Sie sollten auch ein Vermittlungs-Ticket mit Cmd/Strg+o öffnen.\n\nWenn der Käufer die Zahlung noch nicht geschickt hat, sollte der Vermittler vorschlagen, dass beide Handelspartner ihre Sicherheitskaution vollständig zurückerhalten (und der Verkäufer auch den Handels-Betrag). Anderenfalls sollte der Handels-Betrag an den Käufer gehen.\n\nSie können eine Rückerstattung der verlorenen gegangenen Handelsgebühren hier erbitten: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.errorMsgSet=Während der Ausführung des Handel-Protokolls ist ein Fehler aufgetreten.\n\nFehler: {0}\n\nEs kann sein, dass dieser Fehler nicht gravierend ist und der Handel ganz normal abgeschlossen werden kann. Wenn Sie sich unsicher sind, öffnen Sie ein Vermittlungs-Ticket um den Rat eines Haveno Vermittlers zu erhalten.\n\nWenn der Fehler gravierend war, kann der Handel nicht abgeschlossen werden und Sie haben vielleicht die Handelsgebühr verloren. Sie können eine Rückerstattung der verlorenen gegangenen Handelsgebühren hier erbitten: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.missingContract=Der Handelsvertrag ist nicht festgelegt.\n\nDer Handel kann nicht abgeschlossen werden und Sie haben möglicherweise die Handelsgebühr verloren. Sollte das der Fall sein, können Sie eine Rückerstattung der verlorenen gegangenen Handelsgebühren hier beantragen: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=Das Handels-Protokoll hat ein paar Probleme gefunden.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=Das Handels-Protokoll hat ein schwerwiegendes Problem gefunden.\n\n{0}\n\nWollen Sie den Handel zu den fehlgeschlagenen Händeln verschieben?\n\nSie können keine Vermittlungs- oder Schlichtungsverfahren auf der Seite für fehlgeschlagene Händel eröffnen, aber Sie können einen fehlgeschlagene Handel wieder auf die Seite der offenen Händeln zurück verschieben. portfolio.pending.failedTrade.txChainValid.moveToFailed=Das Handels-Protokoll hat ein paar Probleme gefunden.\n\n{0}\n\nDie Transaktionen des Handels wurden veröffentlicht und die Gelder sind gesperrt. Verschieben Sie den Handel nur dann zu den fehlgeschlagenen Händeln, wenn Sie sich wirklich sicher sind. Dies könnte Optionen zur Behebung des Problems verhindern.\n\nWollen Sie den Handel zu den fehlgeschlagenen Händeln verschieben?\n\nSie können keine Vermittlungs- oder Schlichtungsverfahren auf der Seite für fehlgeschlagene Händel eröffnen, aber Sie können einen fehlgeschlagene Handel wieder auf die Seite der offenen Händeln zurück verschieben. diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index 5174d8b022..a8fe7a8fa0 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -125,6 +125,7 @@ shared.noDateAvailable=Sin fecha disponible shared.noDetailsAvailable=Sin detalles disponibles shared.notUsedYet=Sin usar aún shared.date=Fecha +shared.sendFundsDetailsWithFee=Enviando: {0}\n\nA la dirección receptora: {1}\n\nTarifa adicional para el minero: {2}\n\n¿Estás seguro de que deseas enviar esta cantidad? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detectó que esta transacción crearía una salida que está por debajo del umbral mínimo considerada polvo (y no está permitida por las reglas de consenso en Monero). En cambio, esta transacción polvo ({0} satoshi {1}) se agregará a la tarifa de minería.\n\n\n shared.copyToClipboard=Copiar al portapapeles @@ -615,7 +616,8 @@ portfolio.pending.autoConf.state.ERROR=Ocurrió un error en el servicio solicit # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=Un servicio volvió con algún fallo. No es posible la autoconfirmación. -portfolio.pending.step1.info=La transacción de depósito ha sido publicada.\n{0} tiene que esperar al menos una confirmación en la cadena de bloques antes de comenzar el pago. +portfolio.pending.step1.info.you=La transacción de depósito ha sido publicada.\nNecesitas esperar 10 confirmaciones (aproximadamente 20 minutos) antes de que el pago pueda comenzar. +portfolio.pending.step1.info.buyer=La transacción de depósito ha sido publicada.\nEl comprador de XMR necesita esperar 10 confirmaciones (aproximadamente 20 minutos) antes de que el pago pueda comenzar. portfolio.pending.step1.warn=La transacción del depósito aún no se ha confirmado.\nEsto puede suceder en raras ocasiones cuando la tasa de depósito de un comerciante desde una cartera externa es demasiado baja. portfolio.pending.step1.openForDispute=La transacción de depósito aún no ha sido confirmada. Puede esperar más o contactar con el mediador para obtener asistencia. @@ -818,11 +820,11 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=Ya ha aceptado portfolio.pending.failedTrade.taker.missingTakerFeeTx=Falta la transacción de tasa de tomador\n\nSin esta tx, el intercambio no se puede completar. No se han bloqueado fondos y no se ha pagado ninguna tasa de intercambio. Puede mover esta operación a intercambios fallidos. portfolio.pending.failedTrade.maker.missingTakerFeeTx=Falta la transacción de tasa de tomador de su par.\n\nSin esta tx, el intercambio no se puede completar. No se han bloqueado fondos. Su oferta aún está disponible para otros comerciantes, por lo que no ha perdido la tasa de tomador. Puede mover este intercambio a intercambios fallidos. -portfolio.pending.failedTrade.missingDepositTx=Falta la transacción de depósito (la transacción multifirma 2 de 2).\n\nSin esta tx, el intercambio no se puede completar. No se han bloqueado fondos, pero se ha pagado su tarifa comercial. Puede hacer una solicitud para que se le reembolse la tarifa comercial aquí: [HYPERLINK:https://github.com/bisq-network/support/issues].\n\nSiéntase libre de mover esta operación a operaciones fallidas. -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=Falta la transacción de pago demorado, pero los fondos se han bloqueado en la transacción de depósito.\n\nNO envíe el pago traditional o crypto al vendedor de XMR, porque sin el tx de pago demorado, no se puede abrir el arbitraje. En su lugar, abra un ticket de mediación con Cmd / Ctrl + o. El mediador debe sugerir que ambos pares recuperen el monto total de sus depósitos de seguridad (y el vendedor también recibirá el monto total de la operación). De esta manera, no hay riesgo en la seguridad y solo se pierden las tarifas comerciales.\n\nPuede solicitar un reembolso por las tarifas comerciales perdidas aquí: [HYPERLINK:https://github.com/bisq-network/support/issues]. -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=Falta la transacción del pago demorado, pero los fondos se han bloqueado en la transacción de depósito.\n\nSi al comprador también le falta la transacción de pago demorado, se le indicará que NO envíe el pago y abra un ticket de mediación. También debe abrir un ticket de mediación con Cmd / Ctrl + o.\n\nSi el comprador aún no ha enviado el pago, el mediador debe sugerir que ambos pares recuperen el monto total de sus depósitos de seguridad (y el vendedor también recibirá el monto total de la operación). De lo contrario, el monto comercial debe ir al comprador.\n\nPuede solicitar un reembolso por las tarifas comerciales perdidas aquí: [HYPERLINK:https://github.com/bisq-network/support/issues]. +portfolio.pending.failedTrade.missingDepositTx=Falta la transacción de depósito (la transacción multifirma 2 de 2).\n\nSin esta tx, el intercambio no se puede completar. No se han bloqueado fondos, pero se ha pagado su tarifa comercial. Puede hacer una solicitud para que se le reembolse la tarifa comercial aquí: [HYPERLINK:https://github.com/haveno-dex/haveno/issues].\n\nSiéntase libre de mover esta operación a operaciones fallidas. +portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=Falta la transacción de pago demorado, pero los fondos se han bloqueado en la transacción de depósito.\n\nNO envíe el pago traditional o crypto al vendedor de XMR, porque sin el tx de pago demorado, no se puede abrir el arbitraje. En su lugar, abra un ticket de mediación con Cmd / Ctrl + o. El mediador debe sugerir que ambos pares recuperen el monto total de sus depósitos de seguridad (y el vendedor también recibirá el monto total de la operación). De esta manera, no hay riesgo en la seguridad y solo se pierden las tarifas comerciales.\n\nPuede solicitar un reembolso por las tarifas comerciales perdidas aquí: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]. +portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=Falta la transacción del pago demorado, pero los fondos se han bloqueado en la transacción de depósito.\n\nSi al comprador también le falta la transacción de pago demorado, se le indicará que NO envíe el pago y abra un ticket de mediación. También debe abrir un ticket de mediación con Cmd / Ctrl + o.\n\nSi el comprador aún no ha enviado el pago, el mediador debe sugerir que ambos pares recuperen el monto total de sus depósitos de seguridad (y el vendedor también recibirá el monto total de la operación). De lo contrario, el monto comercial debe ir al comprador.\n\nPuede solicitar un reembolso por las tarifas comerciales perdidas aquí: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]. portfolio.pending.failedTrade.errorMsgSet=Hubo un error durante la ejecución del protocolo de intercambio.\n\nError: {0}\n\nPuede ser que este error no sea crítico y que el intercambio se pueda completar normalmente. Si no está seguro, abra un ticket de mediación para obtener consejos de los mediadores de Haveno.\n\nSi el error fue crítico y la operación no se puede completar, es posible que haya perdido su tarifa de operación. Solicite un reembolso por las tarifas comerciales perdidas aquí: [HYPERLINK:ttps://github.com/bisq-network/support/issues]. -portfolio.pending.failedTrade.missingContract=El contrato del intercambio no está establecido.\n\nLa operación no se puede completar y es posible que haya perdido su tarifa de operación. Si es así, puede solicitar un reembolso por las tarifas comerciales perdidas aquí: [HYPERLINK:https://github.com/bisq-network/support/issues]. +portfolio.pending.failedTrade.missingContract=El contrato del intercambio no está establecido.\n\nLa operación no se puede completar y es posible que haya perdido su tarifa de operación. Si es así, puede solicitar un reembolso por las tarifas comerciales perdidas aquí: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]. portfolio.pending.failedTrade.info.popup=El protocolo de intercambio encontró algunos problemas.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=El protocolo de intercambio encontró un problema grave.\n\n{0}\n\n¿Quiere mover la operación a intercambios fallidos?\n\nNo puede abrir mediación o arbitraje desde la vista de operaciones fallidas, pero puede mover un intercambio fallido a la pantalla de intercambios abiertos en cualquier momento. portfolio.pending.failedTrade.txChainValid.moveToFailed=El protocolo de intercambio encontró algunos problemas.\n\n{0}\n\nLas transacciones del intercambio se han publicado y los fondos están bloqueados. Mueva la operación a operaciones fallidas solo si está realmente seguro. Podría impedir opciones para resolver el problema.\n\n¿Quiere mover la operación a operaciones fallidas?\n\nNo puede abrir mediación o el arbitraje desde la vista de intercambios fallidos, pero puede mover un intercambio fallido a la pantalla de intercambios abiertos en cualquier momento. diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index d6e00ed380..53bd2363ce 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -125,6 +125,7 @@ shared.noDateAvailable=تاریخ موجود نیست shared.noDetailsAvailable=جزئیاتی در دسترس نیست shared.notUsedYet=هنوز مورد استفاده قرار نگرفته shared.date=تاریخ +shared.sendFundsDetailsWithFee=ارسال: {0}\n\nبه آدرس گیرنده: {1}\n\nهزینه اضافی ماینر: {2}\n\nآیا مطمئن هستید که می‌خواهید این مبلغ را ارسال کنید؟ # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=کپی در کلیپ‌بورد @@ -614,7 +615,8 @@ portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. N # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. -portfolio.pending.step1.info=تراکنش سپرده منتشر شده است.\nباید برای حداقل یک تأییدیه بلاک چین قبل از آغاز پرداخت، {0} صبر کنید. +portfolio.pending.step1.info.you=تراکنش واریز منتشر شده است.\nشما باید منتظر 10 تاییدیه (حدود 20 دقیقه) باشید تا پرداخت آغاز شود. +portfolio.pending.step1.info.buyer=تراکنش واریز منتشر شده است.\nخریدار XMR باید منتظر ۱۰ تاییدیه (حدود ۲۰ دقیقه) باشد تا پرداخت آغاز شود. portfolio.pending.step1.warn=The deposit transaction is still not confirmed. This sometimes happens in rare cases when the funding fee of one trader from an external wallet was too low. portfolio.pending.step1.openForDispute=The deposit transaction is still not confirmed. You can wait longer or contact the mediator for assistance. @@ -817,11 +819,11 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/bisq-network/support/issues]\n\nFeel free to move this trade to failed trades. -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] +portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nFeel free to move this trade to failed trades. +portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encountered some problems.\n\n{0}\n\nThe trade transactions have been published and funds are locked. Only move the trade to failed trades if you are really sure. It might prevent options to resolve the problem.\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index 2025ed4d66..900e829baa 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -125,6 +125,7 @@ shared.noDateAvailable=Pas de date disponible shared.noDetailsAvailable=Pas de détails disponibles shared.notUsedYet=Pas encore utilisé shared.date=Date +shared.sendFundsDetailsWithFee=Envoyer : {0}\n\nÀ l'adresse de réception : {1}\n\nFrais supplémentaires pour le mineur : {2}\n\nÊtes-vous sûr de vouloir envoyer ce montant ? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno détecte que la transaction produira une sortie inférieure au seuil de fraction minimum (non autorisé par les règles de consensus Monero). Au lieu de cela, ces fractions ({0} satoshi {1}) seront ajoutées aux frais de traitement minier.\n\n\n shared.copyToClipboard=Copier dans le presse-papiers @@ -615,7 +616,8 @@ portfolio.pending.autoConf.state.ERROR=Une erreur lors de la demande du service # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=Un service a retourné un échec. L'auto-confirmation n'est pas possible. -portfolio.pending.step1.info=La transaction de dépôt à été publiée.\n{0} devez attendre au moins une confirmation de la blockchain avant d''initier le paiement. +portfolio.pending.step1.info.you=La transaction de dépôt a été publiée.\nVous devez attendre 10 confirmations (environ 20 minutes) avant que le paiement ne puisse commencer. +portfolio.pending.step1.info.buyer=La transaction de dépôt a été publiée.\nL'acheteur XMR doit attendre 10 confirmations (environ 20 minutes) avant que le paiement puisse commencer. portfolio.pending.step1.warn=La transaction de dépôt n'est toujours pas confirmée. Cela se produit parfois dans de rares occasions lorsque les frais de financement d'un trader en provenance d'un portefeuille externe sont trop bas. portfolio.pending.step1.openForDispute=La transaction de dépôt n'est toujours pas confirmée. Vous pouvez attendre plus longtemps ou contacter le médiateur pour obtenir de l'aide. @@ -819,11 +821,11 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=Vous avez déjà accept portfolio.pending.failedTrade.taker.missingTakerFeeTx=Le frais de transaction du preneur est manquant.\n\nSans ce tx, le trade ne peut être complété. Aucun fonds ont été verrouillés et aucun frais de trade a été payé. Vous pouvez déplacer ce trade vers les trade échoués. portfolio.pending.failedTrade.maker.missingTakerFeeTx=Le frais de transaction du pair preneur est manquant.\n\nSans ce tx, le trade ne peut être complété. Aucun fonds ont été verrouillés. Votre offre est toujours valable pour les autres traders, vous n'avez donc pas perdu le frais de maker. Vous pouvez déplacer ce trade vers les trades échoués. -portfolio.pending.failedTrade.missingDepositTx=Cette transaction de marge (transaction multi-signature de 2 à 2) est manquante.\n\nSans ce tx, la transaction ne peut pas être complétée. Aucun fonds n'est bloqué, mais vos frais de transaction sont toujours payés. Vous pouvez lancer une demande de compensation des frais de transaction ici: [HYPERLINK:https://github.com/bisq-network/support/issues] \nN'hésitez pas à déplacer la transaction vers la transaction échouée. -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=La transaction de paiement différée est manquante, mais les fonds ont été verrouillés dans la transaction de dépôt.\n\nVeuillez NE PAS envoyer de Fiat ou d'crypto au vendeur de XMR, car avec le tx de paiement différé, le jugemenbt ne peut être ouvert. À la place, ouvrez un ticket de médiation avec Cmd/Ctrl+O. Le médiateur devrait suggérer que les deux pair reçoivent tous les deux le montant total de leurs dépôts de sécurité (le vendeur aussi doit reçevoir le montant total du trade). De cette manière, il n'y a pas de risque de non sécurité, et seuls les frais du trade sont perdus.\n\nVous pouvez demander le remboursement des frais de trade perdus ici;\n[LIEN:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=La transaction de paiement différée est manquante, mais les fonds ont été verrouillés dans la transaction de dépôt.\n\nSi l'acheteur n'a pas non plus la transaction de paiement différée, il sera informé du fait de ne PAS envoyer le paiement et d'ouvrir un ticket de médiation à la place. Vous devriez aussi ouvrir un ticket de médiation avec Cmd/Ctrl+o.\n\nSi l'acheteur n'a pas encore envoyé le paiement, le médiateur devrait suggérer que les deux pairs reçoivent le montant total de leurs dépôts de sécurité (le vendeur doit aussi reçevoir le montant total du trade). Sinon, le montant du trade revient à l'acheteur.\n\nVous pouvez effectuer une demande de remboursement pour les frais de trade perdus ici: [LIEN:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.errorMsgSet=Il y'a eu une erreur durant l'exécution du protocole de trade.\n\nErreur: {0}\n\nIl est possible que cette erreur ne soit pas critique, et que le trade puisse être complété normalement. Si vous n'en êtes pas sûr, ouvrez un ticket de médiation pour avoir des conseils de la part des médiateurs de Haveno.\n\nSi cette erreur est critique et que le trade ne peut être complété, il est possible que vous ayez perdu le frais du trade. Effectuez une demande de remboursement ici: [LIEN:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=Le contrat de trade n'est pas complété.\n\nCe trade ne peut être complété et il est possible que vous ayiez perdu votre frais de trade. Dans ce cas, vous pouvez demander un remboursement des frais de trade perdus ici: [LIEN:https://github.com/bisq-network/support/issues] +portfolio.pending.failedTrade.missingDepositTx=Cette transaction de marge (transaction multi-signature de 2 à 2) est manquante.\n\nSans ce tx, la transaction ne peut pas être complétée. Aucun fonds n'est bloqué, mais vos frais de transaction sont toujours payés. Vous pouvez lancer une demande de compensation des frais de transaction ici: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] \nN'hésitez pas à déplacer la transaction vers la transaction échouée. +portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=La transaction de paiement différée est manquante, mais les fonds ont été verrouillés dans la transaction de dépôt.\n\nVeuillez NE PAS envoyer de Fiat ou d'crypto au vendeur de XMR, car avec le tx de paiement différé, le jugemenbt ne peut être ouvert. À la place, ouvrez un ticket de médiation avec Cmd/Ctrl+O. Le médiateur devrait suggérer que les deux pair reçoivent tous les deux le montant total de leurs dépôts de sécurité (le vendeur aussi doit reçevoir le montant total du trade). De cette manière, il n'y a pas de risque de non sécurité, et seuls les frais du trade sont perdus.\n\nVous pouvez demander le remboursement des frais de trade perdus ici;\n[LIEN:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=La transaction de paiement différée est manquante, mais les fonds ont été verrouillés dans la transaction de dépôt.\n\nSi l'acheteur n'a pas non plus la transaction de paiement différée, il sera informé du fait de ne PAS envoyer le paiement et d'ouvrir un ticket de médiation à la place. Vous devriez aussi ouvrir un ticket de médiation avec Cmd/Ctrl+o.\n\nSi l'acheteur n'a pas encore envoyé le paiement, le médiateur devrait suggérer que les deux pairs reçoivent le montant total de leurs dépôts de sécurité (le vendeur doit aussi reçevoir le montant total du trade). Sinon, le montant du trade revient à l'acheteur.\n\nVous pouvez effectuer une demande de remboursement pour les frais de trade perdus ici: [LIEN:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.errorMsgSet=Il y'a eu une erreur durant l'exécution du protocole de trade.\n\nErreur: {0}\n\nIl est possible que cette erreur ne soit pas critique, et que le trade puisse être complété normalement. Si vous n'en êtes pas sûr, ouvrez un ticket de médiation pour avoir des conseils de la part des médiateurs de Haveno.\n\nSi cette erreur est critique et que le trade ne peut être complété, il est possible que vous ayez perdu le frais du trade. Effectuez une demande de remboursement ici: [LIEN:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.missingContract=Le contrat de trade n'est pas complété.\n\nCe trade ne peut être complété et il est possible que vous ayiez perdu votre frais de trade. Dans ce cas, vous pouvez demander un remboursement des frais de trade perdus ici: [LIEN:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=Le protocole de trade a rencontré quelques problèmes/\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=Le protocole de trade a rencontré un problème critique.\n\n{0}\n\nVoulez-vous déplacer ce trade vers les trades échoués?\n\nVous ne pouvez pas ouvrir de médiations ou de jugements depuis la liste des trades échoués, mais vous pouvez redéplacer un trade échoué vers l'écran des trades ouverts quand vous le souhaitez. portfolio.pending.failedTrade.txChainValid.moveToFailed=Il y a des problèmes avec cet accord de transaction. \n\n{0}\n\nLa transaction de devis a été validée et les fonds ont été bloqués. Déplacer la transaction vers une transaction échouée uniquement si elle est certaine. Cela peut empêcher les options disponibles pour résoudre le problème. \n\nÊtes-vous sûr de vouloir déplacer cette transaction vers la transaction échouée? \n\nVous ne pouvez pas ouvrir une médiation ou un arbitrage dans une transaction échouée, mais vous pouvez déplacer une transaction échouée vers la transaction incomplète à tout moment. diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index f482f5b9c7..cbc56c5f5d 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -125,6 +125,7 @@ shared.noDateAvailable=Nessuna data disponibile shared.noDetailsAvailable=Dettagli non disponibili shared.notUsedYet=Non ancora usato shared.date=Data +shared.sendFundsDetailsWithFee=Invio: {0}\n\nAll'indirizzo di ricezione: {1}\n\nCommissione mineraria aggiuntiva: {2}\n\nSei sicuro di voler inviare questa somma? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=Copia negli appunti @@ -614,7 +615,8 @@ portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. N # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. -portfolio.pending.step1.info=La transazione di deposito è stata pubblicata.\n {0} deve attendere almeno una conferma dalla blockchain prima di avviare il pagamento. +portfolio.pending.step1.info.you=La transazione di deposito è stata pubblicata.\nDevi aspettare 10 conferme (circa 20 minuti) prima che il pagamento possa iniziare. +portfolio.pending.step1.info.buyer=La transazione di deposito è stata pubblicata.\nL'acquirente XMR deve aspettare 10 conferme (circa 20 minuti) prima che il pagamento possa iniziare. portfolio.pending.step1.warn=La transazione di deposito non è ancora confermata. Questo accade raramente e nel caso in cui la commissione di transazione di un trader proveniente da un portafoglio esterno è troppo bassa. portfolio.pending.step1.openForDispute=La transazione di deposito non è ancora confermata. Puoi attendere più a lungo o contattare il mediatore per ricevere assistenza. @@ -817,11 +819,11 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=Hai già accettato portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/bisq-network/support/issues]\n\nFeel free to move this trade to failed trades. -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] +portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nFeel free to move this trade to failed trades. +portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encountered some problems.\n\n{0}\n\nThe trade transactions have been published and funds are locked. Only move the trade to failed trades if you are really sure. It might prevent options to resolve the problem.\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index 83f078a7c3..c1d2281373 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -125,6 +125,7 @@ shared.noDateAvailable=日付がありません shared.noDetailsAvailable=詳細不明 shared.notUsedYet=未使用 shared.date=日付 +shared.sendFundsDetailsWithFee=送信中: {0}\n\n受取アドレス: {1}\n\n追加のマイナー手数料: {2}\n\nこの金額を送信してもよろしいですか? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Havenoがこのトランザクションはダストの最小閾値以下のおつりアウトプットを生じることを検出しました(それにしたがって、ビットコインのコンセンサス・ルールによって許されない)。代わりに、その ({0} satoshi{1}) のダストはマイニング手数料に追加されます。\n\n\n shared.copyToClipboard=クリップボードにコピー @@ -615,7 +616,8 @@ portfolio.pending.autoConf.state.ERROR=サービスリクエストにはエラ # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=サービスは失敗を返しました。自動確認できません。 -portfolio.pending.step1.info=デポジットトランザクションが発行されました。\n{0}は、支払いを開始する前に少なくとも1つのブロックチェーンの承認を待つ必要があります。 +portfolio.pending.step1.info.you=入金トランザクションが公開されました。\n支払いが開始されるまで、10回の確認(約20分)を待つ必要があります。 +portfolio.pending.step1.info.buyer=入金トランザクションが公開されました。\nXMRの購入者は、支払いを開始する前に10回の確認(約20分)を待つ必要があります。 portfolio.pending.step1.warn=デポジットトランザクションがまだ承認されていません。外部ウォレットからの取引者の資金調達手数料が低すぎるときには、例外的なケースで起こるかもしれません。 portfolio.pending.step1.openForDispute=デポジットトランザクションがまだ承認されていません。もう少し待つか、助けを求めて調停人に連絡できます。 @@ -818,11 +820,11 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=すでに受け入れて portfolio.pending.failedTrade.taker.missingTakerFeeTx=欠測テイカー手数料のトランザクション。\n\nこのtxがなければ、トレードを完了できません。資金はロックされず、トレード手数料は支払いませんでした。「失敗トレード」へ送ることができます。 portfolio.pending.failedTrade.maker.missingTakerFeeTx=ピアのテイカー手数料のトランザクションは欠測します。\n\nこのtxがなければ、トレードを完了できません。資金はロックされませんでした。あなたのオファーがまだ他の取引者には有効ですので、メイカー手数料は失っていません。このトレードを「失敗トレード」へ送ることができます。 -portfolio.pending.failedTrade.missingDepositTx=入金トランザクション(2-of-2マルチシグトランザクション)は欠測します。\n\nこのtxがなければ、トレードを完了できません。資金はロックされませんでしたが、トレード手数料は支払いました。トレード手数料の返済要求はここから提出できます: [HYPERLINK:https://github.com/bisq-network/support/issues]\n\nこのトレードを「失敗トレード」へ送れます。 -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=遅延支払いトランザクションは欠測しますが、資金は入金トランザクションにロックされました。\n\nこの法定通貨・アルトコイン支払いをXMR売り手に送信しないで下さい。遅延支払いtxがなければ、係争仲裁は開始されることができません。代りに、「Cmd/Ctrl+o」で調停チケットをオープンして下さい。調停者はおそらく両方のピアへセキュリティデポジットの全額を払い戻しを提案します(売り手はトレード金額も払い戻しを受ける)。このような方法でセキュリティーのリスクがなし、トレード手数料のみが失われます。\n\n失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=遅延支払いトランザクションは欠測しますが、資金は入金トランザクションにロックされました。\n\n買い手の遅延支払いトランザクションが同じく欠測される場合、相手は支払いを送信せず調停チケットをオープンするように指示されます。同様に「Cmd/Ctrl+o」で調停チケットをオープンするのは賢明でしょう。\n\n買い手はまだ支払いを送信しなかった場合、調停者はおそらく両方のピアへセキュリティデポジットの全額を払い戻しを提案します(売り手はトレード金額も払い戻しを受ける)。さもなければ、トレード金額は買い手に支払われるでしょう。\n\n失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.errorMsgSet=トレードプロトコルの実行にはエラーが生じました。\n\nエラー: {0}\n\nクリティカル・エラーではない可能性はあり、トレードは普通に完了できるかもしれない。迷う場合は調停チケットをオープンして、Haveno調停者からアドバイスを受けることができます。\n\nクリティカル・エラーでトレードが完了できなかった場合はトレード手数料は失われた可能性があります。失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=トレード契約書は設定されません。\n\nトレードは完了できません。トレード手数料は失われた可能性もあります。その場合は失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/bisq-network/support/issues] +portfolio.pending.failedTrade.missingDepositTx=入金トランザクション(2-of-2マルチシグトランザクション)は欠測します。\n\nこのtxがなければ、トレードを完了できません。資金はロックされませんでしたが、トレード手数料は支払いました。トレード手数料の返済要求はここから提出できます: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nこのトレードを「失敗トレード」へ送れます。 +portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=遅延支払いトランザクションは欠測しますが、資金は入金トランザクションにロックされました。\n\nこの法定通貨・アルトコイン支払いをXMR売り手に送信しないで下さい。遅延支払いtxがなければ、係争仲裁は開始されることができません。代りに、「Cmd/Ctrl+o」で調停チケットをオープンして下さい。調停者はおそらく両方のピアへセキュリティデポジットの全額を払い戻しを提案します(売り手はトレード金額も払い戻しを受ける)。このような方法でセキュリティーのリスクがなし、トレード手数料のみが失われます。\n\n失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=遅延支払いトランザクションは欠測しますが、資金は入金トランザクションにロックされました。\n\n買い手の遅延支払いトランザクションが同じく欠測される場合、相手は支払いを送信せず調停チケットをオープンするように指示されます。同様に「Cmd/Ctrl+o」で調停チケットをオープンするのは賢明でしょう。\n\n買い手はまだ支払いを送信しなかった場合、調停者はおそらく両方のピアへセキュリティデポジットの全額を払い戻しを提案します(売り手はトレード金額も払い戻しを受ける)。さもなければ、トレード金額は買い手に支払われるでしょう。\n\n失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.errorMsgSet=トレードプロトコルの実行にはエラーが生じました。\n\nエラー: {0}\n\nクリティカル・エラーではない可能性はあり、トレードは普通に完了できるかもしれない。迷う場合は調停チケットをオープンして、Haveno調停者からアドバイスを受けることができます。\n\nクリティカル・エラーでトレードが完了できなかった場合はトレード手数料は失われた可能性があります。失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.missingContract=トレード契約書は設定されません。\n\nトレードは完了できません。トレード手数料は失われた可能性もあります。その場合は失われたトレード手数料の払い戻し要求はここから提出できます: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=トレードプロトコルは問題に遭遇しました。\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=トレードプロトコルは深刻な問題に遭遇しました。\n\n{0}\n\nトレードを「失敗トレード」へ送りますか?\n\n「失敗トレード」画面から調停・仲裁を開始できませんけど、失敗トレードがいつでも「オープントレード」へ戻されることができます。 portfolio.pending.failedTrade.txChainValid.moveToFailed=トレードプロトコルは問題に遭遇しました。\n\n{0}\n\nトレードのトランザクションは公開され、資金はロックされました。絶対に確信している場合のみにトレードを「失敗トレード」へ送りましょう。問題を解決できる選択肢に邪魔する可能性はあります。\n\nトレードを「失敗トレード」へ送りますか?\n\n「失敗トレード」画面から調停・仲裁を開始できませんけど、失敗トレードがいつでも「オープントレード」へ戻されることができます。 diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index 767e0b8999..d7463aacb4 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -125,6 +125,7 @@ shared.noDateAvailable=Sem data disponível shared.noDetailsAvailable=Sem detalhes disponíveis shared.notUsedYet=Ainda não usado shared.date=Data +shared.sendFundsDetailsWithFee=Enviando: {0}\n\nPara o endereço de recebimento: {1}\n\nTaxa adicional do minerador: {2}\n\nTem certeza de que deseja enviar esse valor? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=Copiar para área de transferência @@ -617,7 +618,8 @@ portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. N # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. -portfolio.pending.step1.info=A transação de depósito foi publicada\n{0} precisa esperar ao menos uma confirmação da blockchain antes de iniciar o pagamento. +portfolio.pending.step1.info.you=A transação de depósito foi publicada.\nVocê precisa aguardar 10 confirmações (cerca de 20 minutos) antes que o pagamento possa começar. +portfolio.pending.step1.info.buyer=A transação de depósito foi publicada.\nO comprador de XMR precisa aguardar 10 confirmações (cerca de 20 minutos) antes que o pagamento possa ser iniciado. portfolio.pending.step1.warn=A transação do depósito ainda não foi confirmada.\nIsto pode ocorrer em casos raros em que a taxa de financiamento de um dos negociadores enviada a partir de uma carteira externa foi muito baixa. portfolio.pending.step1.openForDispute=A transação de depósito ainda não foi confirmada. Você pode aguardar um pouco mais ou entrar em contato com o mediador para pedir assistência. @@ -820,11 +822,11 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=Você já aceitou portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/bisq-network/support/issues]\n\nFeel free to move this trade to failed trades. -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] +portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nFeel free to move this trade to failed trades. +portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encountered some problems.\n\n{0}\n\nThe trade transactions have been published and funds are locked. Only move the trade to failed trades if you are really sure. It might prevent options to resolve the problem.\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index cba08c5769..b6456daf42 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -125,6 +125,7 @@ shared.noDateAvailable=Sem dada disponível shared.noDetailsAvailable=Sem detalhes disponíveis shared.notUsedYet=Ainda não usado shared.date=Data +shared.sendFundsDetailsWithFee=Enviando: {0}\n\nPara o endereço de recebimento: {1}\n\nTaxa adicional do minerador: {2}\n\nTem certeza de que deseja enviar este valor? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=Copiar para área de transferência @@ -614,7 +615,8 @@ portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. N # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. -portfolio.pending.step1.info=A transação de depósito foi publicada.\n{0} precisa aguardar pelo menos uma confirmação da blockchain antes de iniciar o pagamento. +portfolio.pending.step1.info.you=A transação de depósito foi publicada.\nVocê precisa aguardar 10 confirmações (cerca de 20 minutos) antes que o pagamento possa começar. +portfolio.pending.step1.info.buyer=A transação de depósito foi publicada.\nO comprador de XMR precisa aguardar 10 confirmações (cerca de 20 minutos) antes que o pagamento possa ser iniciado. portfolio.pending.step1.warn=A transação de depósito ainda não foi confirmada. Isso pode acontecer em casos raros, quando a taxa de financiamento de um negociador proveniente de uma carteira externa foi muito baixa. portfolio.pending.step1.openForDispute=A transação de depósito ainda não foi confirmada. Você pode esperar mais tempo ou entrar em contato com o mediador para obter assistência. @@ -817,11 +819,11 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=Você já aceitou portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/bisq-network/support/issues]\n\nFeel free to move this trade to failed trades. -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] +portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nFeel free to move this trade to failed trades. +portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encountered some problems.\n\n{0}\n\nThe trade transactions have been published and funds are locked. Only move the trade to failed trades if you are really sure. It might prevent options to resolve the problem.\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index 956e124eae..3c66af61fe 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -125,6 +125,7 @@ shared.noDateAvailable=Дата не указана shared.noDetailsAvailable=Подробности не указаны shared.notUsedYet=Ещё не использовано shared.date=Дата +shared.sendFundsDetailsWithFee=Отправка: {0}\n\nНа получающий адрес: {1}\n\nДополнительная комиссия майнера: {2}\n\nВы уверены, что хотите отправить эту сумму? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=Скопировать в буфер @@ -614,7 +615,8 @@ portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. N # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. -portfolio.pending.step1.info=Депозитная транзакция опубликована.\n{0} должен дождаться хотя бы одного подтверждения в блокчейне перед началом платежа. +portfolio.pending.step1.info.you=Транзакция депозита была опубликована.\nВам нужно дождаться 10 подтверждений (около 20 минут), прежде чем платеж сможет начаться. +portfolio.pending.step1.info.buyer=Транзакция депозита была опубликована.\nПокупатель XMR должен подождать 10 подтверждений (около 20 минут), прежде чем платеж может быть начат. portfolio.pending.step1.warn=The deposit transaction is still not confirmed. This sometimes happens in rare cases when the funding fee of one trader from an external wallet was too low. portfolio.pending.step1.openForDispute=The deposit transaction is still not confirmed. You can wait longer or contact the mediator for assistance. @@ -817,11 +819,11 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/bisq-network/support/issues]\n\nFeel free to move this trade to failed trades. -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] +portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nFeel free to move this trade to failed trades. +portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encountered some problems.\n\n{0}\n\nThe trade transactions have been published and funds are locked. Only move the trade to failed trades if you are really sure. It might prevent options to resolve the problem.\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index 5205776fe2..8b675222fb 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -125,6 +125,7 @@ shared.noDateAvailable=ไม่มีวันที่ให้แสดง shared.noDetailsAvailable=ไม่มีรายละเอียด shared.notUsedYet=ยังไม่ได้ใช้งาน shared.date=วันที่ +shared.sendFundsDetailsWithFee=กำลังส่ง: {0}\n\nไปยังที่อยู่ผู้รับ: {1}\n\nค่าธรรมเนียมเหมืองเพิ่มเติม: {2}\n\nคุณแน่ใจหรือไม่ว่าต้องการส่งจำนวนนี้? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=คัดลอกไปที่คลิปบอร์ด @@ -614,7 +615,8 @@ portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. N # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. -portfolio.pending.step1.info=ธุรกรรมเงินฝากได้รับการเผยแพร่แล้ว\n{0} ต้องรอการยืนยันของบล็อกเชนอย่างน้อยหนึ่งครั้งก่อนที่จะเริ่มการชำระเงิน +portfolio.pending.step1.info.you=ธุรกรรมการฝากเงินได้รับการเผยแพร่แล้ว\nคุณต้องรอ 10 คอนเฟิร์ม (ประมาณ 20 นาที) ก่อนที่การชำระเงินจะเริ่มต้น +portfolio.pending.step1.info.buyer=การทำธุรกรรมฝากเงินได้รับการเผยแพร่แล้ว。\nผู้ซื้อ XMR จำเป็นต้องรอการยืนยัน 10 ครั้ง (ประมาณ 20 นาที) ก่อนที่การชำระเงินจะเริ่มต้นได้ portfolio.pending.step1.warn=The deposit transaction is still not confirmed. This sometimes happens in rare cases when the funding fee of one trader from an external wallet was too low. portfolio.pending.step1.openForDispute=The deposit transaction is still not confirmed. You can wait longer or contact the mediator for assistance. @@ -817,11 +819,11 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/bisq-network/support/issues]\n\nFeel free to move this trade to failed trades. -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] +portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nFeel free to move this trade to failed trades. +portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encountered some problems.\n\n{0}\n\nThe trade transactions have been published and funds are locked. Only move the trade to failed trades if you are really sure. It might prevent options to resolve the problem.\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. diff --git a/core/src/main/resources/i18n/displayStrings_tr.properties b/core/src/main/resources/i18n/displayStrings_tr.properties index 5d0720412b..eafef04dfc 100644 --- a/core/src/main/resources/i18n/displayStrings_tr.properties +++ b/core/src/main/resources/i18n/displayStrings_tr.properties @@ -134,7 +134,7 @@ shared.noDateAvailable=Geçerli tarih yok shared.noDetailsAvailable=Geçerli detay yok shared.notUsedYet=Henüz kullanılmadı shared.date=Tarih -shared.sendFundsDetailsWithFee=Gönderiliyor: {0}\nAlıcı adresine: {1}.\nGerekli madencilik ücreti: {2}\n\nAlıcı alacak: {3}\n\nBu miktarı çekmek istediğinizden emin misiniz? +shared.sendFundsDetailsWithFee=Gönderilen: {0}\n\nAlıcı adresi: {1}\n\nEk madenci ücreti: {2}\n\nBu tutarı göndermek istediğinizden emin misiniz? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno, bu işlemin minimum toz eşiğinin altında bir değişim çıktısı oluşturacağını (ve bu nedenle Monero konsensüs kuralları tarafından izin verilmediğini) tespit etti. Bunun yerine, bu toz ({0} satoshi{1}) madencilik ücretine eklenecektir.\n\n\n shared.copyToClipboard=Panoya kopyala @@ -669,7 +669,8 @@ portfolio.pending.autoConf.state.ERROR=Bir hizmet talebinde hata oluştu. Otomat # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=Bir hizmet başarısızlıkla sonuçlandı. Otomatik onay mümkün değil. -portfolio.pending.step1.info=Yatırım işlemi yayımlandı.\n{0} ödemeye başlamadan önce 10 onay (yaklaşık 20 dakika) beklemeniz gerekiyor. +portfolio.pending.step1.info.you=Depozito işlemi yayımlandı.\nÖdemenin başlayabilmesi için 10 onay beklemeniz gerekiyor (yaklaşık 20 dakika). +portfolio.pending.step1.info.buyer=Depozito işlemi yayınlandı.\nXMR alıcısının ödeme işlemine başlanmadan önce 10 onay beklemesi gerekiyor (yaklaşık 20 dakika). portfolio.pending.step1.warn=Yatırım işlemi henüz onaylanmadı. Bu genellikle yaklaşık 20 dakika sürer, ancak ağ yoğunsa daha uzun sürebilir. portfolio.pending.step1.openForDispute=Yatırım işlemi hala onaylanmadı. \ 20 dakikadan çok daha uzun süre beklediyseniz, Haveno desteği ile iletişime geçin. @@ -959,7 +960,7 @@ portfolio.pending.failedTrade.maker.missingTakerFeeTx=Karşı tarafın alıcı portfolio.pending.failedTrade.missingDepositTx=Para yatırma işlemi (2-of-2 multisig işlemi) eksik.\n\n\ Bu işlem olmadan, ticaret tamamlanamaz. Hiçbir fon kilitlenmedi ancak ticaret ücretiniz ödendi. \ Ticaret ücretinin geri ödenmesi için burada talepte bulunabilirsiniz: \ - [HYPERLINK:https://github.com/bisq-network/support/issues]\n\n\ + [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\n\ Bu ticareti başarısız ticaretler arasına taşımakta özgürsünüz. portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=Gecikmiş ödeme işlemi eksik, \ ancak fonlar depozito işleminde kilitlendi.\n\n\ @@ -969,7 +970,7 @@ portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=G (satıcı da tam ticaret miktarını geri alır). \ Bu şekilde, güvenlik riski yoktur ve yalnızca ticaret ücretleri kaybedilir. \n\n\ Kaybedilen ticaret ücretleri için burada geri ödeme talebinde bulunabilirsiniz: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=Gecikmiş ödeme işlemi eksik \ ancak fonlar depozito işleminde kilitlendi.\n\n\ Eğer alıcı da gecikmiş ödeme işlemini eksikse, onlara ödemeyi göndermemeleri ve \ @@ -978,18 +979,18 @@ portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx= tamamını geri almasını önermelidir (satıcı da tam ticaret miktarını geri alır). \ Aksi takdirde ticaret miktarı alıcıya gitmelidir. \n\n\ Kaybedilen ticaret ücretleri için burada geri ödeme talebinde bulunabilirsiniz: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.errorMsgSet=Ticaret protokolü yürütülürken bir hata oluştu.\n\n\ Hata: {0}\n\n\ Bu hata kritik olmayabilir ve ticaret normal şekilde tamamlanabilir. Emin değilseniz, \ Haveno arabulucularından tavsiye almak için bir arabuluculuk bileti açın. \n\n\ Eğer hata kritikse ve ticaret tamamlanamazsa, ticaret ücretinizi kaybetmiş olabilirsiniz. \ Kaybedilen ticaret ücretleri için burada geri ödeme talebinde bulunun: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.missingContract=Ticaret sözleşmesi ayarlanmadı.\n\n\ Ticaret tamamlanamaz ve ticaret ücretinizi kaybetmiş olabilirsiniz. \ Eğer öyleyse, kaybedilen ticaret ücretleri için burada geri ödeme talebinde bulunun: \ - [HYPERLINK:https://github.com/bisq-network/support/issues] + [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=Ticaret protokolü bazı sorunlarla karşılaştı.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=Ticaret protokolü ciddi bir sorunla karşılaştı.\n\n{0}\n\n\ Ticareti başarısız ticaretler arasına taşımak ister misiniz?\n\n\ diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index 4b6ba937e4..aeab1530a7 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -125,6 +125,7 @@ shared.noDateAvailable=Ngày tháng không hiển thị shared.noDetailsAvailable=Không có thông tin shared.notUsedYet=Chưa được sử dụng shared.date=Ngày +shared.sendFundsDetailsWithFee=Đang gửi: {0}\n\nĐến địa chỉ nhận: {1}\n\nPhí thợ đào bổ sung: {2}\n\nBạn có chắc chắn muốn gửi số tiền này không? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno detected that this transaction would create a change output which is below the minimum dust threshold (and therefore not allowed by Monero consensus rules). Instead, this dust ({0} satoshi{1}) will be added to the mining fee.\n\n\n shared.copyToClipboard=Sao chép đến clipboard @@ -614,7 +615,8 @@ portfolio.pending.autoConf.state.ERROR=An error at a service request occurred. N # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=A service returned with a failure. No auto-confirm possible. -portfolio.pending.step1.info=Giao dịch đặt cọc đã được công bố.\n{0} Bạn cần đợi ít nhất một xác nhận blockchain trước khi bắt đầu thanh toán. +portfolio.pending.step1.info.you=Giao dịch nạp tiền đã được công bố.\nBạn cần đợi 10 xác nhận (khoảng 20 phút) trước khi thanh toán có thể bắt đầu. +portfolio.pending.step1.info.buyer=Giao dịch gửi tiền đã được công bố.\nNgười mua XMR cần chờ 10 xác nhận (khoảng 20 phút) trước khi thanh toán có thể bắt đầu. portfolio.pending.step1.warn=The deposit transaction is still not confirmed. This sometimes happens in rare cases when the funding fee of one trader from an external wallet was too low. portfolio.pending.step1.openForDispute=The deposit transaction is still not confirmed. You can wait longer or contact the mediator for assistance. @@ -817,11 +819,11 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked and no trade fee has been paid. You can move this trade to failed trades. portfolio.pending.failedTrade.maker.missingTakerFeeTx=The peer's taker fee transaction is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked. Your offer is still available to other traders, so you have not lost the maker fee. You can move this trade to failed trades. -portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/bisq-network/support/issues]\n\nFeel free to move this trade to failed trades. -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] -portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/bisq-network/support/issues] +portfolio.pending.failedTrade.missingDepositTx=The deposit transaction (the 2-of-2 multisig transaction) is missing.\n\nWithout this tx, the trade cannot be completed. No funds have been locked but your trade fee has been paid. You can make a request to be reimbursed the trade fee here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues]\n\nFeel free to move this trade to failed trades. +portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing, but funds have been locked in the deposit transaction.\n\nPlease do NOT send the traditional or crypto payment to the XMR seller, because without the delayed payout tx, arbitration cannot be opened. Instead, open a mediation ticket with Cmd/Ctrl+o. The mediator should suggest that both peers each get back the the full amount of their security deposits (with seller receiving full trade amount back as well). This way, there is no security risk, and only trade fees are lost. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=The delayed payout transaction is missing but funds have been locked in the deposit transaction.\n\nIf the buyer is also missing the delayed payout transaction, they will be instructed to NOT send the payment and open a mediation ticket instead. You should also open a mediation ticket with Cmd/Ctrl+o. \n\nIf the buyer has not sent payment yet, the mediator should suggest that both peers each get back the full amount of their security deposits (with seller receiving full trade amount back as well). Otherwise the trade amount should go to the buyer. \n\nYou can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.errorMsgSet=There was an error during trade protocol execution.\n\nError: {0}\n\nIt might be that this error is not critical, and the trade can be completed normally. If you are unsure, open a mediation ticket to get advice from Haveno mediators. \n\nIf the error was critical and the trade cannot be completed, you might have lost your trade fee. Request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] +portfolio.pending.failedTrade.missingContract=The trade contract is not set.\n\nThe trade cannot be completed and you might have lost your trade fee. If so, you can request a reimbursement for lost trade fees here: [HYPERLINK:https://github.com/haveno-dex/haveno/issues] portfolio.pending.failedTrade.info.popup=The trade protocol encountered some problems.\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=The trade protocol encountered a serious problem.\n\n{0}\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. portfolio.pending.failedTrade.txChainValid.moveToFailed=The trade protocol encountered some problems.\n\n{0}\n\nThe trade transactions have been published and funds are locked. Only move the trade to failed trades if you are really sure. It might prevent options to resolve the problem.\n\nDo you want to move the trade to failed trades?\n\nYou cannot open mediation or arbitration from the failed trades view, but you can move a failed trade back to the open trades screen any time. diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index b9cd9a21c5..a783f387e6 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -125,6 +125,7 @@ shared.noDateAvailable=没有可用数据 shared.noDetailsAvailable=没有可用详细 shared.notUsedYet=尚未使用 shared.date=日期 +shared.sendFundsDetailsWithFee=TOD发送:{0}\n\n接收地址:{1}\n\n额外矿工费:{2}\n\n您确定要发送此金额吗?O # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno 检测到,该交易将产生一个低于最低零头阈值的输出(不被比特币共识规则所允许)。相反,这些零头({0}satoshi{1})将被添加到挖矿手续费中。 shared.copyToClipboard=复制到剪贴板 @@ -615,7 +616,8 @@ portfolio.pending.autoConf.state.ERROR=您请求的服务发生了错误。没 # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=服务返回失败。没有自动确认。 -portfolio.pending.step1.info=存款交易已经发布。\n开始付款之前,{0} 需要等待至少一个区块链确认。 +portfolio.pending.step1.info.you=存款交易已发布。\n您需要等待 10 次确认(约 20 分钟)后付款才能开始。 +portfolio.pending.step1.info.buyer=存款交易已发布。\nXMR 买家需要等待 10 次确认(大约 20 分钟),然后才能开始付款。 portfolio.pending.step1.warn=保证金交易仍未得到确认。这种情况可能会发生在外部钱包转账时使用的交易手续费用较低造成的。 portfolio.pending.step1.openForDispute=保证金交易仍未得到确认。请联系调解员协助。 @@ -818,11 +820,11 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=您已经接受了。 portfolio.pending.failedTrade.taker.missingTakerFeeTx=吃单交易费未找到。\n\n如果没有 tx,交易不能完成。没有资金被锁定以及没有支付交易费用。你可以将交易移至失败的交易。 portfolio.pending.failedTrade.maker.missingTakerFeeTx=挂单费交易未找到。\n\n如果没有 tx,交易不能完成。没有资金被锁定以及没有支付交易费用。你可以将交易移至失败的交易。 -portfolio.pending.failedTrade.missingDepositTx=这个保证金交易(2 对 2 多重签名交易)缺失\n\n没有该 tx,交易不能完成。没有资金被锁定但是您的交易手续费仍然已支出。您可以发起一个请求去赔偿改交易手续费在这里:https://github.com/bisq-network/support/issues\n\n请随意的将该交易移至失败交易 -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=延迟支付交易缺失,但是资金仍然被锁定在保证金交易中。\n\n请不要给比特币卖家发送法币或数字货币,因为没有延迟交易 tx,不能开启仲裁。使用 Cmd/Ctrl+o开启调解协助。调解员应该建议交易双方分别退回全部的保证金(卖方支付的交易金额也会全数返还)。这样的话不会有任何的安全问题只会损失交易手续费。\n\n你可以在这里为失败的交易提出赔偿要求:https://github.com/bisq-network/support/issues -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=延迟支付交易确实但是资金仍然被锁定在保证金交易中。\n\n如果卖家仍然缺失延迟支付交易,他会接到请勿付款的指示并开启一个调节帮助。你也应该使用 Cmd/Ctrl+O 去打开一个调节协助\n\n如果买家还没有发送付款,调解员应该会建议交易双方分别退回全部的保证金(卖方支付的交易金额也会全数返还)。否则交易额应该判给买方。\n\n你可以在这里为失败的交易提出赔偿要求:https://github.com/bisq-network/support/issues -portfolio.pending.failedTrade.errorMsgSet=在处理交易协议是发生了一个错误\n\n错误:{0}\n\n这应该不是致命错误,您可以正常的完成交易。如果你仍担忧,打开一个调解协助并从 Haveno 调解员处得到建议。\n\n如果这个错误是致命的那么这个交易就无法完成,你可能会损失交易费。可以在这里为失败的交易提出赔偿要求:https://github.com/bisq-network/support/issues -portfolio.pending.failedTrade.missingContract=没有设置交易合同。\n\n这个交易无法完成,你可能会损失交易手续费。可以在这里为失败的交易提出赔偿要求:https://github.com/bisq-network/support/issues +portfolio.pending.failedTrade.missingDepositTx=这个保证金交易(2 对 2 多重签名交易)缺失\n\n没有该 tx,交易不能完成。没有资金被锁定但是您的交易手续费仍然已支出。您可以发起一个请求去赔偿改交易手续费在这里:https://github.com/haveno-dex/haveno/issues\n\n请随意的将该交易移至失败交易 +portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=延迟支付交易缺失,但是资金仍然被锁定在保证金交易中。\n\n请不要给比特币卖家发送法币或数字货币,因为没有延迟交易 tx,不能开启仲裁。使用 Cmd/Ctrl+o开启调解协助。调解员应该建议交易双方分别退回全部的保证金(卖方支付的交易金额也会全数返还)。这样的话不会有任何的安全问题只会损失交易手续费。\n\n你可以在这里为失败的交易提出赔偿要求:https://github.com/haveno-dex/haveno/issues +portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=延迟支付交易确实但是资金仍然被锁定在保证金交易中。\n\n如果卖家仍然缺失延迟支付交易,他会接到请勿付款的指示并开启一个调节帮助。你也应该使用 Cmd/Ctrl+O 去打开一个调节协助\n\n如果买家还没有发送付款,调解员应该会建议交易双方分别退回全部的保证金(卖方支付的交易金额也会全数返还)。否则交易额应该判给买方。\n\n你可以在这里为失败的交易提出赔偿要求:https://github.com/haveno-dex/haveno/issues +portfolio.pending.failedTrade.errorMsgSet=在处理交易协议是发生了一个错误\n\n错误:{0}\n\n这应该不是致命错误,您可以正常的完成交易。如果你仍担忧,打开一个调解协助并从 Haveno 调解员处得到建议。\n\n如果这个错误是致命的那么这个交易就无法完成,你可能会损失交易费。可以在这里为失败的交易提出赔偿要求:https://github.com/haveno-dex/haveno/issues +portfolio.pending.failedTrade.missingContract=没有设置交易合同。\n\n这个交易无法完成,你可能会损失交易手续费。可以在这里为失败的交易提出赔偿要求:https://github.com/haveno-dex/haveno/issues portfolio.pending.failedTrade.info.popup=交易协议出现了问题。\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=交易协议出现了严重问题。\n\n{0}\n\n您确定想要将该交易移至失败的交易吗?\n\n您不能在失败的交易中打开一个调解或仲裁,但是你随时可以将失败的交易重新移至未完成交易。 portfolio.pending.failedTrade.txChainValid.moveToFailed=这个交易协议存在一些问题。\n\n{0}\n\n这个报价交易已经被发布以及资金已被锁定。只有在确定情况下将该交易移至失败交易。这可能会阻止解决问题的可用选项。\n\n您确定想要将该交易移至失败的交易吗?\n\n您不能在失败的交易中打开一个调解或仲裁,但是你随时可以将失败的交易重新移至未完成交易。 diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index e5bf5ec76b..e3344d1fea 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -125,6 +125,7 @@ shared.noDateAvailable=沒有可用數據 shared.noDetailsAvailable=沒有可用詳細 shared.notUsedYet=尚未使用 shared.date=日期 +shared.sendFundsDetailsWithFee=發送中:{0}\n\n至接收地址:{1}\n\n額外礦工費:{2}\n\n您確定要發送此金額嗎? # suppress inspection "TrailingSpacesInProperty" shared.sendFundsDetailsDust=Haveno 檢測到,該交易將產生一個低於最低零頭閾值的輸出(不被比特幣共識規則所允許)。相反,這些零頭({0}satoshi{1})將被添加到挖礦手續費中。 shared.copyToClipboard=複製到剪貼板 @@ -615,7 +616,8 @@ portfolio.pending.autoConf.state.ERROR=您請求的服務發生了錯誤。沒 # suppress inspection "UnusedProperty" portfolio.pending.autoConf.state.FAILED=服務返回失敗。沒有自動確認。 -portfolio.pending.step1.info=存款交易已經發布。\n開始付款之前,{0} 需要等待至少一個區塊鏈確認。 +portfolio.pending.step1.info.you=存款交易已發布。\n您需要等待 10 次確認(約 20 分鐘)後,付款才能開始。 +portfolio.pending.step1.info.buyer=存款交易已發佈。\nXMR 購買者需要等待 10 次確認(大約 20 分鐘)才能開始付款。 portfolio.pending.step1.warn=保證金交易仍未得到確認。這種情況可能會發生在外部錢包轉賬時使用的交易手續費用較低造成的。 portfolio.pending.step1.openForDispute=保證金交易仍未得到確認。請聯繫調解員協助。 @@ -818,11 +820,11 @@ portfolio.pending.mediationResult.popup.alreadyAccepted=您已經接受了。 portfolio.pending.failedTrade.taker.missingTakerFeeTx=吃單交易費未找到。\n\n如果沒有 tx,交易不能完成。沒有資金被鎖定以及沒有支付交易費用。你可以將交易移至失敗的交易。 portfolio.pending.failedTrade.maker.missingTakerFeeTx=掛單費交易未找到。\n\n如果沒有 tx,交易不能完成。沒有資金被鎖定以及沒有支付交易費用。你可以將交易移至失敗的交易。 -portfolio.pending.failedTrade.missingDepositTx=這個保證金交易(2 對 2 多重簽名交易)缺失\n\n沒有該 tx,交易不能完成。沒有資金被鎖定但是您的交易手續費仍然已支出。您可以發起一個請求去賠償改交易手續費在這裏:https://github.com/bisq-network/support/issues\n\n請隨意的將該交易移至失敗交易 -portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=延遲支付交易缺失,但是資金仍然被鎖定在保證金交易中。\n\n請不要給比特幣賣家發送法幣或數字貨幣,因為沒有延遲交易 tx,不能開啟仲裁。使用 Cmd/Ctrl+o開啟調解協助。調解員應該建議交易雙方分別退回全部的保證金(賣方支付的交易金額也會全數返還)。這樣的話不會有任何的安全問題只會損失交易手續費。\n\n你可以在這裏為失敗的交易提出賠償要求:https://github.com/bisq-network/support/issues -portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=延遲支付交易確實但是資金仍然被鎖定在保證金交易中。\n\n如果賣家仍然缺失延遲支付交易,他會接到請勿付款的指示並開啟一個調節幫助。你也應該使用 Cmd/Ctrl+O 去打開一個調節協助\n\n如果買家還沒有發送付款,調解員應該會建議交易雙方分別退回全部的保證金(賣方支付的交易金額也會全數返還)。否則交易額應該判給買方。\n\n你可以在這裏為失敗的交易提出賠償要求:https://github.com/bisq-network/support/issues -portfolio.pending.failedTrade.errorMsgSet=在處理交易協議是發生了一個錯誤\n\n錯誤:{0}\n\n這應該不是致命錯誤,您可以正常的完成交易。如果你仍擔憂,打開一個調解協助並從 Haveno 調解員處得到建議。\n\n如果這個錯誤是致命的那麼這個交易就無法完成,你可能會損失交易費。可以在這裏為失敗的交易提出賠償要求:https://github.com/bisq-network/support/issues -portfolio.pending.failedTrade.missingContract=沒有設置交易合同。\n\n這個交易無法完成,你可能會損失交易手續費。可以在這裏為失敗的交易提出賠償要求:https://github.com/bisq-network/support/issues +portfolio.pending.failedTrade.missingDepositTx=這個保證金交易(2 對 2 多重簽名交易)缺失\n\n沒有該 tx,交易不能完成。沒有資金被鎖定但是您的交易手續費仍然已支出。您可以發起一個請求去賠償改交易手續費在這裏:https://github.com/haveno-dex/haveno/issues\n\n請隨意的將該交易移至失敗交易 +portfolio.pending.failedTrade.buyer.existingDepositTxButMissingDelayedPayoutTx=延遲支付交易缺失,但是資金仍然被鎖定在保證金交易中。\n\n請不要給比特幣賣家發送法幣或數字貨幣,因為沒有延遲交易 tx,不能開啟仲裁。使用 Cmd/Ctrl+o開啟調解協助。調解員應該建議交易雙方分別退回全部的保證金(賣方支付的交易金額也會全數返還)。這樣的話不會有任何的安全問題只會損失交易手續費。\n\n你可以在這裏為失敗的交易提出賠償要求:https://github.com/haveno-dex/haveno/issues +portfolio.pending.failedTrade.seller.existingDepositTxButMissingDelayedPayoutTx=延遲支付交易確實但是資金仍然被鎖定在保證金交易中。\n\n如果賣家仍然缺失延遲支付交易,他會接到請勿付款的指示並開啟一個調節幫助。你也應該使用 Cmd/Ctrl+O 去打開一個調節協助\n\n如果買家還沒有發送付款,調解員應該會建議交易雙方分別退回全部的保證金(賣方支付的交易金額也會全數返還)。否則交易額應該判給買方。\n\n你可以在這裏為失敗的交易提出賠償要求:https://github.com/haveno-dex/haveno/issues +portfolio.pending.failedTrade.errorMsgSet=在處理交易協議是發生了一個錯誤\n\n錯誤:{0}\n\n這應該不是致命錯誤,您可以正常的完成交易。如果你仍擔憂,打開一個調解協助並從 Haveno 調解員處得到建議。\n\n如果這個錯誤是致命的那麼這個交易就無法完成,你可能會損失交易費。可以在這裏為失敗的交易提出賠償要求:https://github.com/haveno-dex/haveno/issues +portfolio.pending.failedTrade.missingContract=沒有設置交易合同。\n\n這個交易無法完成,你可能會損失交易手續費。可以在這裏為失敗的交易提出賠償要求:https://github.com/haveno-dex/haveno/issues portfolio.pending.failedTrade.info.popup=交易協議出現了問題。\n\n{0} portfolio.pending.failedTrade.txChainInvalid.moveToFailed=交易協議出現了嚴重問題。\n\n{0}\n\n您確定想要將該交易移至失敗的交易嗎?\n\n您不能在失敗的交易中打開一個調解或仲裁,但是你隨時可以將失敗的交易重新移至未完成交易。 portfolio.pending.failedTrade.txChainValid.moveToFailed=這個交易協議存在一些問題。\n\n{0}\n\n這個報價交易已經被髮布以及資金已被鎖定。只有在確定情況下將該交易移至失敗交易。這可能會阻止解決問題的可用選項。\n\n您確定想要將該交易移至失敗的交易嗎?\n\n您不能在失敗的交易中打開一個調解或仲裁,但是你隨時可以將失敗的交易重新移至未完成交易。 diff --git a/core/src/test/java/haveno/core/user/PreferencesTest.java b/core/src/test/java/haveno/core/user/PreferencesTest.java index 365d54732b..0639ba11af 100644 --- a/core/src/test/java/haveno/core/user/PreferencesTest.java +++ b/core/src/test/java/haveno/core/user/PreferencesTest.java @@ -24,6 +24,7 @@ import haveno.core.locale.CountryUtil; import haveno.core.locale.CryptoCurrency; import haveno.core.locale.CurrencyUtil; import haveno.core.locale.TraditionalCurrency; +import haveno.core.xmr.nodes.XmrNodes; import haveno.core.locale.GlobalSettings; import haveno.core.locale.Res; import javafx.collections.ObservableList; @@ -45,6 +46,7 @@ public class PreferencesTest { private Preferences preferences; private PersistenceManager persistenceManager; + private XmrNodes xmrNodes; @BeforeEach public void setUp() { @@ -53,12 +55,12 @@ public class PreferencesTest { GlobalSettings.setLocale(en_US); Res.setBaseCurrencyCode("XMR"); Res.setBaseCurrencyName("Monero"); - persistenceManager = mock(PersistenceManager.class); Config config = new Config(); - XmrLocalNode xmrLocalNode = new XmrLocalNode(config, preferences); preferences = new Preferences( persistenceManager, config, null, null); + xmrNodes = new XmrNodes(); + XmrLocalNode xmrLocalNode = new XmrLocalNode(config, preferences, xmrNodes); } @Test diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java index 84443da4d5..cbc6b6b2e3 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java @@ -203,12 +203,12 @@ class GrpcOffersService extends OffersImplBase { return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( new HashMap<>() {{ - put(getGetOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, SECONDS)); - put(getGetMyOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, SECONDS)); - put(getGetOffersMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, SECONDS)); - put(getGetMyOffersMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getPostOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getCancelOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getGetOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, SECONDS)); + put(getGetMyOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, SECONDS)); + put(getGetOffersMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, SECONDS)); + put(getGetMyOffersMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getPostOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getCancelOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); }} ))); } diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java index 286fccf51a..2d650dfb53 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java @@ -251,15 +251,15 @@ class GrpcTradesService extends TradesImplBase { return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( new HashMap<>() {{ - put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 1, SECONDS)); - put(getGetTradesMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 1, SECONDS)); - put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 20 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getConfirmPaymentSentMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getCompleteTradeMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 1, SECONDS)); + put(getGetTradesMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 1, SECONDS)); + put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getConfirmPaymentSentMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getCompleteTradeMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); put(getWithdrawFundsMethod().getFullMethodName(), new GrpcCallRateMeter(3, MINUTES)); - put(getGetChatMessagesMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getSendChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getGetChatMessagesMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getSendChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 75 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); }} ))); } diff --git a/daemon/src/main/resources/logback.xml b/daemon/src/main/resources/logback.xml index e6ba6a6327..f838048ece 100644 --- a/daemon/src/main/resources/logback.xml +++ b/daemon/src/main/resources/logback.xml @@ -1,8 +1,11 @@ + + + - %highlight(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30}: %msg %xEx%n) + %hl2(%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{40}: %msg %xEx%n) diff --git a/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml b/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml index a298688669..fc5f50c2b5 100644 --- a/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml +++ b/desktop/package/linux/exchange.haveno.Haveno.metainfo.xml @@ -60,6 +60,6 @@ - + diff --git a/desktop/package/macosx/Info.plist b/desktop/package/macosx/Info.plist index 7693e124b7..a24f430c12 100644 --- a/desktop/package/macosx/Info.plist +++ b/desktop/package/macosx/Info.plist @@ -5,10 +5,10 @@ CFBundleVersion - 1.0.18 + 1.0.19 CFBundleShortVersionString - 1.0.18 + 1.0.19 CFBundleExecutable Haveno diff --git a/desktop/src/main/java/haveno/desktop/components/InputTextArea.java b/desktop/src/main/java/haveno/desktop/components/InputTextArea.java new file mode 100644 index 0000000000..7bcd18de93 --- /dev/null +++ b/desktop/src/main/java/haveno/desktop/components/InputTextArea.java @@ -0,0 +1,140 @@ +/* + * This file is part of Haveno. + * + * Haveno is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Haveno is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Haveno. If not, see . + */ + +package haveno.desktop.components; + + +import com.jfoenix.controls.JFXTextArea; +import haveno.core.util.validation.InputValidator; +import haveno.desktop.util.validation.JFXInputValidator; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.scene.control.Skin; + +/** + * TextArea with validation support. + * If validator is set it supports on focus out validation with that validator. If a more sophisticated validation is + * needed the validationResultProperty can be used for applying validation result done by external validation. + * In case the isValid property in validationResultProperty get set to false we display a red border and an error + * message within the errorMessageDisplay placed on the right of the text area. + * The errorMessageDisplay gets closed when the ValidatingTextArea instance gets removed from the scene graph or when + * hideErrorMessageDisplay() is called. + * There can be only 1 errorMessageDisplays at a time we use static field for it. + * The position is derived from the position of the textArea itself or if set from the layoutReference node. + */ +//TODO There are some rare situation where it behaves buggy. Needs further investigation and improvements. +public class InputTextArea extends JFXTextArea { + + private final ObjectProperty validationResult = new SimpleObjectProperty<> + (new InputValidator.ValidationResult(true)); + + private final JFXInputValidator jfxValidationWrapper = new JFXInputValidator(); + + private InputValidator validator; + private String errorMessage = null; + + + public InputValidator getValidator() { + return validator; + } + + public void setValidator(InputValidator validator) { + this.validator = validator; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public InputTextArea() { + super(); + + getValidators().add(jfxValidationWrapper); + + validationResult.addListener((ov, oldValue, newValue) -> { + if (newValue != null) { + jfxValidationWrapper.resetValidation(); + if (!newValue.isValid) { + if (!newValue.errorMessageEquals(oldValue)) { // avoid blinking + validate(); // ensure that the new error message replaces the old one + } + if (this.errorMessage != null) { + jfxValidationWrapper.applyErrorMessage(this.errorMessage); + } else { + jfxValidationWrapper.applyErrorMessage(newValue); + } + } + validate(); + } + }); + + textProperty().addListener((o, oldValue, newValue) -> { + refreshValidation(); + }); + + focusedProperty().addListener((o, oldValue, newValue) -> { + if (validator != null) { + if (!oldValue && newValue) { + this.validationResult.set(new InputValidator.ValidationResult(true)); + } else { + this.validationResult.set(validator.validate(getText())); + } + } + }); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public methods + /////////////////////////////////////////////////////////////////////////////////////////// + + public void resetValidation() { + jfxValidationWrapper.resetValidation(); + + String input = getText(); + if (input.isEmpty()) { + validationResult.set(new InputValidator.ValidationResult(true)); + } else { + validationResult.set(validator.validate(input)); + } + } + + public void refreshValidation() { + if (validator != null) { + this.validationResult.set(validator.validate(getText())); + } + } + + public void setInvalid(String message) { + validationResult.set(new InputValidator.ValidationResult(false, message)); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + public ObjectProperty validationResultProperty() { + return validationResult; + } + + protected Skin createDefaultSkin() { + return new JFXTextAreaSkinHavenoStyle(this); + } +} diff --git a/desktop/src/main/java/haveno/desktop/haveno.css b/desktop/src/main/java/haveno/desktop/haveno.css index fc92b1c308..2f50f8d0f3 100644 --- a/desktop/src/main/java/haveno/desktop/haveno.css +++ b/desktop/src/main/java/haveno/desktop/haveno.css @@ -501,15 +501,15 @@ tree-table-view:focused { -jfx-default-color: -bs-color-primary; } -.jfx-date-picker .jfx-text-field { +.jfx-date-picker .jfx-text-field .jfx-text-area { -fx-padding: 0.333333em 0em 0.333333em 0em; } -.jfx-date-picker .jfx-text-field > .input-line { +.jfx-date-picker .jfx-text-field .jfx-text-area > .input-line { -fx-translate-x: 0em; } -.jfx-date-picker .jfx-text-field > .input-focused-line { +.jfx-date-picker .jfx-text-field .jfx-text-area > .input-focused-line { -fx-translate-x: 0em; } diff --git a/desktop/src/main/java/haveno/desktop/images.css b/desktop/src/main/java/haveno/desktop/images.css index 39f0c7e806..aacd4c6b1b 100644 --- a/desktop/src/main/java/haveno/desktop/images.css +++ b/desktop/src/main/java/haveno/desktop/images.css @@ -39,10 +39,6 @@ -fx-image: url("../../images/remove.png"); } -#image-edit { - -fx-image: url("../../images/edit.png"); -} - #image-buy-white { -fx-image: url("../../images/buy_white.png"); } diff --git a/desktop/src/main/java/haveno/desktop/main/MainViewModel.java b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java index f120b794e7..8c36eaf179 100644 --- a/desktop/src/main/java/haveno/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java @@ -335,25 +335,23 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener tacWindow.onAction(acceptedHandler::run).show(); }, 1)); - havenoSetup.setDisplayMoneroConnectionFallbackHandler(show -> { - if (moneroConnectionFallbackPopup == null) { + havenoSetup.setDisplayMoneroConnectionFallbackHandler(fallbackMsg -> { + if (fallbackMsg != null && !fallbackMsg.isEmpty()) { moneroConnectionFallbackPopup = new Popup() - .headLine(Res.get("connectionFallback.headline")) - .warning(Res.get("connectionFallback.msg")) - .closeButtonText(Res.get("shared.no")) - .actionButtonText(Res.get("shared.yes")) - .onAction(() -> { - havenoSetup.getConnectionServiceFallbackHandlerActive().set(false); - new Thread(() -> HavenoUtils.xmrConnectionService.fallbackToBestConnection()).start(); - }) - .onClose(() -> { - log.warn("User has declined to fallback to the next best available Monero node."); - havenoSetup.getConnectionServiceFallbackHandlerActive().set(false); - }); - } - if (show) { + .headLine(Res.get("connectionFallback.headline")) + .warning(fallbackMsg) + .closeButtonText(Res.get("shared.no")) + .actionButtonText(Res.get("shared.yes")) + .onAction(() -> { + havenoSetup.getConnectionServiceFallbackHandler().set(""); + new Thread(() -> HavenoUtils.xmrConnectionService.fallbackToBestConnection()).start(); + }) + .onClose(() -> { + log.warn("User has declined to fallback to the next best available Monero node."); + havenoSetup.getConnectionServiceFallbackHandler().set(""); + }); moneroConnectionFallbackPopup.show(); - } else if (moneroConnectionFallbackPopup.isDisplayed()) { + } else if (moneroConnectionFallbackPopup != null && moneroConnectionFallbackPopup.isDisplayed()) { moneroConnectionFallbackPopup.hide(); } }); @@ -420,7 +418,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener havenoSetup.setRejectedTxErrorMessageHandler(msg -> new Popup().width(850).warning(msg).show()); - havenoSetup.setShowPopupIfInvalidBtcConfigHandler(this::showPopupIfInvalidBtcConfig); + havenoSetup.setShowPopupIfInvalidXmrConfigHandler(this::showPopupIfInvalidXmrConfig); havenoSetup.setRevolutAccountsUpdateHandler(revolutAccountList -> { // We copy the array as we will mutate it later @@ -536,7 +534,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener }); } - private void showPopupIfInvalidBtcConfig() { + private void showPopupIfInvalidXmrConfig() { preferences.setMoneroNodesOptionOrdinal(0); new Popup().warning(Res.get("settings.net.warn.invalidXmrConfig")) .hideCloseButton() diff --git a/desktop/src/main/java/haveno/desktop/main/debug/DebugView.java b/desktop/src/main/java/haveno/desktop/main/debug/DebugView.java index 00ab2680b5..f1427c16a2 100644 --- a/desktop/src/main/java/haveno/desktop/main/debug/DebugView.java +++ b/desktop/src/main/java/haveno/desktop/main/debug/DebugView.java @@ -22,7 +22,7 @@ import haveno.common.taskrunner.Task; import haveno.common.util.Tuple2; import haveno.core.offer.availability.tasks.ProcessOfferAvailabilityResponse; import haveno.core.offer.availability.tasks.SendOfferAvailabilityRequest; -import haveno.core.offer.placeoffer.tasks.AddToOfferBook; +import haveno.core.offer.placeoffer.tasks.MaybeAddToOfferBook; import haveno.core.offer.placeoffer.tasks.MakerReserveOfferFunds; import haveno.core.offer.placeoffer.tasks.ValidateOffer; import haveno.core.trade.protocol.tasks.ApplyFilter; @@ -72,7 +72,7 @@ public class DebugView extends InitializableView { FXCollections.observableArrayList(Arrays.asList( ValidateOffer.class, MakerReserveOfferFunds.class, - AddToOfferBook.class) + MaybeAddToOfferBook.class) )); diff --git a/desktop/src/main/java/haveno/desktop/main/funds/locked/LockedView.java b/desktop/src/main/java/haveno/desktop/main/funds/locked/LockedView.java index 0a986b5505..8cf5700cc6 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/locked/LockedView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/locked/LockedView.java @@ -225,8 +225,8 @@ public class LockedView extends ActivatableView { Optional tradeOptional = tradeManager.getOpenTrade(offerId); if (tradeOptional.isPresent()) { return Optional.of(tradeOptional.get()); - } else if (openOfferManager.getOpenOfferById(offerId).isPresent()) { - return Optional.of(openOfferManager.getOpenOfferById(offerId).get()); + } else if (openOfferManager.getOpenOffer(offerId).isPresent()) { + return Optional.of(openOfferManager.getOpenOffer(offerId).get()); } else { return Optional.empty(); } diff --git a/desktop/src/main/java/haveno/desktop/main/funds/reserved/ReservedView.java b/desktop/src/main/java/haveno/desktop/main/funds/reserved/ReservedView.java index bfa8bd26b0..bcef7e6488 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/reserved/ReservedView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/reserved/ReservedView.java @@ -224,8 +224,8 @@ public class ReservedView extends ActivatableView { Optional tradeOptional = tradeManager.getOpenTrade(offerId); if (tradeOptional.isPresent()) { return Optional.of(tradeOptional.get()); - } else if (openOfferManager.getOpenOfferById(offerId).isPresent()) { - return Optional.of(openOfferManager.getOpenOfferById(offerId).get()); + } else if (openOfferManager.getOpenOffer(offerId).isPresent()) { + return Optional.of(openOfferManager.getOpenOffer(offerId).get()); } else { return Optional.empty(); } diff --git a/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java b/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java index 53b3d70bbf..730b47adf6 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java @@ -302,10 +302,9 @@ public class WithdrawalView extends ActivatableView { BigInteger receiverAmount = tx.getOutgoingTransfer().getDestinations().get(0).getAmount(); BigInteger fee = tx.getFee(); String messageText = Res.get("shared.sendFundsDetailsWithFee", - HavenoUtils.formatXmr(receiverAmount.add(fee), true), + HavenoUtils.formatXmr(receiverAmount, true), withdrawToAddress, - HavenoUtils.formatXmr(fee, true), - HavenoUtils.formatXmr(receiverAmount, true)); + HavenoUtils.formatXmr(fee, true)); // popup confirmation message Popup popup = new Popup(); diff --git a/desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsView.java b/desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsView.java index dee3591bc8..97fcce06d1 100644 --- a/desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsView.java +++ b/desktop/src/main/java/haveno/desktop/main/market/trades/TradesChartsView.java @@ -208,29 +208,29 @@ public class TradesChartsView extends ActivatableViewAndModel { + timeUnitChangeListener = (observable, oldValue, newValue) -> UserThread.execute(() -> { if (newValue != null) { model.setTickUnit((TradesChartsViewModel.TickUnit) newValue.getUserData()); priceAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); volumeAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); volumeInUsdAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); } - }; - priceAxisYWidthListener = (observable, oldValue, newValue) -> { + }); + priceAxisYWidthListener = (observable, oldValue, newValue) -> UserThread.execute(() -> { priceAxisYWidth = (double) newValue; layoutChart(); - }; - volumeAxisYWidthListener = (observable, oldValue, newValue) -> { + }); + volumeAxisYWidthListener = (observable, oldValue, newValue) -> UserThread.execute(() -> { volumeAxisYWidth = (double) newValue; layoutChart(); - }; - tradeStatisticsByCurrencyListener = c -> { + }); + tradeStatisticsByCurrencyListener = c -> UserThread.execute(() -> { nrOfTradeStatisticsLabel.setText(Res.get("market.trades.nrOfTrades", model.tradeStatisticsByCurrency.size())); fillList(); - }; - parentHeightListener = (observable, oldValue, newValue) -> layout(); + }); + parentHeightListener = (observable, oldValue, newValue) -> UserThread.execute(this::layout); - priceColumnLabelListener = (o, oldVal, newVal) -> priceColumn.setGraphic(new AutoTooltipLabel(newVal)); + priceColumnLabelListener = (o, oldVal, newVal) -> UserThread.execute(() -> priceColumn.setGraphic(new AutoTooltipLabel(newVal))); // Need to render on next frame as otherwise there are issues in the chart rendering itemsChangeListener = c -> UserThread.execute(this::updateChartData); @@ -238,31 +238,34 @@ public class TradesChartsView extends ActivatableViewAndModel { - priceChart.setVisible(!showAll); - priceChart.setManaged(!showAll); - priceColumn.setSortable(!showAll); + UserThread.execute(() -> { + priceChart.setVisible(!showAll); + priceChart.setManaged(!showAll); + priceColumn.setSortable(!showAll); - if (showAll) { - volumeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amount"))); - priceColumnLabel.set(Res.get("shared.price")); - if (!tableView.getColumns().contains(marketColumn)) - tableView.getColumns().add(1, marketColumn); + if (showAll) { + volumeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amount"))); + priceColumnLabel.set(Res.get("shared.price")); + if (!tableView.getColumns().contains(marketColumn)) + tableView.getColumns().add(1, marketColumn); + + volumeChart.setPrefHeight(volumeChart.getMaxHeight()); + volumeInUsdChart.setPrefHeight(volumeInUsdChart.getMaxHeight()); + } else { + volumeChart.setPrefHeight(volumeChart.getMinHeight()); + volumeInUsdChart.setPrefHeight(volumeInUsdChart.getMinHeight()); + priceSeries.setName(selectedTradeCurrency.getName()); + String code = selectedTradeCurrency.getCode(); + volumeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amountWithCur", code))); + + priceColumnLabel.set(CurrencyUtil.getPriceWithCurrencyCode(code)); + + tableView.getColumns().remove(marketColumn); + } + + layout(); + }); - volumeChart.setPrefHeight(volumeChart.getMaxHeight()); - volumeInUsdChart.setPrefHeight(volumeInUsdChart.getMaxHeight()); - } else { - volumeChart.setPrefHeight(volumeChart.getMinHeight()); - volumeInUsdChart.setPrefHeight(volumeInUsdChart.getMinHeight()); - priceSeries.setName(selectedTradeCurrency.getName()); - String code = selectedTradeCurrency.getCode(); - volumeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amountWithCur", code))); - - priceColumnLabel.set(CurrencyUtil.getPriceWithCurrencyCode(code)); - - tableView.getColumns().remove(marketColumn); - } - - layout(); return null; }); } @@ -286,14 +289,14 @@ public class TradesChartsView extends ActivatableViewAndModel { + currencyComboBox.setOnChangeConfirmed(e -> UserThread.execute(() -> { if (currencyComboBox.getEditor().getText().isEmpty()) currencyComboBox.getSelectionModel().select(SHOW_ALL); CurrencyListItem selectedItem = currencyComboBox.getSelectionModel().getSelectedItem(); if (selectedItem != null) { model.onSetTradeCurrency(selectedItem.tradeCurrency); } - }); + })); toggleGroup.getToggles().get(model.tickUnit.ordinal()).setSelected(true); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java index e3f9132a84..abdd2d0ec7 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java @@ -44,8 +44,8 @@ import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.BalanceTextField; import haveno.desktop.components.BusyAnimation; import haveno.desktop.components.FundsTextField; -import haveno.desktop.components.HavenoTextArea; import haveno.desktop.components.InfoInputTextField; +import haveno.desktop.components.InputTextArea; import haveno.desktop.components.InputTextField; import haveno.desktop.components.TitledGroupBg; import haveno.desktop.main.MainView; @@ -76,7 +76,6 @@ import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; import javafx.scene.control.Separator; -import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.control.ToggleButton; import javafx.scene.control.Tooltip; @@ -140,7 +139,7 @@ public abstract class MutableOfferView> exten private BalanceTextField balanceTextField; private ToggleButton reserveExactAmountSlider; private ToggleButton buyerAsTakerWithoutDepositSlider; - protected TextArea extraInfoTextArea; + protected InputTextArea extraInfoTextArea; private FundsTextField totalToPayTextField; private Label amountDescriptionLabel, priceCurrencyLabel, priceDescriptionLabel, volumeDescriptionLabel, waitingForFundsLabel, marketBasedPriceLabel, percentagePriceDescriptionLabel, tradeFeeDescriptionLabel, @@ -211,7 +210,7 @@ public abstract class MutableOfferView> exten createListeners(); - balanceTextField.setFormatter(model.getBtcFormatter()); + balanceTextField.setFormatter(model.getXmrFormatter()); paymentAccountsComboBox.setConverter(GUIUtil.getPaymentAccountsComboBoxStringConverter()); paymentAccountsComboBox.setButtonCell(GUIUtil.getComboBoxButtonCell(Res.get("shared.chooseTradingAccount"), @@ -263,6 +262,7 @@ public abstract class MutableOfferView> exten buyerAsTakerWithoutDepositSlider.setSelected(model.dataModel.getBuyerAsTakerWithoutDeposit().get()); + triggerPriceInputTextField.setText(model.triggerPrice.get()); extraInfoTextArea.setText(model.dataModel.extraInfo.get()); } } @@ -592,6 +592,7 @@ public abstract class MutableOfferView> exten triggerPriceInputTextField.validationResultProperty().bind(model.triggerPriceValidationResult); volumeTextField.validationResultProperty().bind(model.volumeValidationResult); securityDepositInputTextField.validationResultProperty().bind(model.securityDepositValidationResult); + extraInfoTextArea.validationResultProperty().bind(model.extraInfoValidationResult); // funding fundingHBox.visibleProperty().bind(model.getDataModel().getIsXmrWalletFunded().not().and(model.showPayFundsScreenDisplayed)); @@ -713,7 +714,7 @@ public abstract class MutableOfferView> exten triggerPriceInputTextField.setText(model.triggerPrice.get()); }; extraInfoFocusedListener = (observable, oldValue, newValue) -> { - model.onFocusOutExtraInfoTextField(oldValue, newValue); + model.onFocusOutExtraInfoTextArea(oldValue, newValue); extraInfoTextArea.setText(model.extraInfo.get()); }; @@ -1097,7 +1098,7 @@ public abstract class MutableOfferView> exten Res.get("payment.shared.optionalExtra"), 25 + heightAdjustment); GridPane.setColumnSpan(extraInfoTitledGroupBg, 3); - extraInfoTextArea = new HavenoTextArea(); + extraInfoTextArea = new InputTextArea(); extraInfoTextArea.setPromptText(Res.get("payment.shared.extraInfo.prompt.offer")); extraInfoTextArea.getStyleClass().add("text-area"); extraInfoTextArea.setWrapText(true); @@ -1109,7 +1110,7 @@ public abstract class MutableOfferView> exten GridPane.setColumnSpan(extraInfoTextArea, GridPane.REMAINING); GridPane.setColumnIndex(extraInfoTextArea, 0); GridPane.setHalignment(extraInfoTextArea, HPos.LEFT); - GridPane.setMargin(extraInfoTextArea, new Insets(Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, 0)); + GridPane.setMargin(extraInfoTextArea, new Insets(Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE, 0, 10, 0)); gridPane.getChildren().add(extraInfoTextArea); } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java index 5588bb53c0..d49b4f2d15 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java @@ -99,7 +99,7 @@ public abstract class MutableOfferViewModel ext private final AccountAgeWitnessService accountAgeWitnessService; private final Navigation navigation; private final Preferences preferences; - protected final CoinFormatter btcFormatter; + protected final CoinFormatter xmrFormatter; private final FiatVolumeValidator fiatVolumeValidator; private final AmountValidator4Decimals amountValidator4Decimals; private final AmountValidator8Decimals amountValidator8Decimals; @@ -160,6 +160,7 @@ public abstract class MutableOfferViewModel ext final ObjectProperty triggerPriceValidationResult = new SimpleObjectProperty<>(new InputValidator.ValidationResult(true)); final ObjectProperty volumeValidationResult = new SimpleObjectProperty<>(); final ObjectProperty securityDepositValidationResult = new SimpleObjectProperty<>(); + final ObjectProperty extraInfoValidationResult = new SimpleObjectProperty<>(); private ChangeListener amountStringListener; private ChangeListener minAmountStringListener; @@ -195,26 +196,26 @@ public abstract class MutableOfferViewModel ext FiatVolumeValidator fiatVolumeValidator, AmountValidator4Decimals amountValidator4Decimals, AmountValidator8Decimals amountValidator8Decimals, - XmrValidator btcValidator, + XmrValidator xmrValidator, SecurityDepositValidator securityDepositValidator, PriceFeedService priceFeedService, AccountAgeWitnessService accountAgeWitnessService, Navigation navigation, Preferences preferences, - @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter xmrFormatter, OfferUtil offerUtil) { super(dataModel); this.fiatVolumeValidator = fiatVolumeValidator; this.amountValidator4Decimals = amountValidator4Decimals; this.amountValidator8Decimals = amountValidator8Decimals; - this.xmrValidator = btcValidator; + this.xmrValidator = xmrValidator; this.securityDepositValidator = securityDepositValidator; this.priceFeedService = priceFeedService; this.accountAgeWitnessService = accountAgeWitnessService; this.navigation = navigation; this.preferences = preferences; - this.btcFormatter = btcFormatter; + this.xmrFormatter = xmrFormatter; this.offerUtil = offerUtil; paymentLabel = Res.get("createOffer.fundsBox.paymentLabel", dataModel.shortOfferId); @@ -502,8 +503,7 @@ public abstract class MutableOfferViewModel ext extraInfoStringListener = (ov, oldValue, newValue) -> { if (newValue != null) { extraInfo.set(newValue); - } else { - extraInfo.set(""); + onExtraInfoTextAreaChanged(); } }; @@ -531,7 +531,7 @@ public abstract class MutableOfferViewModel ext tradeFee.set(HavenoUtils.formatXmr(makerFee)); tradeFeeInXmrWithFiat.set(OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil, dataModel.getMaxMakerFee(), - btcFormatter)); + xmrFormatter)); } @@ -585,6 +585,7 @@ public abstract class MutableOfferViewModel ext dataModel.getVolume().removeListener(volumeListener); dataModel.getSecurityDepositPct().removeListener(securityDepositAsDoubleListener); dataModel.getBuyerAsTakerWithoutDeposit().removeListener(buyerAsTakerWithoutDepositListener); + dataModel.getExtraInfo().removeListener(extraInfoStringListener); //dataModel.feeFromFundingTxProperty.removeListener(feeFromFundingTxListener); dataModel.getIsXmrWalletFunded().removeListener(isWalletFundedListener); @@ -665,7 +666,7 @@ public abstract class MutableOfferViewModel ext createOfferRequested = false; createOfferCanceled = true; OpenOfferManager openOfferManager = HavenoUtils.openOfferManager; - Optional openOffer = openOfferManager.getOpenOfferById(offer.getId()); + Optional openOffer = openOfferManager.getOpenOffer(offer.getId()); if (openOffer.isPresent()) { openOfferManager.cancelOpenOffer(openOffer.get(), () -> { UserThread.execute(() -> { @@ -836,9 +837,17 @@ public abstract class MutableOfferViewModel ext } } - public void onFocusOutExtraInfoTextField(boolean oldValue, boolean newValue) { + public void onFocusOutExtraInfoTextArea(boolean oldValue, boolean newValue) { if (oldValue && !newValue) { - dataModel.setExtraInfo(extraInfo.get()); + onExtraInfoTextAreaChanged(); + } + } + + public void onExtraInfoTextAreaChanged() { + extraInfoValidationResult.set(getExtraInfoValidationResult()); + updateButtonDisableState(); + if (extraInfoValidationResult.get().isValid) { + setExtraInfoToModel(); } } @@ -1045,8 +1054,8 @@ public abstract class MutableOfferViewModel ext .show(); } - CoinFormatter getBtcFormatter() { - return btcFormatter; + CoinFormatter getXmrFormatter() { + return xmrFormatter; } public boolean isShownAsBuyOffer() { @@ -1064,7 +1073,7 @@ public abstract class MutableOfferViewModel ext public String getTradeAmount() { return OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil, dataModel.getAmount().get(), - btcFormatter); + xmrFormatter); } public String getSecurityDepositLabel() { @@ -1084,7 +1093,7 @@ public abstract class MutableOfferViewModel ext return OfferViewModelUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil, dataModel.getSecurityDeposit(), dataModel.getAmount().get(), - btcFormatter + xmrFormatter ); } @@ -1097,7 +1106,7 @@ public abstract class MutableOfferViewModel ext return OfferViewModelUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil, dataModel.getMaxMakerFee(), dataModel.getAmount().get(), - btcFormatter); + xmrFormatter); } public String getMakerFeePercentage() { @@ -1108,7 +1117,7 @@ public abstract class MutableOfferViewModel ext public String getTotalToPayInfo() { return OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil, dataModel.totalToPay.get(), - btcFormatter); + xmrFormatter); } public String getFundsStructure() { @@ -1181,7 +1190,7 @@ public abstract class MutableOfferViewModel ext private void setAmountToModel() { if (amount.get() != null && !amount.get().isEmpty()) { - BigInteger amount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.amount.get(), btcFormatter)); + BigInteger amount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.amount.get(), xmrFormatter)); long maxTradeLimit = dataModel.getMaxTradeLimit(); Price price = dataModel.getPrice().get(); @@ -1202,7 +1211,7 @@ public abstract class MutableOfferViewModel ext private void setMinAmountToModel() { if (minAmount.get() != null && !minAmount.get().isEmpty()) { - BigInteger minAmount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.minAmount.get(), btcFormatter)); + BigInteger minAmount = HavenoUtils.coinToAtomicUnits(DisplayUtils.parseToCoinWith4Decimals(this.minAmount.get(), xmrFormatter)); Price price = dataModel.getPrice().get(); long maxTradeLimit = dataModel.getMaxTradeLimit(); @@ -1343,10 +1352,20 @@ public abstract class MutableOfferViewModel ext inputDataValid = inputDataValid && securityDepositValidator.validate(securityDeposit.get()).isValid; } + inputDataValid = inputDataValid && getExtraInfoValidationResult().isValid; + isNextButtonDisabled.set(!inputDataValid); isPlaceOfferButtonDisabled.set(createOfferRequested || !inputDataValid || !dataModel.getIsXmrWalletFunded().get()); } + private ValidationResult getExtraInfoValidationResult() { + if (extraInfo.get() != null && !extraInfo.get().isEmpty() && extraInfo.get().length() > Restrictions.MAX_EXTRA_INFO_LENGTH) { + return new InputValidator.ValidationResult(false, Res.get("createOffer.extraInfo.invalid.tooLong", Restrictions.MAX_EXTRA_INFO_LENGTH)); + } else { + return new InputValidator.ValidationResult(true); + } + } + private void updateMarketPriceToManual() { final String currencyCode = dataModel.getTradeCurrencyCode().get(); MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java index b15f43db8a..af47d9bec5 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java @@ -20,6 +20,7 @@ package haveno.desktop.main.offer.offerbook; import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIconView; import haveno.common.UserThread; import haveno.common.app.DevEnv; import haveno.common.util.Tuple3; @@ -1082,7 +1083,7 @@ abstract public class OfferBookView onRemoveOpenOffer(offer)); - iconView2.setId("image-edit"); + iconView2.setSize("16px"); button2.updateText(Res.get("shared.edit")); button2.setOnAction(e -> onEditOpenOffer(offer)); button2.setManaged(true); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java index d98098e99c..c93dfa8d43 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java @@ -711,6 +711,6 @@ abstract class OfferBookViewModel extends ActivatableViewModel { abstract String getCurrencyCodeFromPreferences(OfferDirection direction); public OpenOffer getOpenOffer(Offer offer) { - return openOfferManager.getOpenOfferById(offer.getId()).orElse(null); + return openOfferManager.getOpenOffer(offer.getId()).orElse(null); } } diff --git a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java index f6b947954d..1a548f1b1f 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java @@ -284,6 +284,7 @@ class TakeOfferDataModel extends OfferDataModel { // handle error if (errorMsg != null) { new Popup().warning(errorMsg).show(); + log.warn("Error taking offer " + offer.getId() + ": " + errorMsg); errorMessageHandler.handleErrorMessage(errorMsg); } } diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/Overlay.java b/desktop/src/main/java/haveno/desktop/main/overlays/Overlay.java index 4d0eb57fea..e216b14ed9 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/Overlay.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/Overlay.java @@ -765,9 +765,7 @@ public abstract class Overlay> { FormBuilder.getIconForLabel(AwesomeIcon.COPY, copyIcon, "1.1em"); copyIcon.addEventHandler(MOUSE_CLICKED, mouseEvent -> { if (message != null) { - String forClipboard = headLineLabel.getText() + System.lineSeparator() + message - + System.lineSeparator() + (messageHyperlinks == null ? "" : messageHyperlinks.toString()); - Utilities.copyToClipboard(forClipboard); + Utilities.copyToClipboard(getClipboardText()); Tooltip tp = new Tooltip(Res.get("shared.copiedToClipboard")); Node node = (Node) mouseEvent.getSource(); UserThread.runAfter(() -> tp.hide(), 1); @@ -1083,6 +1081,11 @@ public abstract class Overlay> { return isDisplayed; } + public String getClipboardText() { + return headLineLabel.getText() + System.lineSeparator() + message + + System.lineSeparator() + (messageHyperlinks == null ? "" : messageHyperlinks.toString()); + } + @Override public String toString() { return "Popup{" + diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java index 3ef8ca521b..2139bf2813 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java @@ -342,7 +342,7 @@ public class OfferDetailsWindow extends Overlay { BigInteger reservedAmount = isMyOffer ? offer.getReservedAmount() : null; // get offer challenge - OpenOffer myOpenOffer = HavenoUtils.openOfferManager.getOpenOfferById(offer.getId()).orElse(null); + OpenOffer myOpenOffer = HavenoUtils.openOfferManager.getOpenOffer(offer.getId()).orElse(null); String offerChallenge = myOpenOffer == null ? null : myOpenOffer.getChallenge(); rows = 3; diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/QRCodeWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/QRCodeWindow.java index 223933e5c1..8bca62d143 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/QRCodeWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/QRCodeWindow.java @@ -37,10 +37,10 @@ import java.io.ByteArrayInputStream; public class QRCodeWindow extends Overlay { private static final Logger log = LoggerFactory.getLogger(QRCodeWindow.class); private final ImageView qrCodeImageView; - private final String bitcoinURI; + private final String moneroUri; public QRCodeWindow(String bitcoinURI) { - this.bitcoinURI = bitcoinURI; + this.moneroUri = bitcoinURI; final byte[] imageBytes = QRCode .from(bitcoinURI) .withSize(300, 300) @@ -70,7 +70,7 @@ public class QRCodeWindow extends Overlay { GridPane.setHalignment(qrCodeImageView, HPos.CENTER); gridPane.getChildren().add(qrCodeImageView); - String request = bitcoinURI.replace("%20", " ").replace("?", "\n?").replace("&", "\n&"); + String request = moneroUri.replace("%20", " ").replace("?", "\n?").replace("&", "\n&"); Label infoLabel = new AutoTooltipLabel(Res.get("qRCodeWindow.request", request)); infoLabel.setMouseTransparent(true); infoLabel.setWrapText(true); @@ -87,4 +87,8 @@ public class QRCodeWindow extends Overlay { applyStyles(); display(); } + + public String getClipboardText() { + return moneroUri; + } } diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java index 21ab8a7788..5828b94348 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferDataModel.java @@ -26,6 +26,7 @@ import haveno.core.locale.TradeCurrency; import haveno.core.offer.CreateOfferService; import haveno.core.offer.Offer; import haveno.core.offer.OfferUtil; +import haveno.core.offer.OpenOffer; import haveno.core.offer.OpenOfferManager; import haveno.core.payment.PaymentAccount; import haveno.core.provider.price.PriceFeedService; @@ -89,13 +90,15 @@ class DuplicateOfferDataModel extends MutableOfferDataModel { setPrice(offer.getPrice()); setVolume(offer.getVolume()); setUseMarketBasedPrice(offer.isUseMarketBasedPrice()); - setBuyerAsTakerWithoutDeposit(offer.hasBuyerAsTakerWithoutDeposit()); - - setSecurityDepositPct(getSecurityAsPercent(offer)); - if (offer.isUseMarketBasedPrice()) { setMarketPriceMarginPct(offer.getMarketPriceMarginPct()); } + setBuyerAsTakerWithoutDeposit(offer.hasBuyerAsTakerWithoutDeposit()); + setSecurityDepositPct(getSecurityAsPercent(offer)); + setExtraInfo(offer.getOfferExtraInfo()); + + OpenOffer openOffer = openOfferManager.getOpenOffer(offer.getId()).orElse(null); + if (openOffer != null) setTriggerPrice(openOffer.getTriggerPrice()); } private double getSecurityAsPercent(Offer offer) { diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferViewModel.java index 08c5e18f9e..e6e2cf883f 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/duplicateoffer/DuplicateOfferViewModel.java @@ -29,6 +29,7 @@ import haveno.core.payment.validation.XmrValidator; import haveno.core.provider.price.PriceFeedService; import haveno.core.user.Preferences; import haveno.core.util.FormattingUtils; +import haveno.core.util.PriceUtil; import haveno.core.util.coin.CoinFormatter; import haveno.core.util.validation.AmountValidator4Decimals; import haveno.core.util.validation.AmountValidator8Decimals; @@ -76,6 +77,16 @@ class DuplicateOfferViewModel extends MutableOfferViewModel 0) { + triggerPrice.set(PriceUtil.formatMarketPrice(triggerPriceAsLong, dataModel.getCurrencyCode())); + } else { + triggerPrice.set(""); + } + onTriggerPriceTextFieldChanged(); + triggerFocusOutOnAmountFields(); onFocusOutPriceAsPercentageTextField(true, false); } diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java index b9cd38efb5..1e7f358572 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferDataModel.java @@ -20,6 +20,8 @@ package haveno.desktop.main.portfolio.editoffer; import com.google.inject.Inject; import com.google.inject.name.Named; + +import haveno.common.UserThread; import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ResultHandler; import haveno.core.account.witness.AccountAgeWitnessService; @@ -135,6 +137,9 @@ class EditOfferDataModel extends MutableOfferDataModel { securityDepositPct.set(securityDepositPercent); allowAmountUpdate = false; + + triggerPrice = openOffer.getTriggerPrice(); + extraInfo.set(offer.getOfferExtraInfo()); } @Override @@ -162,10 +167,10 @@ class EditOfferDataModel extends MutableOfferDataModel { setPrice(offer.getPrice()); setVolume(offer.getVolume()); setUseMarketBasedPrice(offer.isUseMarketBasedPrice()); - setTriggerPrice(openOffer.getTriggerPrice()); if (offer.isUseMarketBasedPrice()) { setMarketPriceMarginPct(offer.getMarketPriceMarginPct()); } + setTriggerPrice(openOffer.getTriggerPrice()); setExtraInfo(offer.getOfferExtraInfo()); } @@ -226,8 +231,10 @@ class EditOfferDataModel extends MutableOfferDataModel { openOfferManager.editOpenOfferPublish(editedOffer, triggerPrice, initialState, () -> { openOffer = null; - resultHandler.handleResult(); - }, errorMessageHandler); + UserThread.execute(() -> resultHandler.handleResult()); + }, (errorMsg) -> { + UserThread.execute(() -> errorMessageHandler.handleErrorMessage(errorMsg)); + }); } public void onCancelEditOffer(ErrorMessageHandler errorMessageHandler) { diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferView.java index 3752ab9dcb..bc804b5576 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/editoffer/EditOfferView.java @@ -205,7 +205,8 @@ public class EditOfferView extends MutableOfferView { cancelButton.setDisable(true); busyAnimation.play(); spinnerInfoLabel.setText(Res.get("editOffer.publishOffer")); - //edit offer + + // edit offer model.onPublishOffer(() -> { String key = "editOfferSuccess"; if (DontShowAgainLookup.showAgain(key)) { diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java index 7649274aa1..ea93d145fb 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java @@ -553,13 +553,6 @@ public class PendingTradesDataModel extends ActivatableDataModel { disputeManager = arbitrationManager; Dispute dispute = disputesService.createDisputeForTrade(trade, offer, pubKeyRingProvider.get(), isMaker, isSupportTicket); - // export latest multisig hex - try { - trade.exportMultisigHex(); - } catch (Exception e) { - log.error("Failed to export multisig hex", e); - } - // send dispute opened message sendDisputeOpenedMessage(dispute, disputeManager); tradeManager.requestPersistence(); 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 41881c58be..6a34504dad 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 @@ -100,7 +100,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel buyerState = new SimpleObjectProperty<>(); private final ObjectProperty sellerState = new SimpleObjectProperty<>(); @Getter - private final ObjectProperty messageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); + private final ObjectProperty paymentSentMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); private Subscription tradeStateSubscription; private Subscription paymentAccountDecryptedSubscription; private Subscription payoutStateSubscription; @@ -186,7 +186,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel { onPayoutStateChanged(state); }); - messageStateSubscription = EasyBind.subscribe(trade.getProcessModel().getPaymentSentMessageStateProperty(), this::onMessageStateChanged); + messageStateSubscription = EasyBind.subscribe(trade.getSeller().getPaymentSentMessageStateProperty(), this::onPaymentSentMessageStateChanged); } } } @@ -215,8 +215,8 @@ public class PendingTradesViewModel extends ActivatableWithDataModelSet the mandatory minimum version for trading (optional) -If applicable, update the mandatory minimum version for trading, by entering `ctrl + f` to open the Filter window, enter a private key with developer privileges, and enter the minimum version (e.g. 1.0.18) in the field labeled "Min. version required for trading". +If applicable, update the mandatory minimum version for trading, by entering `ctrl + f` to open the Filter window, enter a private key with developer privileges, and enter the minimum version (e.g. 1.0.19) in the field labeled "Min. version required for trading". Send update alert diff --git a/docs/developer-guide.md b/docs/developer-guide.md index 565ee4886e..e09b953df7 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -8,7 +8,7 @@ This document is a guide for Haveno development. ## Run the UI proof of concept -Follow [instructions](https://github.com/haveno-dex/haveno-ts#run-in-a-browser) to run Haveno's UI proof of concept in a browser. +Follow [instructions](https://github.com/haveno-dex/haveno-ui-poc) to run Haveno's UI proof of concept in a browser. This proof of concept demonstrates using Haveno's gRPC server with a web frontend (react and typescript) instead of Haveno's JFX application. @@ -28,7 +28,7 @@ Follow [instructions](https://github.com/haveno-dex/haveno-ts#run-tests) to run 2. Define the new service or message in Haveno's [protobuf definition](../proto/src/main/proto/grpc.proto). 3. Clean and build Haveno after modifying the protobuf definition: `make clean && make` 4. Implement the new service in Haveno's backend, following existing patterns.
- For example, the gRPC function to get offers is implemented by [`GrpcServer`](https://github.com/haveno-dex/haveno/blob/master/daemon/src/main/java/haveno/daemon/grpc/GrpcServer.java) > [`GrpcOffersService.getOffers(...)`](https://github.com/haveno-dex/haveno/blob/b761dbfd378faf49d95090c126318b419af7926b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java#L104) > [`CoreApi.getOffers(...)`](https://github.com/haveno-dex/haveno/blob/b761dbfd378faf49d95090c126318b419af7926b/core/src/main/java/haveno/core/api/CoreApi.java#L128) > [`CoreOffersService.getOffers(...)`](https://github.com/haveno-dex/haveno/blob/b761dbfd378faf49d95090c126318b419af7926b/core/src/main/java/haveno/core/api/CoreOffersService.java#L126) > [`OfferBookService.getOffers()`](https://github.com/haveno-dex/haveno/blob/b761dbfd378faf49d95090c126318b419af7926b/core/src/main/java/haveno/core/offer/OfferBookService.java#L193). + For example, the gRPC function to get offers is implemented by [`GrpcServer`](https://github.com/haveno-dex/haveno/blob/master/daemon/src/main/java/haveno/daemon/grpc/GrpcServer.java) > [`GrpcOffersService.getOffers(...)`](https://github.com/haveno-dex/haveno/blob/060d9fa4f138ca07f596386972265782e5ec7b7a/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java#L102) > [`CoreApi.getOffers(...)`](https://github.com/haveno-dex/haveno/blob/060d9fa4f138ca07f596386972265782e5ec7b7a/core/src/main/java/haveno/core/api/CoreApi.java#L403) > [`CoreOffersService.getOffers(...)`](https://github.com/haveno-dex/haveno/blob/060d9fa4f138ca07f596386972265782e5ec7b7a/core/src/main/java/haveno/core/api/CoreOffersService.java#L131) > [`OfferBookService.getOffers()`](https://github.com/haveno-dex/haveno/blob/060d9fa4f138ca07f596386972265782e5ec7b7a/core/src/main/java/haveno/core/offer/OfferBookService.java#L248). 5. Build Haveno: `make` 6. Update the gRPC client in haveno-ts: `npm install` 7. Add the corresponding typescript method(s) to [HavenoClient.ts](https://github.com/haveno-dex/haveno-ts/blob/master/src/HavenoClient.ts) with clear and concise documentation. diff --git a/docs/external-tor-usage.md b/docs/external-tor-usage.md new file mode 100644 index 0000000000..ace4a2817a --- /dev/null +++ b/docs/external-tor-usage.md @@ -0,0 +1,460 @@ +# **Using External `tor` with Haveno** +## [How to Install little-t-`tor` for Your Platform](https://support.torproject.org/little-t-tor/#little-t-tor_install-little-t-tor) + +The following `tor` installation instructions are presented here for convenience. + +* **For the most complete, up-to-date & authoritative steps, readers are encouraged to refer to the [Tor Project's Official Homepage](https://www.torproject.org) linked in the header** + +* **Notes:** + + For optimum compatibility with Haveno the running `tor` version should match that of the internal Haveno `tor` version + + For best results, use a version of `tor` which supports the [Onion Service Proof of Work](https://onionservices.torproject.org/technology/security/pow) (`PoW`) mechanism + * (IE: `GNU` build of `tor`) + +--- + +* **Note Regarding Admin Access:** + + To install `tor` you need root privileges. Below all commands that need to be run as `root` user like `apt` and `dpkg` are prepended with `#`, while commands to be run as user with `$` resembling the standard prompt in a terminal. + +### macOS +#### Install a Package Manager + Two of the most popular package managers for `macOS` are: + + [`Homebrew`](https://brew.sh) + + and + + [`Macports`](https://www.macports.org) + + (You can use the package manager of your choice) + + + Install [`Homebrew`](https://brew.sh) + + Follow the instructions on [brew.sh](https://brew.sh) + + + Install [`Macports`](https://www.macports.org) + + Follow the instructions on [macports.org](https://www.macports.org) + +#### Package Installation +##### [`Homebrew`](https://brew.sh) + ```shell + # brew update && brew install tor + ``` + +##### [`Macports`](https://www.macports.org) + ```shell + # port sync && port install tor + ``` + +### Debian / Ubuntu +* *Do **not** use the packages in Ubuntu's universe. In the past they have not reliably been updated. That means you could be missing stability and security fixes.* + +* Configure the [Official Tor Package Repository](https://deb.torproject.org/torproject.org) + + Enable the [Official Tor Package Repository](https://deb.torproject.org/torproject.org) following these [instructions](https://support.torproject.org/apt/tor-deb-repo/) + +#### Package Installation +```shell +# apt update && apt install tor +``` + +### Fedora + * Configure the [Official Tor Package Repository](https://rpm.torproject.org/fedora) + + Enable the [Official Tor Package Repository](https://rpm.torproject.org/fedora) by following these [instructions](https://support.torproject.org/rpm/tor-rpm-install) + +#### Package Installation +``` +# dnf update && dnf install tor +``` + +### Arch Linux +#### Package Installation +```shell +# pacman -Fy && pacman -Syu tor +``` + +### Installing `tor` from source +#### Download Latest Release & Dependencies +The latest release of `tor` can be found on the [download](https://www.torproject.org/download/tor) page + +* When building from source: + + *First* install `libevent`,`openssl` & `zlib` + + *(Including the -devel packages when applicable)* + +#### Install `tor` +```shell +$ tar -xzf tor-.tar.gz; cd tor- +``` + +* Replace \ with the latest version of `tor` + + > For example, `tor-0.4.8.14` + +```shell +$ ./configure && make +``` + +* Now you can run `tor` (0.4.3.x and Later) locally like this: + +```shell +$ ./src/app/tor +``` + +Or, you can run `make install` (as `root` if necessary) to install it globally into `/usr/local/` + +* Now you can run `tor` directly without absolute path like this: + +```shell +$ tor +``` + +### Windows +#### Download +* Download the `Windows Expert Bundle` from the [Official Tor Project's Download page](https://www.torproject.org/download/tor) + +#### Extract +* Extract Archive to Disk + +#### Open Terminal +* Open PowerShell with Admin Privileges + +#### Change to Location of Extracted Archive +* Navigate to `Tor` Directory + +#### Package Installation +* v10 +```powershell +PS C:\Tor\> tor.exe –-service install +``` + +* v11 +```powershell +PS C:\Tor\> tor.exe –-service install +``` + +#### Create Service +```powershell +PS C:\Tor\> sc create tor start=auto binPath="\Tor\tor.exe -nt-service" +``` + +#### Start Service +```powershell +PS C:\Tor\> sc start tor +``` + +## Configuring `tor` via `torrc` +#### [I'm supposed to "edit my torrc". What does that mean?](https://support.torproject.org/tbb/tbb-editing-torrc/) +* Per the [Official Tor Project's support page](https://support.torproject.org/tbb/tbb-editing-torrc/): + * **WARNING:** Do **NOT** follow random advice instructing you to edit your torrc! Doing so can allow an attacker to compromise your security and anonymity through malicious configuration of your torrc. + + **Note:** + + The `torrc` location will ***not*** match those stated in the documentation linked above and will vary across each platform. + +#### [Sample `torrc`](https://gitlab.torproject.org/tpo/core/tor/-/blob/HEAD/src/config/torrc.sample.in) +Users are ***strongly*** encouraged to review both the [Official Tor Project's support page](https://support.torproject.org/tbb/tbb-editing-torrc/) as well as the [sample `torrc`](https://gitlab.torproject.org/tpo/core/tor/-/blob/HEAD/src/config/torrc.sample.in) before proceeding. + +#### Enable `torControlPort` in `torrc` +In order for Haveno to use the `--torControlPort` option, it must be enabled and accessible. The most common way to do so is to edit the `torrc` fiel with a text editor to ensure that an entry for `ControlPort` followed by port number to listen on is present in the `torrc` file. + +#### [Authentication](https://spec.torproject.org/control-spec/implementation-notes.html#authentication) +Per the [Tor Control Protocol - Implementation Notes](https://spec.torproject.org/control-spec/implementation-notes.html): + + * ***"If the control port is open and no authentication operation is enabled, `tor` trusts any local user that connects to the control port. This is generally a poor idea."*** + +##### `CookieAuthentication` +If the `CookieAuthentication` option is true, `tor` writes a *"magic cookie"* file named `control_auth_cookie` into its data directory (or to another file specified in the `CookieAuthFile` option). + +##### Example: +```shell +ControlPort 9051 +CookieAuthentication 1 +``` + +##### `HashedControlPassword` +If the `HashedControlPassword` option is set, it must contain the salted hash of a secret password. The salted hash is computed according to the S2K algorithm in `RFC 2440` of `OpenPGP`, and prefixed with the s2k specifier. This is then encoded in hexadecimal, prefixed by the indicator sequence "16:". + +* `HashedControlPassword` can be generated like so: + ```shell + $ tor --hash-password + ``` + +###### Example: +```shell +ControlPort 9051 +HashedControlPassword 16:C01147DC5F4DA2346056668DD23522558D0E0C8B5CC88FE72EEBC51967 +``` + +##### Restart `tor` +`tor` must be restarted for changes to `torrc` to be applied. + +### \* ***Optional*** \* +#### [Set Up Your Onion Service](https://community.torproject.org/onion-services/setup) + +While not a *strict* requirement for use with Haveno, some users may wish to configure an [Onion Service](https://community.torproject.org/onion-services) + + * ***Only Required When Using The Haveno `--hiddenServiceAddress` Option*** + +Please see the [Official Tor Project's Documentation](https://community.torproject.org/onion-services/setup) for more information about configuration and usage of these services + +--- + +## Haveno's `tor` Aware Options + +Haveno is a natively `tor` aware application and offers **many** flexible configuration options for use by privacy conscious users. + +While some are mutually exclusive, many are cross-applicable. + +Users are encouraged to experiment with options before use to determine which options best fit their personal threat profile. + +### Options +#### `--hiddenServiceAddress` +* Function: + + This option configures a *static* Hidden Service Address to listen on + +* Expected Input Format: + + `` + + (`ed25519`) + +* Acceptable Values + + `` + +* Default value: + + `null` + +#### `--socks5ProxyXmrAddress` +* Function: + + A proxy address to be used for `monero` network + +* Expected Input Format: + + `` + +* Acceptable Values + + `` + +* Default value: + + `null` + +#### `--torrcFile` +* Function: + + An existing `torrc`-file to be sourced for `tor` + + **Note:** + + `torrc`-entries which are critical to Haveno's flawless operation (`torrc` options line, `torrc` option, ...) **can not** be overwritten + +* Expected Input Format: + + `` + +* Acceptable Values + + `` + +* Default value: + + `null` + +#### `--torrcOptions` +* Function: + + A list of `torrc`-entries to amend to Haveno's `torrc` + + **Note:** + + *`torrc`-entries which are critical to Haveno's flawless operation (`torrc` options line, `torrc` option, ...) can **not** be overwritten* + +* Expected Input Format: + + `` + +* Acceptable Values + + `<^([^\s,]+\s[^,]+,?\s*)+$>` + +* Default value: + + `null` + +#### `--torControlHost` ++ Function + + The control `hostname` or `IP` of an already running `tor` service to be used by Haveno + +* Expected Input Format + + `` + + (`hostname`, `IPv4` or `IPv6`) + +* Acceptable Values + + `` + +* Default Value + + `null` + +#### `--torControlPort` ++ Function + + The control port of an already running `tor` service to be used by Haveno + +* Expected Input Format + + `` + +* Acceptable Values + + `` + +* Default Value + + `-1` + +#### `--torControlPassword` ++ Function + + The password for controlling the already running `tor` service + +* Expected Input Format + + `` + +* Acceptable Values + + `` + +* Default Value + + `null` + +#### `--torControlCookieFile` ++ Function + + The cookie file for authenticating against the already running `tor` service + * Used in conjunction with `--torControlUseSafeCookieAuth` option + +* Expected Input Format + + `` + +* Acceptable Values + + `` + +* Default Value + + `null` + +#### `--torControlUseSafeCookieAuth` ++ Function + + Use the `SafeCookie` method when authenticating to the already running `tor` service + +* Expected Input Format + + `null` + +* Acceptable Values + + `none` + +* Default Value + + `off` + +#### `--torStreamIsolation` ++ Function + + Use stream isolation for Tor + * This option is currently considered ***experimental*** + +* Expected Input Format + + `` + +* Acceptable Values + + `` + +* Default Value + + `off` + +#### `--useTorForXmr` ++ Function + + Configure `tor` for `monero` connections with ***either***: + + * after_sync + + **or** + + * off + + **or** + + * on + +* Expected Input Format + + `` + +* Acceptable Values + + `` + +* Default Value + + `AFTER_SYNC` + +#### `--socks5DiscoverMode` ++ Function + + Specify discovery mode for `monero` nodes + +* Expected Input Format + + `` + +* Acceptable Values + + `ADDR, DNS, ONION, ALL` + + One or more comma separated. + + *(Will be **OR**'d together)* + +* Default Value + + `ALL` + +--- + +## Starting Haveno Using Externally Available `tor` +### Dynamic Onion Assignment via `--torControlPort` +```shell +$ /opt/haveno/bin/Haveno --torControlPort='9051' --torControlCookieFile='/var/run/tor/control.authcookie' --torControlUseSafeCookieAuth --useTorForXmr='on' --socks5ProxyXmrAddress='127.0.0.1:9050' +``` + +### Static Onion Assignment via `--hiddenServiceAddress` +```shell +$ /opt/haveno/bin/Haveno --socks5ProxyXmrAddress='127.0.0.1:9050' --useTorForXmr='on' --hiddenServiceAddress='2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion' +``` diff --git a/docs/import-haveno.md b/docs/import-haveno.md index b05e8925a8..23f0a7e8c3 100644 --- a/docs/import-haveno.md +++ b/docs/import-haveno.md @@ -1,10 +1,16 @@ -## Importing Haveno into development environment +# Importing Haveno dev environment This document describes how to import Haveno into an integrated development environment (IDE). -## Importing Haveno into Eclipse IDE +First [install and run a Haveno test network](installing.md), then use the following instructions to import Haveno into an IDE. -These steps describe how to import Haveno into Eclipse IDE for development. You can also develop using [IntelliJ IDEA](#importing-haveno-into-intellij-idea) or VSCode if you prefer. +## Visual Studio Code (recommended) + +1. Download and open Visual Studio Code: https://code.visualstudio.com/. +2. File > Add folder to Workspace... +3. Browse to the `haveno` git project. + +## Eclipse IDE > Note: Use default values unless specified otherwise. @@ -26,7 +32,7 @@ These steps describe how to import Haveno into Eclipse IDE for development. You You are now ready to make, run, and test changes to the Haveno project! -## Importing Haveno into IntelliJ IDEA +## IntelliJ IDEA > Note: These instructions are outdated and for Haveno. diff --git a/docs/installing.md b/docs/installing.md index 29a15b54f5..eefd844ce6 100644 --- a/docs/installing.md +++ b/docs/installing.md @@ -19,9 +19,9 @@ On Windows, first install MSYS2: 4. Update pacman: `pacman -Syy` 5. Install dependencies. During installation, use default=all by leaving the input blank and pressing enter. - 64-bit: `pacman -S mingw-w64-x86_64-toolchain make mingw-w64-x86_64-cmake git` + 64-bit: `pacman -S mingw-w64-x86_64-toolchain make mingw-w64-x86_64-cmake git zip unzip` - 32-bit: `pacman -S mingw-w64-i686-toolchain make mingw-w64-i686-cmake git` + 32-bit: `pacman -S mingw-w64-i686-toolchain make mingw-w64-i686-cmake git zip unzip` On all platforms, install Java JDK 21: @@ -30,6 +30,8 @@ curl -s "https://get.sdkman.io" | bash sdk install java 21.0.2.fx-librca ``` +Restart the terminal for the changes to take effect. + ## Build Haveno If it's the first time you are building Haveno, run the following commands to download the repository, the needed dependencies, and build the latest release. If using a third party network, replace the repository URL with theirs: @@ -75,7 +77,7 @@ Steps: 1. Run `make user1-desktop-stagenet` to start the application. 2. Click on the "Funds" tab in the top menu and copy the generated XMR address. -3. Go to the [stagenet faucet](https://community.rino.io/faucet/stagenet/) and paste the address above in the "Get XMR" field. Submit and see the stagenet coins being sent to your Haveno instance. +3. Go to the [stagenet faucet](https://stagenet-faucet.xmr-tw.org) and paste the address above in the "Get XMR" field. Submit and see the stagenet coins being sent to your Haveno instance. 4. While you wait the 10 confirmations (20 minutes) needed for your funds to be spendable, create a fiat account by clicking on "Account" in the top menu, select the "National currency accounts" tab, then add a new account. For simplicity, we suggest to test using a Revolut account with a random ID. 5. Now pick up an existing offer or open a new one. Fund your trade and wait 10 blocks for your deposit to be unlocked. 6. Now if you are taking a trade you'll be asked to confirm you have sent the payment outside Haveno. Confirm in the app and wait for the confirmation of received payment from the other trader. diff --git a/docs/tor-upgrade.md b/docs/tor-upgrade.md index 990cc5d161..cf7548207f 100644 --- a/docs/tor-upgrade.md +++ b/docs/tor-upgrade.md @@ -10,7 +10,7 @@ As per the project's authors, `netlayer` is _"essentially a wrapper around the o easy use and convenient integration into Kotlin/Java projects"_. Similarly, `tor-binary` is _"[the] Tor binary packaged in a way that can be used for java projects"_. The project -unpacks the tor browser binaries to extract and repackage the tor binaries themselves. +unpacks the Tor Browser binaries to extract and repackage the tor binaries themselves. Therefore, upgrading tor in Haveno comes down to upgrading these two artefacts. @@ -22,8 +22,8 @@ Therefore, upgrading tor in Haveno comes down to upgrading these two artefacts. - Find out which tor version Haveno currently uses - Find out the current `netlayer` version (see `netlayerVersion` in `haveno/build.gradle`) - - Find that release on the project's [releases page][3] - - The release description says which tor version it includes + - Find that tag on the project's [Tags page][3] + - The tag description says which tor version it includes - Find out the latest available tor release - See the [official tor changelog][4] @@ -32,23 +32,24 @@ Therefore, upgrading tor in Haveno comes down to upgrading these two artefacts. During this update, you will need to keep track of: - - the new tor browser version + - the new Tor Browser version - the new tor binary version Create a PR for the `master` branch of [tor-binary][2] with the following changes: - - Decide which tor browser version contains the desired tor binary version - - The official tor browser releases are here: https://dist.torproject.org/torbrowser/ - - For the chosen tor browser version, get the list of SHA256 checksums and its signature - - For example, for tor browser 10.0.12: - - https://dist.torproject.org/torbrowser/10.0.12/sha256sums-signed-build.txt - - https://dist.torproject.org/torbrowser/10.0.12/sha256sums-signed-build.txt.asc + - Decide which Tor Browser version contains the desired tor binary version + - The latest official Tor Browser releases are here: https://dist.torproject.org/torbrowser/ + - All official Tor Browser releases are here: https://archive.torproject.org/tor-package-archive/torbrowser/ + - For the chosen Tor Browser version, get the list of SHA256 checksums and its signature + - For example, for Tor Browser 14.0.7: + - https://dist.torproject.org/torbrowser/14.0.7/sha256sums-signed-build.txt + - https://dist.torproject.org/torbrowser/14.0.7/sha256sums-signed-build.txt.asc - Verify the signature of the checksums list (see [instructions][5]) - Update the `tor-binary` checksums - For each file present in `tor-binary/tor-binary-resources/checksums`: - - Rename the file such that it reflects the new tor browser version, but preserves the naming scheme + - Rename the file such that it reflects the new Tor Browser version, but preserves the naming scheme - Update the contents of the file with the corresponding SHA256 checksum from the list - - Update `torbrowser.version` to the new tor browser version in: + - Update `torbrowser.version` to the new Tor Browser version in: - `tor-binary/build.xml` - `tor-binary/pom.xml` - Update `version` to the new tor binary version in: @@ -72,7 +73,7 @@ next. ### 3. Update `netlayer` -Create a PR for the `externaltor` branch of [netlayer][1] with the following changes: +Create a PR for the `master` branch of [netlayer][1] with the following changes: - In `netlayer/pom.xml`: - Update `tor-binary.version` to the `tor-binary` commit ID from above (e.g. `a4b868a`) @@ -82,13 +83,13 @@ Create a PR for the `externaltor` branch of [netlayer][1] with the following cha - `netlayer/tor.external/pom.xml` - `netlayer/tor.native/pom.xml` -Once the PR is merged, make a note of the commit ID in the `externaltor` branch (for example `32779ac`), as it will be +Once the PR is merged, make a note of the commit ID in the `master` branch (for example `32779ac`), as it will be needed next. Create a tag for the new artefact version, having the new tor binary version as description, for example: ``` -# Create tag locally for new netlayer release, on the externaltor branch +# Create tag locally for new netlayer release, on the master branch git tag -s 0.7.0 -m"tor 0.4.5.6" # Push it to netlayer repo @@ -105,8 +106,6 @@ Create a Haveno PR with the following changes: - See instructions in `haveno/gradle/witness/gradle-witness.gradle` - - ## Credits Thanks to freimair, JesusMcCloud, mrosseel, sschuberth and cedricwalter for their work on the original @@ -115,8 +114,8 @@ Thanks to freimair, JesusMcCloud, mrosseel, sschuberth and cedricwalter for thei -[1]: https://github.com/bisq-network/netlayer "netlayer" -[2]: https://github.com/bisq-network/tor-binary "tor-binary" -[3]: https://github.com/bisq-network/netlayer/releases "netlayer releases" +[1]: https://github.com/haveno-dex/netlayer "netlayer" +[2]: https://github.com/haveno-dex/tor-binary "tor-binary" +[3]: https://github.com/haveno-dex/netlayer/tags "netlayer Tags" [4]: https://gitweb.torproject.org/tor.git/plain/ChangeLog "tor changelog" [5]: https://support.torproject.org/tbb/how-to-verify-signature/ "verify tor signature" diff --git a/gpg_keys/woodser.asc b/gpg_keys/woodser.asc index 2dcc3f3a7a..4bbc755edc 100644 --- a/gpg_keys/woodser.asc +++ b/gpg_keys/woodser.asc @@ -1,5 +1,4 @@ -----BEGIN PGP PUBLIC KEY BLOCK----- -Comment: GPGTools - https://gpgtools.org mQINBFpYwMsBEACpSn/AxDOGCELE9lmYPfvBzgw2+1xS3TX7kYdlvVDQf+8eCgGz 8ZpBY3lXdga/yMZZBoDknGzjlyaiG/vi7NljMQmWd5eGyhyfkWpeDXYLbiB5HlKe @@ -25,29 +24,42 @@ zA6zmydMyNeUOYKjqnimQUuHBhxuUl5FlokoWaXnUavJvOjVfsoTcNxCcvMHnhFN R5TmNLOLPXrXwdU0V86nDmHstXl+E02SWFTgZ8Vxg318ZLpIw3rb65zUALTfZwpl 32XhIUhBBnN0zRl3scGW+oj6ks8WgErQ7o6dYdTu17AIggNdpHXO3XXVnW0mS6tz IeCvDkEQxegoL/B83B+9LI//U9sc5iSCQOEZQ1YLUdEkNSFgr7HU9GPllop52HUB -GffqGoz4F7MXl3g2ZrkCDQRaWMDLARAAxCZjAQT2bz3wqyz0vDMzwbtPRtHo3DY+ -r1n6CLxXDXu8u6DWGvRX3M2Z7jrbJe4e/cYDSDfNVj9E2YIVyD8pUbv9AUYh5VBq -hQU5C+3aeReO1js2iS1Xk6IAJ60aqp/JsrnRyOQfpAnGQaZlvqomdbbrzZaAaOXv -dgbHyBRj2eHZtSfYkhndfstpkE28etoZhNZP2h0e5DVLmfniwgMmMuZoiJNzEAGG -e9kAxdkvKgRp9HDrj6mGkHmbw6bam87DVrveNTPp662H7gLpIcUUJxzV7LttZDJa -k1/JxCQVbPoy0Frmp3TxXhmSJlV1vGVX8SFucaxrSS8ARhCSBrf+hGypbDGm+Tg5 -+oa1gdUSw24FODk7ut6LNwEgJ4n9ubs/8EP7/9rReiVLjJsW46ZueS1EjFTneZM1 -VyeAqBKqbwj21H9KxTghogCxpPHe4tqTr3J8eFjVYoNZDoFO3b00kjhXWOWicbCt -aT4SYUsRZP5WuBwgQu8W4AGgQpCFv6kJ37ctYfeSduDfGsMK0EJxpxutaDZC2940 -VfUA38LORFbwzPaNAGV8e7mViqEEmDE4g6fT0vyGodCsAM5EIbP/Q4u6ftNfE7Mf -mmp2CLnqHsfVLUvGbH8GbMLqoS1bajy8t4HEU0OZ7N12IQ1hnfnKHrLKpfGKXfl4 -1jkrL2gnuyUAEQEAAYkCNgQYAQoAIBYhBFL9fAGHfKloyXEY0FWhDdSK3uXvBQJa -WMDLAhsMAAoJEFWhDdSK3uXvf3wQAJyXitW8l+D2AaaszKmm4VXYkqf+azrVmRLp -nqUMvIaxhJTY4J2H5bT6JAAEU3/Dp6/ghYvqGbz25r94PUkDPKZ/23MvBMFab8bi -I//pT+jJwQFXKrXEIWhuBNFvqKhL8OxMi1kqys3E456quueohQzZbKyzTAYrEBQX -8/fNf/qaGuWIzcrdWqAO1OxnO/LBTZIh4Jrn1spBh3nW/U6k3LLSsXsPkBv9EIHx -R680R8cstT9cLaxUzqBhXX+iKPq8MqWXD5hZKKBCylWybdfhGc4FF+OszduWDP4n -VahNGD7pFX9hCMi6K5uIRj8bMtVahN7bBiwZMp3nQRAGCO5upqowMaGJv7A9zQ14 -lPKEEOf+3kQUj2XUw4juRmViU91hpIRy4Hf/4Wry3AhqICf9mMgkm/tI1ez+moWQ -RhopYZ4WTNbIhQrSUtaEOQHBcJFinKuR4SXxxmrFHpZ37It3SZZ5zJyZHrLypT9r -y0xrm7JWF++wQVofqvzTmVtIiwbYADuL/fDvyolo85rSeoDSdZVGnvY2tipMhr0+ -qBDrOi3tSaFzU+pmd0/hBmeNxS1ciYnxA6Ei+w0v79mbgKywngMTq+wQDynXrIHe -Np1oXqGvFU9bQ6BhDDKS54pPHm0ZlEg80+vealNXpXIVtjSM2PlRpsTlmqs3YcIa -mqKdaDoa -=bRX1 ------END PGP PUBLIC KEY BLOCK----- +GffqGoz4F7MXl3g2ZrQzd29vZHNlciA8MTMwNjg4NTkrd29vZHNlckB1c2Vycy5u +b3JlcGx5LmdpdGh1Yi5jb20+iQJRBBMBCAA7FiEEUv18AYd8qWjJcRjQVaEN1Ire +5e8FAmfBv40CGwMFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQVaEN1Ire +5e8bDBAAgET7qqMAhymtofo0NSemxLck1xEcZfco3inX4HVAV3J8HBnZPP2q19IP +F+Lj2GTRJZstRWNLwD2+7N3LgPOTGt0X+f6BsHLP0NMR87I7NpBoU+QEJv6fY1Ld +kZbgqfX0MPgHWHVN2qOsgZXQE4WKJECVpb8hJVNicfXb3Em+g5AtbI7ff4ycpRqz +ajSTTnvcn6meoN/LgGHjnFmYkV8CXVfgpcvUQJNqNHsrk6/iFPiWly9zb7G/4Vh7 +MqdjEZwEfGwgjA8Tzeh4Cks1fLM5KcZdMgRUmTSXZJxVdrq7ODwT9uRwCLJyncRx +wA1VrZHqEtiv+k3U9ef7ZngVlRdwogam5WJzyCioNCxBBzs4Z3dm/ZWwR/80YSa1 +DIGq//ybOaZqJ15wNAPzqdM1CwLg17w1sY//eKFFUQPZ7KmhG42/wWYG6ka9wgai +x4iPzO73weQQU/kxa4hjnU07zw+NJUxHfsNmqgJW+fRKmi50h6uz5WxRDigjkdGR +oe0HLipZ3cQjgLHaqR4Uw86yyWXQUYxZ+gmStUkrN3hgAX+JuXBxvKKlQQYUS3/j +JwAepRhi3mkFyoJveGUyfYXvTgYddIiCXBpdRIZSlWOabSYfdxFq+CBuAi16IhII +ulgsAXwKqUuX464zEFb+Ept5ESnApm8qDDXAzCBHlM6tJcOi3ey5Ag0EWljAywEQ +AMQmYwEE9m898Kss9LwzM8G7T0bR6Nw2Pq9Z+gi8Vw17vLug1hr0V9zNme462yXu +Hv3GA0g3zVY/RNmCFcg/KVG7/QFGIeVQaoUFOQvt2nkXjtY7NoktV5OiACetGqqf +ybK50cjkH6QJxkGmZb6qJnW2682WgGjl73YGx8gUY9nh2bUn2JIZ3X7LaZBNvHra +GYTWT9odHuQ1S5n54sIDJjLmaIiTcxABhnvZAMXZLyoEafRw64+phpB5m8Om2pvO +w1a73jUz6euth+4C6SHFFCcc1ey7bWQyWpNfycQkFWz6MtBa5qd08V4ZkiZVdbxl +V/EhbnGsa0kvAEYQkga3/oRsqWwxpvk4OfqGtYHVEsNuBTg5O7reizcBICeJ/bm7 +P/BD+//a0XolS4ybFuOmbnktRIxU53mTNVcngKgSqm8I9tR/SsU4IaIAsaTx3uLa +k69yfHhY1WKDWQ6BTt29NJI4V1jlonGwrWk+EmFLEWT+VrgcIELvFuABoEKQhb+p +Cd+3LWH3knbg3xrDCtBCcacbrWg2QtveNFX1AN/CzkRW8Mz2jQBlfHu5lYqhBJgx +OIOn09L8hqHQrADORCGz/0OLun7TXxOzH5pqdgi56h7H1S1Lxmx/BmzC6qEtW2o8 +vLeBxFNDmezddiENYZ35yh6yyqXxil35eNY5Ky9oJ7slABEBAAGJAjYEGAEKACAW +IQRS/XwBh3ypaMlxGNBVoQ3Uit7l7wUCWljAywIbDAAKCRBVoQ3Uit7l7398EACc +l4rVvJfg9gGmrMyppuFV2JKn/ms61ZkS6Z6lDLyGsYSU2OCdh+W0+iQABFN/w6ev +4IWL6hm89ua/eD1JAzymf9tzLwTBWm/G4iP/6U/oycEBVyq1xCFobgTRb6ioS/Ds +TItZKsrNxOOeqrrnqIUM2Wyss0wGKxAUF/P3zX/6mhrliM3K3VqgDtTsZzvywU2S +IeCa59bKQYd51v1OpNyy0rF7D5Ab/RCB8UevNEfHLLU/XC2sVM6gYV1/oij6vDKl +lw+YWSigQspVsm3X4RnOBRfjrM3blgz+J1WoTRg+6RV/YQjIuiubiEY/GzLVWoTe +2wYsGTKd50EQBgjubqaqMDGhib+wPc0NeJTyhBDn/t5EFI9l1MOI7kZlYlPdYaSE +cuB3/+Fq8twIaiAn/ZjIJJv7SNXs/pqFkEYaKWGeFkzWyIUK0lLWhDkBwXCRYpyr +keEl8cZqxR6Wd+yLd0mWecycmR6y8qU/a8tMa5uyVhfvsEFaH6r805lbSIsG2AA7 +i/3w78qJaPOa0nqA0nWVRp72NrYqTIa9PqgQ6zot7Umhc1PqZndP4QZnjcUtXImJ +8QOhIvsNL+/Zm4CssJ4DE6vsEA8p16yB3jadaF6hrxVPW0OgYQwykueKTx5tGZRI +PNPr3mpTV6VyFbY0jNj5UabE5ZqrN2HCGpqinWg6Gg== +=4SFl +-----END PGP PUBLIC KEY BLOCK----- \ No newline at end of file diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 069e08177b..15194cfc52 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -205,44 +205,49 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + + + + + + diff --git a/p2p/src/main/java/haveno/network/Socks5ProxyProvider.java b/p2p/src/main/java/haveno/network/Socks5ProxyProvider.java index f9c498f08e..79317494bd 100644 --- a/p2p/src/main/java/haveno/network/Socks5ProxyProvider.java +++ b/p2p/src/main/java/haveno/network/Socks5ProxyProvider.java @@ -95,7 +95,9 @@ public class Socks5ProxyProvider { String[] tokens = socks5ProxyAddress.split(":"); if (tokens.length == 2) { try { - return new Socks5Proxy(tokens[0], Integer.valueOf(tokens[1])); + Socks5Proxy proxy = new Socks5Proxy(tokens[0], Integer.valueOf(tokens[1])); + proxy.resolveAddrLocally(false); + return proxy; } catch (UnknownHostException e) { log.error(ExceptionUtils.getStackTrace(e)); } diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 04c32ee8dc..f919f4f1e7 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -385,7 +385,7 @@ message DisputeOpenedMessage { NodeAddress sender_node_address = 2; string uid = 3; SupportType type = 4; - string updated_multisig_hex = 5; + string opener_updated_multisig_hex = 5; PaymentSentMessage payment_sent_message = 6; } @@ -1465,6 +1465,7 @@ message Trade { SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG = 24; SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG = 25; SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG = 26; + BUYER_RECEIVED_PAYMENT_RECEIVED_MSG = 27; } enum Phase { @@ -1568,8 +1569,8 @@ message ProcessModel { bytes payout_tx_signature = 4; bool use_savings_wallet = 5; int64 funds_needed_for_trade = 6; - string payment_sent_message_state = 7; - string payment_sent_message_state_arbitrator = 8; + string payment_sent_message_state_seller = 7 [deprecated = true]; + string payment_sent_message_state_arbitrator = 8 [deprecated = true]; bytes maker_signature = 9; TradePeer maker = 10; TradePeer taker = 11; @@ -1581,6 +1582,7 @@ message ProcessModel { int64 seller_payout_amount_from_mediation = 17; int64 trade_protocol_error_height = 18; string trade_fee_address = 19; + bool import_multisig_hex_scheduled = 20; } message TradePeer { @@ -1612,7 +1614,7 @@ message TradePeer { string made_multisig_hex = 31; string exchanged_multisig_hex = 32; string updated_multisig_hex = 33; - bool deposits_confirmed_message_acked = 34; + bool deposits_confirmed_message_acked = 34 [deprecated = true]; string deposit_tx_hash = 35; string deposit_tx_hex = 36; string deposit_tx_key = 37; @@ -1621,6 +1623,9 @@ message TradePeer { string unsigned_payout_tx_hex = 40; int64 payout_tx_fee = 41; int64 payout_amount = 42; + string deposits_confirmed_message_state = 43; + string payment_sent_message_state = 44; + string payment_received_message_state = 45; } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/scripts/install_whonix_qubes/INSTALL.md b/scripts/install_whonix_qubes/INSTALL.md new file mode 100644 index 0000000000..c56b35cacd --- /dev/null +++ b/scripts/install_whonix_qubes/INSTALL.md @@ -0,0 +1,401 @@ +# Haveno on Qubes/Whonix + +## **Conventions:** + ++ \# – Requires given linux commands to be executed with root privileges either directly as a root user or by use of sudo command + ++ $ or % – Requires given linux commands to be executed as a regular non-privileged user + ++ \ – Used to indicate user supplied variable + +--- + +## **Installation - Scripted & Manual (GUI + CLI):** +### *Acquire release files:* +#### In `dispXXXX` AppVM: +##### Clone repository +```shell +% git clone --depth=1 https://github.com/haveno-dex/haveno +``` + +--- + +### **Create TemplateVM, NetVM & AppVM:** +#### Scripted +##### In `dispXXXX` AppVM: +###### Prepare files for transfer to `dom0` +```shell +% tar -C haveno/scripts/install_qubes/scripts/0-dom0 -zcvf /tmp/haveno.tgz . +``` + +##### In `dom0`: +###### Copy files to `dom0` +```shell +$ mkdir -p /tmp/haveno && qvm-run -p dispXXXX 'cat /tmp/haveno.tgz' > /tmp/haveno.tgz && tar -C /tmp/haveno -zxfv /tmp/haveno.tgz +$ bash /tmp/haveno/0.0-dom0.sh && bash /tmp/haveno/0.1-dom0.sh && bash /tmp/haveno/0.2-dom0.sh +``` + +#### GUI +##### TemplateVM +###### Via `Qubes Manager`: + ++ Locate & highlight whonix-workstation-17 (TemplateVM) + ++ Right-Click "whonix-workstation-17" and select "Clone qube" from Drop-Down + ++ Enter "haveno-template" in "Name" + ++ Click OK Button + +##### NetVM +###### Via `Qubes Manager`: + ++ Click "New qube" Button + ++ Enter "sys-haveno" for "Name and label" + ++ Click the Button Beside "Name and label" and Select "orange" + ++ Select "whonix-gateway-17" from "Template" Drop-Down + ++ Select "sys-firewall" from "Networking" Drop-Down + ++ Tick "Launch settings after creation" Radio-Box + ++ Click OK + ++ Click "Advanced" Tab + ++ Enter "512" for "Initial memory" + +

(Within reason, can adjust to personal preference)

+ ++ Enter "512" for "Max memory" + +

(Within reason, can adjust to personal preference)

+ ++ Tick "Provides network" Radio-Box + ++ Click "Apply" Button + ++ Click "OK" Button + +##### AppVM +###### Via `Qubes Manager`: + ++ Click "New qube" Button + ++ Enter "haveno" for "Name and label" + ++ Click the Button Beside "Name and label" and Select "orange" + ++ Select "haveno-template" from "Template" Drop-Down + ++ Select "sys-haveno" from "Networking" Drop-Down + ++ Tick "Launch settings after creation" Radio-Box + ++ Click OK + ++ Click "Advanced" Tab + ++ Enter "2048" for "Initial memory" + +

(Within reason, can adjust to personal preference)

+ ++ Enter "4096" for "Max memory" + +

(Within reason, can adjust to personal preference)

+ ++ Click "Apply" Button + ++ Click "OK" Button + + +#### CLI +##### TemplateVM +###### In `dom0`: +```shell +$ qvm-clone whonix-workstation-17 haveno-template +``` + +##### NetVM +##### In `dom0`: +```shell +$ qvm-create --template whonix-gateway-17 --class AppVM --label=orange --property memory=512 --property maxmem=512 --property netvm=sys-firewall sys-haveno && qvm-prefs --set sys-haveno provides_network True +``` + +#### AppVM +##### In `dom0`: +```shell +$ qvm-create --template haveno-template --class AppVM --label=orange --property memory=2048 --property maxmem=4096 --property netvm=sys-haveno haveno +$ printf 'haveno-Haveno.desktop' | qvm-appmenus --set-whitelist – haveno +``` + +--- + +### **Build TemplateVM, NetVM & AppVM:** +#### *TemplateVM Using Precompiled Package via `git` Repository (Scripted)* +##### In `dispXXXX` AppVM: +```shell +% qvm-copy haveno/scripts/install_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh +``` + ++ Select "haveno-template" for "Target" of Pop-Up + ++ Click OK + +##### In `haveno-template` TemplateVM: +```shell +% sudo bash QubesIncoming/dispXXXX/1.0-haveno-templatevm.sh "" "" +``` + +

Example:

+ +```shell +% sudo bash QubesIncoming/dispXXXX/1.0-haveno-templatevm.sh "https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno-linux-deb.zip" "ABAF11C65A2970B130ABE3C479BE3E4300411886" +``` + +#### *TemplateVM Using Precompiled Package From `git` Repository (CLI)* +##### In `haveno-template` TemplateVM: +###### Download & Import Project PGP Key +

For Whonix On Qubes OS:

+ +```shell +# export https_proxy=http://127.0.0.1:8082 +# export KEY_SEARCH="" +# curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_SEARCH" | gpg --import +``` + +

Example:

+ +```shell +# export https_proxy=http://127.0.0.1:8082 +# export KEY_SEARCH="ABAF11C65A2970B130ABE3C479BE3E4300411886" +# curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_SEARCH" | gpg --import +``` + +

For Whonix On Anything Other Than Qubes OS:

+ +```shell +# export KEY_SEARCH="" +# curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_SEARCH" | gpg --import +``` + +

Example:

+ +```shell +# export KEY_SEARCH="ABAF11C65A2970B130ABE3C479BE3E4300411886" +# curl -sL "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_SEARCH" | gpg --import +``` + + +###### Download Release Files +

For Whonix On Qubes OS:

+ +```shell +# export https_proxy=http://127.0.0.1:8082 +# curl -sSLo /tmp/hashes.txt https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/1.0.18-hashes.txt +# curl -sSLo /tmp/hashes.txt.sig https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/1.0.18-hashes.txt.sig +# curl -sSLo /tmp/haveno.zip https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno_amd64_deb-latest.zip +# curl -sSLo /tmp/haveno.zip.sig https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno_amd64_deb-latest.zip.sig +``` + +

Note:

+

Above are dummy URLS which MUST be replaced with actual working URLs

+ +

For Whonix On Anything Other Than Qubes OS:

+ +```shell +# curl -sSLo /tmp/hashes.txt https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/1.0.18-hashes.txt +# curl -sSLo /tmp/hashes.txt.sig https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/1.0.18-hashes.txt.sig +# curl -sSLo /tmp/haveno.zip https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno_amd64_deb-latest.zip +# curl -sSLo /tmp/haveno.zip.sig https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno_amd64_deb-latest.zip.sig +``` + +

Note:

+

Above are dummy URLS which MUST be replaced with actual working URLs

+ +###### Verify Release Files +```shell +# if gpg --digest-algo SHA256 --verify /tmp/hashes.txt.sig >/dev/null 2>&1; then printf $'SHASUM file has a VALID signature!\n'; else printf $'SHASUMS failed signature check\n' && sleep 5 && exit 1; fi +``` + +###### Verify Hash, Unpack & Install Package +```shell +# if [[ $(cat /tmp/hashes.txt) =~ $(sha512sum /tmp/haveno*.zip | awk '{ print $1 }') ]] ; then printf $'SHA Hash IS valid!\n' && mkdir -p /usr/share/desktop-directories && cd /tmp && unzip /tmp/haveno*.zip && apt install -y /tmp/haveno*.deb; else printf $'WARNING: Bad Hash!\n' && exit; fi +``` + +###### Verify Jar +```shell +# if [[ $(cat /tmp/desktop*.SHA-256) =~ $(sha256sum /opt/haveno/lib/app/desktop*.jar | awk '{ print $1 }') ]] ; then printf $'SHA Hash IS valid!\n' && printf 'Happy trading!\n'; else printf $'WARNING: Bad Hash!\n' && exit; fi +``` + +#### *TemplateVM Building From Source via `git` Repository (Scripted)* +##### In `dispXXXX` AppVM: +```shell +% bash haveno/scripts/install_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh "" "" "" +``` + +

Example:

+ +```shell +% bash haveno/scripts/install_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh "https://download.bell-sw.com/java/21.0.6+10/bellsoft-jdk21.0.6+10-linux-amd64.deb" "a5e3fd9f5323de5fc188180c91e0caa777863b5b" "https://github.com/haveno-dex/haveno" +``` ++ Upon Successful Compilation & Packaging, A `Filecopy` Confirmation Will Be Presented + ++ Select "haveno-template" for "Target" of Pop-Up + ++ Click OK + +##### In `haveno-template` TemplateVM: +```shell +% sudo apt install -y ./QubesIncoming/dispXXXX/haveno.deb +``` + +#### *NetVM (Scripted)* +##### In `dispXXXX` AppVM: +```shell +$ qvm-copy haveno/scripts/install_qubes/scripts/2-NetVM/2.0-haveno-netvm.sh +``` + ++ Select "sys-haveno" for "Target" Within Pop-Up + ++ Click "OK" Button + +##### In `sys-haveno` NetVM: +(Allow bootstrap process to complete) +```shell +% sudo zsh QubesIncoming/dispXXXX/2.0-haveno-netvm.sh +``` + +#### *NetVM (CLI)* +##### In `sys-haveno` NetVM: +###### Add `onion-grater` Profile +```shell +# onion-grater-add 40_haveno +``` + +###### Restart `onion-grater` Service +```shell +# systemctl restart onion-grater.service +# poweroff +``` + +#### *AppVM (Scripted)* +##### In `dispXXXX` AppVM: +```shell +$ qvm-copy haveno/scripts/install_qubes/scripts/3-AppVM/3.0-haveno-appvm.sh +``` + ++ Select "haveno" for "Target" of Pop-Up + ++ Click OK + +##### In `haveno` AppVM: +```shell +% sudo zsh QubesIncoming/dispXXXX/3.0-haveno-appvm.sh +``` + +#### *AppVM (CLI)* +##### In `haveno` AppVM: +###### Adjust `sdwdate` Configuration +```shell +# mkdir /usr/local/etc/sdwdate-gui.d +# printf "gateway=sys-haveno\n" > /usr/local/etc/sdwdate-gui.d/50_user.conf +# systemctl restart sdwdate +``` + +###### Prepare Firewall Settings via `/rw/config/rc.local` +```shell +# printf "\n# Prepare Local FW Settings\nmkdir -p /usr/local/etc/whonix_firewall.d\n" >> /rw/config/rc.local +# printf "\n# Poke FW\nprintf \"EXTERNAL_OPEN_PORTS+=\\\\\" 9999 \\\\\"\\\n\" | tee /usr/local/etc/whonix_firewall.d/50_user.conf\n" >> /rw/config/rc.local +# printf "\n# Restart FW\nwhonix_firewall\n\n" >> /rw/config/rc.local +``` + +###### View & Verify Change +```shell +# tail /rw/config/rc.local +``` + +

Confirm output contains:

+ +> # Poke FW +> printf "EXTERNAL_OPEN_PORTS+=\" 9999 \"\n" | tee /usr/local/etc/whonix_firewall.d/50_user.conf +> +> # Restart FW +> whonix_firewall + +###### Restart `whonix_firewall` +```shell +# whonix_firewall +``` + +###### Create `haveno-Haveno.desktop` +```shell +# mkdir -p /home/$(ls /home)/\.local/share/applications +# sed 's|/opt/haveno/bin/Haveno|/opt/haveno/bin/Haveno --torControlPort=9051 --socks5ProxyXmrAddress=127.0.0.1:9050 --useTorForXmr=on|g' /opt/haveno/lib/haveno-Haveno.desktop > /home/$(ls /home)/.local/share/applications/haveno-Haveno.desktop +# chown -R $(ls /home):$(ls /home) /home/$(ls /home)/.local/share/applications +``` + +###### View & Verify Change +```shell +# tail /home/$(ls /home)/.local/share/applications/haveno-Haveno.desktop +``` + +

Confirm output contains:

+ +> [Desktop Entry] +> Name=Haveno +> Comment=Haveno +> Exec=/opt/haveno/bin/Haveno --torControlPort=9051 --socks5ProxyXmrAddress=127.0.0.1:9050 --useTorForXmr=on +> Icon=/opt/haveno/lib/Haveno.png +> Terminal=false +> Type=Application +> Categories=Network +> MimeType= + +###### Poweroff +```shell +# poweroff +``` + +### **Remove TemplateVM, NetVM & AppVM:** +#### Scripted +##### In `dom0`: +```shell +$ bash /tmp/haveno/0.3-dom0.sh +``` + +#### GUI +##### Via `Qubes Manager`: + ++ Highlight "haveno" (AppVM) + ++ Click "Delete qube" + ++ Enter "haveno" + ++ Click "OK" Button + ++ Highlight "haveno-template" (TemplateVM) + ++ Click "Delete qube" + ++ Enter "haveno-template" + ++ Click "OK" Button + ++ Highlight "sys-haveno" (NetVM) + ++ Click "Delete qube" + ++ Enter "sys-haveno" + ++ Click "OK" Button + +#### CLI +##### In `dom0`: +```shell +$ qvm-shutdown --force --quiet haveno haveno-template sys-haveno && qvm-remove --force --quiet haveno haveno-template sys-haveno +``` diff --git a/scripts/install_whonix_qubes/README.md b/scripts/install_whonix_qubes/README.md new file mode 100644 index 0000000000..72670e41ca --- /dev/null +++ b/scripts/install_whonix_qubes/README.md @@ -0,0 +1,75 @@ +# Install Haveno on Qubes/Whonix + + +After you already have [`Qubes`](https://www.qubes-os.org/downloads) or [`Whonix`](https://www.whonix.org/wiki/Download) installed: + +1. Download [scripts](https://github.com/haveno-dex/haveno/tree/master/scripts/install_whonix_qubes/scripts). +2. Move script(s) to their respective destination (`0.*-dom0.sh` -> `dom0`, `1.0-haveno-templatevm.sh` -> `haveno-template`, etc.). +3. Consecutively execute the following commands in their respective destinations. + +--- + +## **Create VMs** +[`Qubes`](https://www.qubes-os.org/downloads) +### **In `dom0`:** + +```shell +$ bash 0.0-dom0.sh && bash 0.1-dom0.sh && bash 0.2-dom0.sh +``` + +[`Whonix`](https://www.whonix.org/wiki/Download) On Anything Other Than [`Qubes`](https://www.qubes-os.org/downloads) + +- Clone `Whonix Workstation` To VM Named `haveno-template` +- Clone `Whonix Gateway` To VM Named `sys-haveno` +- Create New Linked VM Clone Based On `haveno-template` Named `haveno` + + +## **Build TemplateVM** +### *Via Binary Archive* +#### **In `haveno-template` `TemplateVM`:** + +```shell +% sudo bash QubesIncoming/dispXXXX/1.0-haveno-templatevm.sh "" "" +``` + +

Example:

+ +```shell +% sudo bash 1.0-haveno-templatevm.sh "https://github.com/havenoexample/haveno-example/releases/download/v1.0.18/haveno-linux-deb.zip" "ABAF11C65A2970B130ABE3C479BE3E4300411886" +``` + +### *Via Source* +#### **In `dispXXXX` `AppVM`:** +```shell +% bash 1.0-haveno-templatevm.sh "" "" "" +``` + +

Example:

+ +```shell +% bash 1.0-haveno-templatevm.sh "https://download.bell-sw.com/java/21.0.6+10/bellsoft-jdk21.0.6+10-linux-amd64.deb" "a5e3fd9f5323de5fc188180c91e0caa777863b5b" "https://github.com/haveno-dex/haveno" +``` + +#### **In `haveno-template` `TemplateVM`:** + +```shell +% sudo apt install -y haveno.deb +``` + +## **Build NetVM** +### **In `sys-haveno` `NetVM`:** + +```shell +% sudo zsh 3.0-haveno-appvm.sh +``` + +## **Build AppVM** +### **In `haveno` `AppVM`:** + +```shell +% sudo zsh 3.0-haveno-appvm.sh +``` + +--- + +Complete Documentation Can Be Found [Here](https://github.com/haveno-dex/haveno/blob/master/scripts/install_whonix_qubes/INSTALL.md). diff --git a/scripts/install_whonix_qubes/scripts/0-dom0/0.0-dom0.sh b/scripts/install_whonix_qubes/scripts/0-dom0/0.0-dom0.sh new file mode 100644 index 0000000000..5618cf1e12 --- /dev/null +++ b/scripts/install_whonix_qubes/scripts/0-dom0/0.0-dom0.sh @@ -0,0 +1,6 @@ +#!/bin/bash +## ./haveno-on-qubes/scripts/0.0-dom0.sh + +## Create Haveno TemplateVM: +qvm-clone whonix-workstation-17 haveno-template + diff --git a/scripts/install_whonix_qubes/scripts/0-dom0/0.1-dom0.sh b/scripts/install_whonix_qubes/scripts/0-dom0/0.1-dom0.sh new file mode 100644 index 0000000000..befa8b6702 --- /dev/null +++ b/scripts/install_whonix_qubes/scripts/0-dom0/0.1-dom0.sh @@ -0,0 +1,6 @@ +#!/bin/bash +## ./haveno-on-qubes/scripts/0.1-dom0.sh + +## Create Haveno NetVM: +qvm-create --template whonix-gateway-17 --class AppVM --label=orange --property memory=512 --property maxmem=512 --property netvm=sys-firewall sys-haveno && qvm-prefs --set sys-haveno provides_network True + diff --git a/scripts/install_whonix_qubes/scripts/0-dom0/0.2-dom0.sh b/scripts/install_whonix_qubes/scripts/0-dom0/0.2-dom0.sh new file mode 100644 index 0000000000..6f52637632 --- /dev/null +++ b/scripts/install_whonix_qubes/scripts/0-dom0/0.2-dom0.sh @@ -0,0 +1,7 @@ +#!/bin/bash +## ./haveno-on-qubes/scripts/0.2-dom0.sh + +## Create Haveno AppVM: +qvm-create --template haveno-template --class AppVM --label=orange --property memory=2048 --property maxmem=4096 --property netvm=sys-haveno haveno +printf 'haveno-Haveno.desktop' | qvm-appmenus --set-whitelist - haveno + diff --git a/scripts/install_whonix_qubes/scripts/0-dom0/0.3-dom0.sh b/scripts/install_whonix_qubes/scripts/0-dom0/0.3-dom0.sh new file mode 100644 index 0000000000..4bdae35533 --- /dev/null +++ b/scripts/install_whonix_qubes/scripts/0-dom0/0.3-dom0.sh @@ -0,0 +1,6 @@ +#!/bin/bash +## ./haveno-on-qubes/scripts/0.3-dom0.sh + +## Remove Haveno GuestVMs +qvm-shutdown --force --quiet haveno haveno-template sys-haveno && qvm-remove --force --quiet haveno haveno-template sys-haveno + diff --git a/scripts/install_whonix_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh b/scripts/install_whonix_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh new file mode 100644 index 0000000000..f1ab43ae1b --- /dev/null +++ b/scripts/install_whonix_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh @@ -0,0 +1,185 @@ +#!/bin/bash +## ./haveno-on-qubes/scripts/1.1-haveno-templatevm_maker.sh + + +function remote { + if [[ -z $PRECOMPILED_URL || -z $FINGERPRINT ]]; then + printf "\nNo arguments provided!\n\nThis script requires two arguments to be provided:\nBinary URL & PGP Fingerprint\n\nPlease review documentation and try again.\n\nExiting now ...\n" + exit 1 + fi + ## Update & Upgrade + apt update && apt upgrade -y + + + ## Install wget + apt install -y wget + + + ## Function to print messages in blue: + echo_blue() { + echo -e "\033[1;34m$1\033[0m" + } + + + # Function to print error messages in red: + echo_red() { + echo -e "\033[0;31m$1\033[0m" + } + + + ## Sweep for old release files + rm *.asc desktop-*-SNAPSHOT-all.jar.SHA-256 haveno* + + + ## Define URL & PGP Fingerprint etc. vars: + user_url=$PRECOMPILED_URL + base_url=$(printf ${user_url} | awk -F'/' -v OFS='/' '{$NF=""}1') + expected_fingerprint=$FINGERPRINT + binary_filename=$(awk -F'/' '{ print $NF }' <<< "$user_url") + package_filename="haveno.deb" + signature_filename="${binary_filename}.sig" + key_filename="$(printf "$expected_fingerprint" | tr -d ' ' | sed -E 's/.*(................)/\1/' )".asc + wget_flags="--tries=10 --timeout=10 --waitretry=5 --retry-connrefused --show-progress" + + + ## Debug: + printf "\nUser URL=$user_url\n" + printf "\nBase URL=$base_url\n" + printf "\nFingerprint=$expected_fingerprint\n" + printf "\nBinary Name=$binary_filename\n" + printf "\nPackage Name=$package_filename\n" + printf "\nSig Filename=$signature_filename\n" + printf "\nKey Filename=$key_filename\n" + + + ## Configure for tinyproxy: + export https_proxy=http://127.0.0.1:8082 + + + ## Download Haveno binary: + echo_blue "Downloading Haveno from URL provided ..." + wget "${wget_flags}" -cq "${user_url}" || { echo_red "Failed to download Haveno binary."; exit 1; } + + + ## Download Haveno signature file: + echo_blue "Downloading Haveno signature ..." + wget "${wget_flags}" -cq "${base_url}""${signature_filename}" || { echo_red "Failed to download Haveno signature."; exit 1; } + + + ## Download the GPG key: + echo_blue "Downloading signing GPG key ..." + wget "${wget_flags}" -cqO "${key_filename}" "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$(echo "$expected_fingerprint" | tr -d ' ')" || { echo_red "Failed to download GPG key."; exit 1; } + + + ## Import the GPG key: + echo_blue "Importing the GPG key ..." + gpg --import "${key_filename}" || { echo_red "Failed to import GPG key."; exit 1; } + + + ## Extract imported fingerprints: + imported_fingerprints=$(gpg --with-colons --fingerprint | grep -A 1 'pub' | grep 'fpr' | cut -d: -f10 | tr -d '\n') + + + ## Remove spaces from the expected fingerprint for comparison: + formatted_expected_fingerprint=$(echo "${expected_fingerprint}" | tr -d ' ') + + + ## Check if the expected fingerprint is in the list of imported fingerprints: + if [[ ! "${imported_fingerprints}" =~ "${formatted_expected_fingerprint}" ]]; then + echo_red "The imported GPG key fingerprint does not match the expected fingerprint." + exit 1 + fi + + + ## Verify the downloaded binary with the signature: + echo_blue "Verifying the signature of the downloaded file ..." + if gpg --digest-algo SHA256 --verify "${signature_filename}" >/dev/null 2>&1; then + 7z x "${binary_filename}" && mv haveno*.deb "${package_filename}"; + else echo_red "Verification failed!" && sleep 5 + exit 1; + fi + + + echo_blue "Haveno binaries have been successfully verified." + + + # Install Haveno: + echo_blue "Installing Haveno ..." + apt install -y ./"${package_filename}" || { echo_red "Failed to install Haveno."; exit 1; } + + ## Finalize + echo_blue "Haveno TemplateVM installation and configuration complete." + echo_blue "\nHappy Trading\!\n" + printf "%s \n" "Press [ENTER] to complete ..." + read ans + #exit + poweroff +} + + +function build { + if [[ -z $JAVA_URL || -z $JAVA_SHA1 || -z $SOURCE_URL ]]; then + printf "\nNo arguments provided!\n\nThis script requires three argument to be provided:\n\nURL for Java 21 JDK Debian Package\n\nSHA1 Hash for Java 21 JDK Debian Package\n\nURL for Remote Git Source Repository\n\nPlease review documentation and try again.\n\nExiting now ...\n" + exit 1 + fi + # Dependancies + sudo apt install -y make git expect fakeroot binutils + + # Java + curl -fsSLo jdk21.deb ${JAVA_URL} + if [[ $(shasum ./jdk21.deb | awk '{ print $1 }') == ${JAVA_SHA1} ]] ; then printf $'SHA Hash IS valid!\n'; else printf $'WARNING: Bad Hash!\n' && exit; fi + sudo apt install -y ./jdk21.deb + + # Build + git clone --depth=1 $SOURCE_URL + GIT_DIR=$(awk -F'/' '{ print $NF }' <<< "$SOURCE_URL") + cd ${GIT_DIR} + git checkout master + sed -i 's|XMR_STAGENET|XMR_MAINNET|g' desktop/package/package.gradle + ./gradlew clean build --refresh-keys --refresh-dependencies + + # Package + # Expect + cat <> /tmp/haveno_package_deb.exp +set send_slow {1 .1} +proc send {ignore arg} { + sleep 1.1 + exp_send -s -- \$arg +} +set timeout -1 +spawn ./gradlew packageInstallers --console=plain +match_max 100000 +expect -exact "" +send -- "y\r" +expect -exact "" +send -- "y\r" +expect -exact "" +send -- "y\r" +expect -exact "app-image" +send -- \x03 +expect eof +DONE + + # Package + expect -f /tmp/haveno_package_deb.exp && find ./ -name '*.deb' -exec qvm-copy {} \; + printf "\nHappy Trading!\n" + +} + +if ! [[ $# -eq 2 || $# -eq 3 ]] ; then + printf "\nFor this script to function, user supplied arguments are required.\n\n" + printf "\nPlease review documentation and try again.\n\n" +fi + +if [[ $# -eq 2 ]] ; then + PRECOMPILED_URL=$1 + FINGERPRINT=$2 + remote +fi + +if [[ $# -eq 3 ]] ; then + JAVA_URL=$1 + JAVA_SHA1=$2 + SOURCE_URL=$3 + build +fi diff --git a/scripts/install_whonix_qubes/scripts/2-NetVM/2.0-haveno-netvm.sh b/scripts/install_whonix_qubes/scripts/2-NetVM/2.0-haveno-netvm.sh new file mode 100644 index 0000000000..d29e61dcf5 --- /dev/null +++ b/scripts/install_whonix_qubes/scripts/2-NetVM/2.0-haveno-netvm.sh @@ -0,0 +1,30 @@ +#!/bin/zsh +## ./haveno-on-qubes/scripts/2.0-haveno-netvm_taker.sh + +## Function to print messages in blue: +echo_blue() { + echo -e "\033[1;34m$1\033[0m" +} + + +# Function to print error messages in red: +echo_red() { + echo -e "\033[0;31m$1\033[0m" +} + + +## onion-grater +# Add onion-grater Profile +echo_blue "\nAdding onion-grater Profile ..." +onion-grater-add 40_haveno + + +# Restart onion-grater +echo_blue "\nRestarting onion-grater Service ..." +systemctl restart onion-grater.service +echo_blue "Haveno NetVM configuration complete." +printf "%s \n" "Press [ENTER] to complete ..." +read ans +#exit +poweroff + diff --git a/scripts/install_whonix_qubes/scripts/3-AppVM/3.0-haveno-appvm.sh b/scripts/install_whonix_qubes/scripts/3-AppVM/3.0-haveno-appvm.sh new file mode 100644 index 0000000000..11582a8314 --- /dev/null +++ b/scripts/install_whonix_qubes/scripts/3-AppVM/3.0-haveno-appvm.sh @@ -0,0 +1,61 @@ +#!/bin/zsh +## ./haveno-on-qubes/scripts/3.0-haveno-appvm_taker.sh + +## Function to print messages in blue: +echo_blue() { + echo -e "\033[1;34m$1\033[0m" +} + + +# Function to print error messages in red: +echo_red() { + echo -e "\033[0;31m$1\033[0m" +} + + +## Adjust sdwdate Configuration +mkdir -p /usr/local/etc/sdwdate-gui.d +printf "gateway=sys-haveno\n" > /usr/local/etc/sdwdate-gui.d/50_user.conf +systemctl restart sdwdate + + +## Prepare Firewall Settings +echo_blue "\nConfiguring FW ..." +printf "\n# Prepare Local FW Settings\nmkdir -p /usr/local/etc/whonix_firewall.d\n" >> /rw/config/rc.local +printf "\n# Poke FW\nprintf \"EXTERNAL_OPEN_PORTS+=\\\\\" 9999 \\\\\"\\\n\" | tee /usr/local/etc/whonix_firewall.d/50_user.conf\n" >> /rw/config/rc.local +printf "\n# Restart FW\nwhonix_firewall\n\n" >> /rw/config/rc.local + + +## View & Verify Change +echo_blue "\nReview the following output and be certain in matches documentation!\n" +tail /rw/config/rc.local +printf "%s \n" "Press [ENTER] to continue ..." +read ans +: + + +## Restart FW +echo_blue "\nRestarting Whonix FW ..." +whonix_firewall + + +### Create Desktop Launcher: +echo_blue "Creating desktop launcher ..." +mkdir -p /home/$(ls /home)/\.local/share/applications +sed 's|/opt/haveno/bin/Haveno|/opt/haveno/bin/Haveno --torControlPort=9051 --torControlUseSafeCookieAuth --torControlCookieFile=/var/run/tor/control.authcookie --socks5ProxyXmrAddress=127.0.0.1:9050 --useTorForXmr=on|g' /opt/haveno/lib/haveno-Haveno.desktop > /home/$(ls /home)/.local/share/applications/haveno-Haveno.desktop +chown -R $(ls /home):$(ls /home) /home/$(ls /home)/.local/share/applications/haveno-Haveno.desktop + + +## View & Verify Change +echo_blue "\nReview the following output and be certain in matches documentation!\n" +tail /home/$(ls /home)/.local/share/applications/haveno-Haveno.desktop +printf "%s \n" "Press [ENTER] to continue ..." +read ans +: + +echo_blue "Haveno AppVM configuration complete." +echo_blue "Refresh applications via Qubes Manager GUI now." +printf "%s \n" "Press [ENTER] to complete ..." +read ans +#exit +poweroff diff --git a/seednode/src/main/java/haveno/seednode/SeedNodeMain.java b/seednode/src/main/java/haveno/seednode/SeedNodeMain.java index 281e138c0b..35d4bbbe17 100644 --- a/seednode/src/main/java/haveno/seednode/SeedNodeMain.java +++ b/seednode/src/main/java/haveno/seednode/SeedNodeMain.java @@ -41,7 +41,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class SeedNodeMain extends ExecutableForAppWithP2p { private static final long CHECK_CONNECTION_LOSS_SEC = 30; - private static final String VERSION = "1.0.18"; + private static final String VERSION = "1.0.19"; private SeedNode seedNode; private Timer checkConnectionLossTime;