From 61a62a1d942babbdb57da9eedc966376a29b0fdd Mon Sep 17 00:00:00 2001 From: boldsuck Date: Sun, 9 Mar 2025 01:42:53 +0100 Subject: [PATCH 01/48] Update tor-upgrade.md docu (#1645) --- docs/tor-upgrade.md | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) 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" From 03a1132c2f9e7a11a7af2a119e22e7976c00ddc3 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sat, 8 Mar 2025 06:23:55 -0500 Subject: [PATCH 02/48] copy monero payment uri to clipboard in qr code window --- .../java/haveno/desktop/main/overlays/Overlay.java | 9 ++++++--- .../desktop/main/overlays/windows/QRCodeWindow.java | 10 +++++++--- 2 files changed, 13 insertions(+), 6 deletions(-) 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/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; + } } From e5f729d12f8389a50a0a3b6113ec23f56ac3ed44 Mon Sep 17 00:00:00 2001 From: boldsuck Date: Sun, 9 Mar 2025 19:32:32 +0100 Subject: [PATCH 03/48] Update Tor Browser version: 14.0.7 and tor binary version: 0.4.8.14 (#1650) --- build.gradle | 2 +- gradle/verification-metadata.xml | 48 ++++++++++++++++---------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/build.gradle b/build.gradle index 74e776cb2b..e083e8ec08 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 = 'd4f9d0ce24' // Tor browser version 14.0.7 and tor binary version: 0.4.8.14 protobufVersion = '3.19.1' protocVersion = protobufVersion pushyVersion = '0.13.2' diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 069e08177b..cbfb839d9f 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -205,44 +205,44 @@ - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + From c853c4ffcb0579625f630ef83f0af4c1957e8e97 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sun, 9 Mar 2025 16:54:28 -0400 Subject: [PATCH 04/48] bump version to 1.0.19 --- build.gradle | 2 +- common/src/main/java/haveno/common/app/Version.java | 2 +- desktop/package/linux/exchange.haveno.Haveno.metainfo.xml | 2 +- desktop/package/macosx/Info.plist | 4 ++-- docs/deployment-guide.md | 2 +- seednode/src/main/java/haveno/seednode/SeedNodeMain.java | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index e083e8ec08..3c3d95d074 100644 --- a/build.gradle +++ b/build.gradle @@ -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/app/Version.java b/common/src/main/java/haveno/common/app/Version.java index d39016dc31..74f3ceb9d6 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. 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/docs/deployment-guide.md b/docs/deployment-guide.md index 42f89b115b..67b5236d33 100644 --- a/docs/deployment-guide.md +++ b/docs/deployment-guide.md @@ -270,7 +270,7 @@ Then follow these instructions: https://github.com/haveno-dex/haveno/blob/master Set 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/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; From 9acd7ad584c62c4123bc8f47ccf7ca7e32ce734f Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 9 Mar 2025 17:02:15 -0400 Subject: [PATCH 05/48] rename config handler from btc to xmr --- core/src/main/java/haveno/core/app/HavenoHeadlessApp.java | 2 +- core/src/main/java/haveno/core/app/HavenoSetup.java | 4 ++-- core/src/main/java/haveno/core/app/WalletAppSetup.java | 6 +++--- .../src/main/java/haveno/desktop/main/MainViewModel.java | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) 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..6637511298 100644 --- a/core/src/main/java/haveno/core/app/HavenoSetup.java +++ b/core/src/main/java/haveno/core/app/HavenoSetup.java @@ -176,7 +176,7 @@ public class HavenoSetup { private Consumer displayPrivateNotificationHandler; @Setter @Nullable - private Runnable showPopupIfInvalidBtcConfigHandler; + private Runnable showPopupIfInvalidXmrConfigHandler; @Setter @Nullable private Consumer> revolutAccountsUpdateHandler; @@ -461,7 +461,7 @@ public class HavenoSetup { havenoSetupListeners.forEach(HavenoSetupListener::onInitWallet); walletAppSetup.init(chainFileLockedExceptionHandler, showFirstPopupIfResyncSPVRequestedHandler, - showPopupIfInvalidBtcConfigHandler, + showPopupIfInvalidXmrConfigHandler, () -> {}, () -> {}); } 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/desktop/src/main/java/haveno/desktop/main/MainViewModel.java b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java index f120b794e7..670543a7aa 100644 --- a/desktop/src/main/java/haveno/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java @@ -420,7 +420,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 +536,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() From 2d46b2ab7c72db58a1ca8be8a3ce288d5a961d06 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Thu, 6 Mar 2025 10:16:23 -0500 Subject: [PATCH 06/48] log warning on error taking offer from ui --- .../haveno/desktop/main/offer/takeoffer/TakeOfferDataModel.java | 1 + 1 file changed, 1 insertion(+) 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); } } From 8b1d2aa203d4a680fe69ce502955232f9947ebcd Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Thu, 6 Mar 2025 10:53:18 -0500 Subject: [PATCH 07/48] fix bug to delete scheduled failed trade after restart --- core/src/main/java/haveno/core/trade/Trade.java | 13 +++++++++---- .../main/java/haveno/core/trade/TradeManager.java | 8 ++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 6ad8e7aef3..ed4a8ce8b0 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -1618,15 +1618,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,6 +1681,10 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { }); } + public boolean isProtocolErrorHandlingScheduled() { + return processModel.getTradeProtocolErrorHeight() > 0; + } + private void restoreDepositsPublishedTrade() { // close open offer diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index caabdb384a..8e5b5d9dd8 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) { From bf97fbc7eacd344567bb3302633de67de6658f21 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 7 Mar 2025 09:43:29 -0500 Subject: [PATCH 08/48] skip reset address entries when failed trade is scheduled for deletion --- core/src/main/java/haveno/core/trade/TradeManager.java | 8 +++++++- .../java/haveno/core/xmr/wallet/XmrWalletService.java | 7 +++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index 8e5b5d9dd8..41135e62f1 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -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(); 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 045897ed37..23e4f74550 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -1016,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 From b0e9627c10ae0a9499d2b802639caba73a30a103 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sat, 8 Mar 2025 07:55:06 -0500 Subject: [PATCH 09/48] rename openOfferManager.getOpenOffer(id) --- .../java/haveno/core/api/CoreOffersService.java | 2 +- .../java/haveno/core/offer/OpenOfferManager.java | 14 +++++++------- .../dispute/mediation/MediationManager.java | 2 +- .../core/support/dispute/refund/RefundManager.java | 4 ++-- core/src/main/java/haveno/core/trade/Trade.java | 4 ++-- .../main/java/haveno/core/trade/TradeManager.java | 2 +- .../tasks/MaybeSendSignContractRequest.java | 2 +- .../haveno/core/xmr/wallet/XmrWalletService.java | 2 +- .../desktop/main/funds/locked/LockedView.java | 4 ++-- .../desktop/main/funds/reserved/ReservedView.java | 4 ++-- .../desktop/main/offer/MutableOfferViewModel.java | 2 +- .../main/offer/offerbook/OfferBookViewModel.java | 2 +- .../main/overlays/windows/OfferDetailsWindow.java | 2 +- 13 files changed, 23 insertions(+), 23 deletions(-) diff --git a/core/src/main/java/haveno/core/api/CoreOffersService.java b/core/src/main/java/haveno/core/api/CoreOffersService.java index a66388c040..f036fb13ea 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))); diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 5df08a38ba..7c71aa9a90 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -236,7 +236,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); @@ -573,7 +573,7 @@ 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 { @@ -686,7 +686,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(); @@ -750,7 +750,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 +813,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) { @@ -1575,7 +1575,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()) { diff --git a/core/src/main/java/haveno/core/support/dispute/mediation/MediationManager.java b/core/src/main/java/haveno/core/support/dispute/mediation/MediationManager.java index b7fa902b83..56686faa61 100644 --- a/core/src/main/java/haveno/core/support/dispute/mediation/MediationManager.java +++ b/core/src/main/java/haveno/core/support/dispute/mediation/MediationManager.java @@ -196,7 +196,7 @@ public final class MediationManager 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/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/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index ed4a8ce8b0..0bc6105049 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -1604,7 +1604,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()); } @@ -1688,7 +1688,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { 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())); } diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index 41135e62f1..88db590c42 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -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; 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/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 23e4f74550..97aef9545f 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -1171,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/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/offer/MutableOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java index 5588bb53c0..e32869afe2 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java @@ -665,7 +665,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(() -> { 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/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; From bedd38748ea282161d8a33f903cd15940a6d4a59 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sat, 8 Mar 2025 17:30:31 -0500 Subject: [PATCH 10/48] sign and post offer directly if reserve amount = available balance --- .../haveno/core/offer/OpenOfferManager.java | 67 ++++++++++++------- .../tasks/MakerReserveOfferFunds.java | 3 + .../tasks/MakerSendSignOfferRequest.java | 2 +- .../tasks/TakerReserveTradeFunds.java | 3 + 4 files changed, 49 insertions(+), 26 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 7c71aa9a90..a475691736 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -987,26 +987,16 @@ 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 @@ -1017,11 +1007,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe 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); errorMessageHandler.handleErrorMessage(e.getMessage()); @@ -1087,13 +1076,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 +1104,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; @@ -1299,13 +1316,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe 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); 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/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); From 251a973fd6296912e8109d16805991265c35a326 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sun, 9 Mar 2025 09:44:20 -0400 Subject: [PATCH 11/48] do not refresh or republish offers if disconnected from xmr node --- .../haveno/core/offer/OpenOfferManager.java | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index a475691736..b4b8dd80fb 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -1978,6 +1978,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } private boolean preventedFromPublishing(OpenOffer openOffer) { + if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) return true; return openOffer.isDeactivated() || openOffer.isCanceled() || openOffer.getOffer().getOfferPayload().getArbitratorSigner() == null; } @@ -2000,25 +2001,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."); From 00a2a7c2b7981310f657cc8dad4edc7456339690 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sun, 9 Mar 2025 09:50:20 -0400 Subject: [PATCH 12/48] nack offer availability request if disconnected from xmr node --- .../src/main/java/haveno/core/offer/OpenOfferManager.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index b4b8dd80fb..32a1507f77 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -1574,6 +1574,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); From 84d8a17ab492d5c173a2e5bf4fc6bdc23e226f37 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Mon, 3 Mar 2025 06:56:20 -0500 Subject: [PATCH 13/48] rename payment sent message state property for seller --- .../main/java/haveno/core/trade/Trade.java | 8 +++--- .../core/trade/protocol/ProcessModel.java | 26 ++++++++++++------- .../core/trade/protocol/TradeProtocol.java | 3 ++- .../tasks/BuyerSendPaymentSentMessage.java | 6 ++--- ...yerSendPaymentSentMessageToArbitrator.java | 3 +-- .../BuyerSendPaymentSentMessageToSeller.java | 10 +++---- .../pendingtrades/PendingTradesViewModel.java | 10 +++---- .../steps/buyer/BuyerStep3View.java | 6 ++--- proto/src/main/proto/pb.proto | 2 +- 9 files changed, 41 insertions(+), 33 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 0bc6105049..6ae5c9fcc4 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -735,9 +735,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // 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? 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 != processModel.getPaymentSentMessageStatePropertySeller().get()) { + log.warn("Updating unexpected payment sent message state for {} {}, expected={}, actual={}", getClass().getSimpleName(), getId(), expectedState, processModel.getPaymentSentMessageStatePropertySeller().get()); + processModel.getPaymentSentMessageStatePropertySeller().set(expectedState); } } @@ -2022,7 +2022,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { public MessageState getPaymentSentMessageState() { if (isPaymentReceived()) return MessageState.ACKNOWLEDGED; - if (processModel.getPaymentSentMessageStateProperty().get() == MessageState.ACKNOWLEDGED) return MessageState.ACKNOWLEDGED; + if (processModel.getPaymentSentMessageStatePropertySeller().get() == MessageState.ACKNOWLEDGED) return MessageState.ACKNOWLEDGED; switch (state) { case BUYER_SENT_PAYMENT_SENT_MSG: return MessageState.SENT; 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..d81c4f476a 100644 --- a/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java +++ b/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java @@ -163,7 +163,7 @@ public class ProcessModel implements Model, PersistablePayload { // 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. @Setter - private ObjectProperty paymentSentMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); + private ObjectProperty paymentSentMessageStatePropertySeller = new SimpleObjectProperty<>(MessageState.UNDEFINED); @Setter private ObjectProperty paymentSentMessageStatePropertyArbitrator = new SimpleObjectProperty<>(MessageState.UNDEFINED); private ObjectProperty paymentAccountDecryptedProperty = new SimpleObjectProperty<>(false); @@ -203,7 +203,7 @@ 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) @@ -240,9 +240,9 @@ 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); + String paymentSentMessageStateSellerString = ProtoUtil.stringOrNullFromProto(proto.getPaymentSentMessageStateSeller()); + MessageState paymentSentMessageStateSeller = ProtoUtil.enumFromProto(MessageState.class, paymentSentMessageStateSellerString); + processModel.setPaymentSentMessageStateSeller(paymentSentMessageStateSeller); String paymentSentMessageStateArbitratorString = ProtoUtil.stringOrNullFromProto(proto.getPaymentSentMessageStateArbitrator()); MessageState paymentSentMessageStateArbitrator = ProtoUtil.enumFromProto(MessageState.class, paymentSentMessageStateArbitratorString); @@ -274,11 +274,11 @@ public class ProcessModel implements Model, PersistablePayload { return getP2PService().getAddress(); } - void setPaymentSentAckMessage(AckMessage ackMessage) { + void setPaymentSentAckMessageSeller(AckMessage ackMessage) { MessageState messageState = ackMessage.isSuccess() ? MessageState.ACKNOWLEDGED : MessageState.FAILED; - setPaymentSentMessageState(messageState); + setPaymentSentMessageStateSeller(messageState); } void setPaymentSentAckMessageArbitrator(AckMessage ackMessage) { @@ -288,8 +288,8 @@ public class ProcessModel implements Model, PersistablePayload { setPaymentSentMessageStateArbitrator(messageState); } - public void setPaymentSentMessageState(MessageState paymentSentMessageStateProperty) { - this.paymentSentMessageStateProperty.set(paymentSentMessageStateProperty); + public void setPaymentSentMessageStateSeller(MessageState paymentSentMessageStateProperty) { + this.paymentSentMessageStatePropertySeller.set(paymentSentMessageStateProperty); if (tradeManager != null) { tradeManager.requestPersistence(); } @@ -302,6 +302,14 @@ public class ProcessModel implements Model, PersistablePayload { } } + public boolean isPaymentSentMessageAckedBySeller() { + return paymentSentMessageStatePropertySeller.get() == MessageState.ACKNOWLEDGED; + } + + public boolean isPaymentSentMessageAckedByArbitrator() { + return paymentSentMessageStatePropertyArbitrator.get() == MessageState.ACKNOWLEDGED; + } + void setDepositTxSentAckMessage(AckMessage ackMessage) { MessageState messageState = ackMessage.isSuccess() ? MessageState.ACKNOWLEDGED : 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..6a158bea7a 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -652,11 +652,12 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D // 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); + processModel.setPaymentSentAckMessageSeller(ackMessage); trade.setStateIfValidTransitionTo(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG); processModel.getTradeManager().requestPersistence(); } else if (trade.getTradePeer(sender) == trade.getArbitrator()) { processModel.setPaymentSentAckMessageArbitrator(ackMessage); + processModel.getTradeManager().requestPersistence(); } 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); 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 f060effe78..bc064399a3 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 @@ -170,7 +170,7 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask timer.stop(); } if (listener != null) { - processModel.getPaymentSentMessageStateProperty().removeListener(listener); + processModel.getPaymentSentMessageStatePropertySeller().removeListener(listener); } } @@ -194,8 +194,8 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask if (resendCounter == 0) { listener = (observable, oldValue, newValue) -> onMessageStateChange(newValue); - processModel.getPaymentSentMessageStateProperty().addListener(listener); - onMessageStateChange(processModel.getPaymentSentMessageStateProperty().get()); + processModel.getPaymentSentMessageStatePropertySeller().addListener(listener); + onMessageStateChange(processModel.getPaymentSentMessageStatePropertySeller().get()); } // first re-send is after 2 minutes, then increase the delay exponentially 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..cd3098737a 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; @@ -59,6 +58,6 @@ public class BuyerSendPaymentSentMessageToArbitrator extends BuyerSendPaymentSen @Override protected boolean isAckedByReceiver() { - return trade.getProcessModel().getPaymentSentMessageStatePropertyArbitrator().get() == MessageState.ACKNOWLEDGED; + return trade.getProcessModel().isPaymentSentMessageAckedByArbitrator(); } } 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..825220d5b4 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 @@ -40,25 +40,25 @@ public class BuyerSendPaymentSentMessageToSeller extends BuyerSendPaymentSentMes @Override protected void setStateSent() { - trade.getProcessModel().setPaymentSentMessageState(MessageState.SENT); + trade.getProcessModel().setPaymentSentMessageStateSeller(MessageState.SENT); super.setStateSent(); } @Override protected void setStateArrived() { - trade.getProcessModel().setPaymentSentMessageState(MessageState.ARRIVED); + trade.getProcessModel().setPaymentSentMessageStateSeller(MessageState.ARRIVED); super.setStateArrived(); } @Override protected void setStateStoredInMailbox() { - trade.getProcessModel().setPaymentSentMessageState(MessageState.STORED_IN_MAILBOX); + trade.getProcessModel().setPaymentSentMessageStateSeller(MessageState.STORED_IN_MAILBOX); super.setStateStoredInMailbox(); } @Override protected void setStateFault() { - trade.getProcessModel().setPaymentSentMessageState(MessageState.FAILED); + trade.getProcessModel().setPaymentSentMessageStateSeller(MessageState.FAILED); super.setStateFault(); } @@ -72,6 +72,6 @@ public class BuyerSendPaymentSentMessageToSeller extends BuyerSendPaymentSentMes @Override protected boolean isAckedByReceiver() { - return trade.getState().ordinal() >= Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG.ordinal(); + return trade.getProcessModel().isPaymentSentMessageAckedBySeller(); } } 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..b9936236c6 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.getProcessModel().getPaymentSentMessageStatePropertySeller(), this::onPaymentSentMessageStateChanged); } } } @@ -215,8 +215,8 @@ public class PendingTradesViewModel extends ActivatableWithDataModel Date: Mon, 3 Mar 2025 07:55:57 -0500 Subject: [PATCH 14/48] save payment received message immediately for reprocessing --- .../haveno/core/trade/protocol/TradeProtocol.java | 13 +++++++++++++ .../tasks/ProcessPaymentReceivedMessage.java | 3 --- 2 files changed, 13 insertions(+), 3 deletions(-) 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 6a158bea7a..117c16cc05 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -536,6 +536,19 @@ 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()); + + // 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(); + if (!trade.isInitialized() || trade.isShutDown()) return; ThreadUtils.execute(() -> { if (!(trade instanceof BuyerTrade || trade instanceof ArbitratorTrade)) { 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..f1016f3c61 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 @@ -80,9 +80,6 @@ public class ProcessPaymentReceivedMessage extends TradeTask { return; } - // save message for reprocessing - trade.getSeller().setPaymentReceivedMessage(message); - // set state trade.getSeller().setUpdatedMultisigHex(message.getUpdatedMultisigHex()); trade.getBuyer().setAccountAgeWitness(message.getBuyerAccountAgeWitness()); From fb2b4a0c6ab20a4d8d451d0e7b6e2d641458c9c5 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Mon, 3 Mar 2025 08:51:32 -0500 Subject: [PATCH 15/48] save and reprocess payment sent message --- .../main/java/haveno/core/trade/Trade.java | 5 +- .../core/trade/protocol/TradeProtocol.java | 49 ++++++++++++++++++- .../tasks/ProcessPaymentSentMessage.java | 1 - 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 6ae5c9fcc4..dc3a1bc7a2 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -2441,8 +2441,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { if (!wasWalletSynced) trySyncWallet(true); updatePollPeriod(); - // reprocess pending payout messages - this.getProtocol().maybeReprocessPaymentReceivedMessage(false); + // reprocess pending messages + getProtocol().maybeReprocessPaymentSentMessage(false); + getProtocol().maybeReprocessPaymentReceivedMessage(false); HavenoUtils.arbitrationManager.maybeReprocessDisputeClosedMessage(this, false); startPolling(); 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 117c16cc05..6400e69f6a 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -106,6 +106,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D protected ErrorMessageHandler errorMessageHandler; private boolean depositsConfirmedTasksCalled; + private int reprocessPaymentSentMessageCount; private int reprocessPaymentReceivedMessageCount; /////////////////////////////////////////////////////////////////////////////////////////// @@ -279,6 +280,22 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D }, trade.getId()); } + public void maybeReprocessPaymentSentMessage(boolean reprocessOnError) { + if (trade.isShutDownStarted()) return; + ThreadUtils.execute(() -> { + synchronized (trade.getLock()) { + + // skip if no need to reprocess + if (trade.isBuyer() || trade.getBuyer().getPaymentSentMessage() == null || trade.getState().ordinal() >= Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal()) { + return; + } + + log.warn("Reprocessing payment sent message 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(() -> { @@ -481,7 +498,25 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D // received by seller and arbitrator protected void handle(PaymentSentMessage message, NodeAddress peer) { + handle(message, peer, true); + } + + // received by seller and arbitrator + protected void handle(PaymentSentMessage message, NodeAddress peer, boolean reprocessOnError) { System.out.println(getClass().getSimpleName() + ".handle(PaymentSentMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); + + // 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(); + if (!trade.isInitialized() || trade.isShutDown()) return; if (!(trade instanceof SellerTrade || trade instanceof ArbitratorTrade)) { log.warn("Ignoring PaymentSentMessage since not seller or arbitrator"); @@ -521,7 +556,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(); 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..de7d949ade 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 @@ -48,7 +48,6 @@ public class ProcessPaymentSentMessage extends TradeTask { trade.getBuyer().setNodeAddress(processModel.getTempTradePeerNodeAddress()); // update state from message - trade.getBuyer().setPaymentSentMessage(message); trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex()); trade.getSeller().setAccountAgeWitness(message.getSellerAccountAgeWitness()); String counterCurrencyTxId = message.getCounterCurrencyTxId(); From 1510e6f18d49ef9ff6610b9b6f550b12300ee228 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Mon, 3 Mar 2025 17:27:50 -0500 Subject: [PATCH 16/48] logging cleanup --- core/src/main/java/haveno/core/trade/Trade.java | 2 +- core/src/main/java/haveno/core/trade/TradeManager.java | 4 ++-- .../java/haveno/core/xmr/setup/MoneroWalletRpcManager.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index dc3a1bc7a2..9466355f34 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -1089,10 +1089,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 } diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index 88db590c42..c98978ae4c 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -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()); 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); } From a55daf803efca28662af7aaebdbe26fd28d64632 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Tue, 4 Mar 2025 10:23:25 -0500 Subject: [PATCH 17/48] call trade message handling off trade thread --- .../main/java/haveno/core/trade/protocol/TradeProtocol.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 6400e69f6a..caeb654813 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -125,12 +125,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) { From 46734459d497ce2b2c9190897c78e2d146719711 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Tue, 4 Mar 2025 06:57:41 -0500 Subject: [PATCH 18/48] highlight logs for handling trade protocol messages --- .../core/trade/protocol/ArbitratorProtocol.java | 4 ++-- .../haveno/core/trade/protocol/BuyerProtocol.java | 2 +- .../core/trade/protocol/SellerProtocol.java | 2 +- .../haveno/core/trade/protocol/TradeProtocol.java | 15 ++++++++------- 4 files changed, 12 insertions(+), 11 deletions(-) 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/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/SellerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java index 4de8fa0d6a..2b7f45877a 100644 --- a/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java @@ -115,7 +115,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(); 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 caeb654813..08c99570f6 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[0m"; // terminal default protected final ProcessModel processModel; protected final Trade trade; @@ -313,7 +314,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } 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()) { @@ -350,7 +351,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()) { @@ -393,7 +394,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()) { @@ -439,7 +440,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()) { @@ -469,7 +470,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()) { @@ -503,7 +504,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D // received by seller and arbitrator protected void handle(PaymentSentMessage message, NodeAddress peer, boolean reprocessOnError) { - System.out.println(getClass().getSimpleName() + ".handle(PaymentSentMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); + log.info(LOG_HIGHLIGHT + "handle(PaymentSentMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + peer); // validate signature try { @@ -582,7 +583,7 @@ 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()); + log.info(LOG_HIGHLIGHT + "handle(PaymentReceivedMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId() + " from " + peer); // validate signature try { From cb69d0646883490b75ae01060e1c6aca23b0ed30 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Tue, 4 Mar 2025 15:12:57 -0500 Subject: [PATCH 19/48] increase grpc rate limits for testnet --- .../java/haveno/daemon/grpc/GrpcOffersService.java | 2 +- .../java/haveno/daemon/grpc/GrpcTradesService.java | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java index 84443da4d5..485ff38ca8 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java @@ -208,7 +208,7 @@ class GrpcOffersService extends OffersImplBase { 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(getCancelOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 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..0a5d2d39d2 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java @@ -252,14 +252,14 @@ class GrpcTradesService extends TradesImplBase { .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(getGetTradesMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 1, SECONDS)); + put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getConfirmPaymentSentMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getCompleteTradeMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 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() ? 30 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getSendChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); }} ))); } From d4eb30bb979c855f71ad9e408ecf14c2b274c9df Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Tue, 4 Mar 2025 17:20:58 -0500 Subject: [PATCH 20/48] schedule import multisig hex on deposit confirmation msg --- .../main/java/haveno/core/trade/Trade.java | 41 ++++++++++++++++--- .../core/trade/protocol/ProcessModel.java | 7 +++- .../ProcessDepositsConfirmedMessage.java | 13 +----- proto/src/main/proto/pb.proto | 1 + 4 files changed, 43 insertions(+), 19 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 9466355f34..13594cc3a1 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; @@ -741,6 +742,11 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } + // handle confirmations + walletHeight.addListener((observable, oldValue, newValue) -> { + importMultisigHexIfScheduled(); + }); + // trade is initialized isInitialized = true; @@ -1077,6 +1083,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 @@ -1141,6 +1167,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 @@ -2350,7 +2379,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); } @@ -2366,11 +2400,6 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Private - /////////////////////////////////////////////////////////////////////////////////////////// - // lazy initialization private ObjectProperty getAmountProperty() { if (tradeAmountProperty == null) 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 d81c4f476a..8521174ca8 100644 --- a/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java +++ b/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java @@ -158,6 +158,9 @@ public class ProcessModel implements Model, PersistablePayload { @Getter @Setter private long tradeProtocolErrorHeight; + @Getter + @Setter + private boolean importMultisigHexScheduled; // 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. @@ -207,7 +210,8 @@ public class ProcessModel implements Model, PersistablePayload { .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 +235,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())); 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/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 6436052333..9bca09b668 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -1581,6 +1581,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 { From 63917fe8ccd8a40e75d921960c01da9e1b44c00c Mon Sep 17 00:00:00 2001 From: woodser Date: Tue, 11 Mar 2025 13:42:10 -0400 Subject: [PATCH 21/48] replace sys.outs with log.info in buyer/seller protocols --- .../java/haveno/core/trade/protocol/BuyerAsMakerProtocol.java | 4 ++-- .../java/haveno/core/trade/protocol/BuyerAsTakerProtocol.java | 4 ++-- .../haveno/core/trade/protocol/SellerAsMakerProtocol.java | 2 +- .../haveno/core/trade/protocol/SellerAsTakerProtocol.java | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) 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/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(); From 34458cf3dfd2aea691c1eacccace8eb2014d950c Mon Sep 17 00:00:00 2001 From: PromptPunksFauxCough <200402670+PromptPunksFauxCough@users.noreply.github.com> Date: Sun, 16 Mar 2025 14:11:17 +0000 Subject: [PATCH 22/48] Install Haveno on Whonix + Qubes (#1628) --- scripts/install_whonix_qubes/INSTALL.md | 401 ++++++++++++++++++ scripts/install_whonix_qubes/README.md | 75 ++++ .../scripts/0-dom0/0.0-dom0.sh | 6 + .../scripts/0-dom0/0.1-dom0.sh | 6 + .../scripts/0-dom0/0.2-dom0.sh | 7 + .../scripts/0-dom0/0.3-dom0.sh | 6 + .../1-TemplateVM/1.0-haveno-templatevm.sh | 185 ++++++++ .../scripts/2-NetVM/2.0-haveno-netvm.sh | 30 ++ .../scripts/3-AppVM/3.0-haveno-appvm.sh | 61 +++ 9 files changed, 777 insertions(+) create mode 100644 scripts/install_whonix_qubes/INSTALL.md create mode 100644 scripts/install_whonix_qubes/README.md create mode 100644 scripts/install_whonix_qubes/scripts/0-dom0/0.0-dom0.sh create mode 100644 scripts/install_whonix_qubes/scripts/0-dom0/0.1-dom0.sh create mode 100644 scripts/install_whonix_qubes/scripts/0-dom0/0.2-dom0.sh create mode 100644 scripts/install_whonix_qubes/scripts/0-dom0/0.3-dom0.sh create mode 100644 scripts/install_whonix_qubes/scripts/1-TemplateVM/1.0-haveno-templatevm.sh create mode 100644 scripts/install_whonix_qubes/scripts/2-NetVM/2.0-haveno-netvm.sh create mode 100644 scripts/install_whonix_qubes/scripts/3-AppVM/3.0-haveno-appvm.sh 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..5bfda7419e --- /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/install_whonix_qubes/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/install_whonix_qubes/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 From b7c9dea5185b410af4b5fb494ae3a102027078a7 Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 16 Mar 2025 10:21:58 -0400 Subject: [PATCH 23/48] fix links in whonix instructions --- scripts/install_whonix_qubes/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/install_whonix_qubes/README.md b/scripts/install_whonix_qubes/README.md index 5bfda7419e..72670e41ca 100644 --- a/scripts/install_whonix_qubes/README.md +++ b/scripts/install_whonix_qubes/README.md @@ -3,7 +3,7 @@ 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/install_whonix_qubes/scripts/install_whonix_qubes/scripts). +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. @@ -72,4 +72,4 @@ $ bash 0.0-dom0.sh && bash 0.1-dom0.sh && bash 0.2-dom0.sh --- -Complete Documentation Can Be Found [Here](https://github.com/haveno-dex/haveno/blob/master/install_whonix_qubes/scripts/install_whonix_qubes/INSTALL.md). +Complete Documentation Can Be Found [Here](https://github.com/haveno-dex/haveno/blob/master/scripts/install_whonix_qubes/INSTALL.md). From cb25a23779855d64b15de5ac9fedb4ce136f35e6 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 14 Mar 2025 08:55:47 -0400 Subject: [PATCH 24/48] refactor message resending, reprocessing, and ack handling --- .../main/java/haveno/core/trade/Trade.java | 77 +++++-------- .../core/trade/protocol/ProcessModel.java | 84 ++++++-------- .../core/trade/protocol/SellerProtocol.java | 49 ++++---- .../haveno/core/trade/protocol/TradePeer.java | 100 +++++++++++++++- .../core/trade/protocol/TradeProtocol.java | 108 ++++++++++++------ .../tasks/BuyerSendPaymentSentMessage.java | 23 ++-- ...yerSendPaymentSentMessageToArbitrator.java | 21 +--- .../BuyerSendPaymentSentMessageToSeller.java | 14 +-- .../SellerSendPaymentReceivedMessage.java | 85 +++++++++++++- ...llerSendPaymentReceivedMessageToBuyer.java | 24 ++++ .../tasks/SendDepositsConfirmedMessage.java | 27 +++-- ...dDepositsConfirmedMessageToArbitrator.java | 12 +- .../SendDepositsConfirmedMessageToBuyer.java | 12 +- .../SendDepositsConfirmedMessageToSeller.java | 12 +- .../pendingtrades/PendingTradesViewModel.java | 3 +- .../steps/seller/SellerStep3View.java | 1 + proto/src/main/proto/pb.proto | 10 +- 17 files changed, 419 insertions(+), 243 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 13594cc3a1..72d77de7d6 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -46,7 +46,6 @@ import haveno.common.taskrunner.Model; import haveno.common.util.Utilities; import haveno.core.monetary.Price; import haveno.core.monetary.Volume; -import haveno.core.network.MessageState; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.OpenOffer; @@ -195,7 +194,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() { @@ -603,12 +603,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); } - } + } /////////////////////////////////////////////////////////////////////////////////////////// @@ -618,8 +618,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 (isPayoutUnlocked() && isCompleted() && !getProtocol().needsToResendPaymentReceivedMessages()) { clearAndShutDown(); return; } @@ -733,15 +734,6 @@ 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? - if (isBuyer()) { - MessageState expectedState = getPaymentSentMessageState(); - if (expectedState != null && expectedState != processModel.getPaymentSentMessageStatePropertySeller().get()) { - log.warn("Updating unexpected payment sent message state for {} {}, expected={}, actual={}", getClass().getSimpleName(), getId(), expectedState, processModel.getPaymentSentMessageStatePropertySeller().get()); - processModel.getPaymentSentMessageStatePropertySeller().set(expectedState); - } - } - // handle confirmations walletHeight.addListener((observable, oldValue, newValue) -> { importMultisigHexIfScheduled(); @@ -771,11 +763,20 @@ 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 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 } @@ -1535,7 +1536,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); } } @@ -2049,25 +2050,6 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { throw new IllegalArgumentException("Trade is not buyer, seller, or arbitrator"); } - public MessageState getPaymentSentMessageState() { - if (isPaymentReceived()) return MessageState.ACKNOWLEDGED; - if (processModel.getPaymentSentMessageStatePropertySeller().get() == MessageState.ACKNOWLEDGED) return MessageState.ACKNOWLEDGED; - switch (state) { - case BUYER_SENT_PAYMENT_SENT_MSG: - return MessageState.SENT; - case BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG: - return MessageState.ARRIVED; - case BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG: - return MessageState.STORED_IN_MAILBOX; - case SELLER_RECEIVED_PAYMENT_SENT_MSG: - return MessageState.ACKNOWLEDGED; - case BUYER_SEND_FAILED_PAYMENT_SENT_MSG: - return MessageState.FAILED; - default: - return null; - } - } - public String getPeerRole(TradePeer peer) { if (peer == getBuyer()) return "Buyer"; if (peer == getSeller()) return "Seller"; @@ -2444,11 +2426,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 @@ -2457,24 +2440,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 messages - getProtocol().maybeReprocessPaymentSentMessage(false); - getProtocol().maybeReprocessPaymentReceivedMessage(false); - HavenoUtils.arbitrationManager.maybeReprocessDisputeClosedMessage(this, false); - startPolling(); } @@ -2825,7 +2802,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/protocol/ProcessModel.java b/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java index 8521174ca8..209e28afd5 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 @@ -161,15 +165,11 @@ public class ProcessModel implements Model, PersistablePayload { @Getter @Setter private boolean importMultisigHexScheduled; - - // 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. - @Setter - private ObjectProperty paymentSentMessageStatePropertySeller = new SimpleObjectProperty<>(MessageState.UNDEFINED); - @Setter - private ObjectProperty paymentSentMessageStatePropertyArbitrator = new SimpleObjectProperty<>(MessageState.UNDEFINED); 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()); @@ -191,6 +191,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; } @@ -245,14 +270,13 @@ public class ProcessModel implements Model, PersistablePayload { processModel.setTradeFeeAddress(ProtoUtil.stringOrNullFromProto(proto.getTradeFeeAddress())); processModel.setMultisigAddress(ProtoUtil.stringOrNullFromProto(proto.getMultisigAddress())); + // 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.setPaymentSentMessageStateSeller(paymentSentMessageStateSeller); - + 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; } @@ -279,40 +303,8 @@ public class ProcessModel implements Model, PersistablePayload { return getP2PService().getAddress(); } - void setPaymentSentAckMessageSeller(AckMessage ackMessage) { - MessageState messageState = ackMessage.isSuccess() ? - MessageState.ACKNOWLEDGED : - MessageState.FAILED; - setPaymentSentMessageStateSeller(messageState); - } - - void setPaymentSentAckMessageArbitrator(AckMessage ackMessage) { - MessageState messageState = ackMessage.isSuccess() ? - MessageState.ACKNOWLEDGED : - MessageState.FAILED; - setPaymentSentMessageStateArbitrator(messageState); - } - - public void setPaymentSentMessageStateSeller(MessageState paymentSentMessageStateProperty) { - this.paymentSentMessageStatePropertySeller.set(paymentSentMessageStateProperty); - if (tradeManager != null) { - tradeManager.requestPersistence(); - } - } - - public void setPaymentSentMessageStateArbitrator(MessageState paymentSentMessageStateProperty) { - this.paymentSentMessageStatePropertyArbitrator.set(paymentSentMessageStateProperty); - if (tradeManager != null) { - tradeManager.requestPersistence(); - } - } - - public boolean isPaymentSentMessageAckedBySeller() { - return paymentSentMessageStatePropertySeller.get() == MessageState.ACKNOWLEDGED; - } - - public boolean isPaymentSentMessageAckedByArbitrator() { - return paymentSentMessageStatePropertyArbitrator.get() == MessageState.ACKNOWLEDGED; + public boolean isPaymentReceivedMessagesReceived() { + return getArbitrator().isPaymentReceivedMessageReceived() && getBuyer().isPaymentReceivedMessageReceived(); } void setDepositTxSentAckMessage(AckMessage ackMessage) { 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 2b7f45877a..4daa800229 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,9 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class SellerProtocol extends DisputeProtocol { + + private static final long RESEND_PAYMENT_RECEIVED_MSGS_AFTER = 1741629525730L; // Mar 10 2025 17:58 UTC + enum SellerEvent implements FluentProtocol.Event { STARTUP, DEPOSIT_TXS_CONFIRMED, @@ -69,31 +72,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.getTakeOfferDate().getTime() > RESEND_PAYMENT_RECEIVED_MSGS_AFTER; + } + @Override protected void onTradeMessage(TradeMessage message, NodeAddress peer) { super.onTradeMessage(message, peer); 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 08c99570f6..512db73983 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -252,6 +252,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 @@ -281,6 +284,10 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D }, trade.getId()); } + public boolean needsToResendPaymentReceivedMessages() { + return false; // seller protocol overrides + } + public void maybeReprocessPaymentSentMessage(boolean reprocessOnError) { if (trade.isShutDownStarted()) return; ThreadUtils.execute(() -> { @@ -291,7 +298,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D return; } - log.warn("Reprocessing payment sent 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()); @@ -307,7 +314,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D return; } - log.warn("Reprocessing payment received message for {} {}", trade.getClass().getSimpleName(), trade.getId()); + log.warn("Reprocessing PaymentReceivedMessage for {} {}", trade.getClass().getSimpleName(), trade.getId()); handle(trade.getSeller().getPaymentReceivedMessage(), trade.getSeller().getPaymentReceivedMessage().getSenderNodeAddress(), reprocessOnError); } }, trade.getId()); @@ -710,47 +717,76 @@ 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.setPaymentSentAckMessageSeller(ackMessage); - trade.setStateIfValidTransitionTo(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG); - processModel.getTradeManager().requestPersistence(); - } else if (trade.getTradePeer(sender) == trade.getArbitrator()) { - processModel.setPaymentSentAckMessageArbitrator(ackMessage); - processModel.getTradeManager().requestPersistence(); - } 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 - } + // 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()); } } 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 bc064399a3..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 @@ -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.getPaymentSentMessageStatePropertySeller().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,8 +193,8 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask if (resendCounter == 0) { listener = (observable, oldValue, newValue) -> onMessageStateChange(newValue); - processModel.getPaymentSentMessageStatePropertySeller().addListener(listener); - onMessageStateChange(processModel.getPaymentSentMessageStatePropertySeller().get()); + getReceiver().getPaymentSentMessageStateProperty().addListener(listener); + onMessageStateChange(getReceiver().getPaymentSentMessageStateProperty().get()); } // first re-send is after 2 minutes, then increase the delay exponentially @@ -212,12 +211,12 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask } 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 cd3098737a..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 @@ -38,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().isPaymentSentMessageAckedByArbitrator(); - } } 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 825220d5b4..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().setPaymentSentMessageStateSeller(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().setPaymentSentMessageStateSeller(MessageState.ARRIVED); + trade.setStateIfValidTransitionTo(Trade.State.BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG); super.setStateArrived(); } @Override protected void setStateStoredInMailbox() { - trade.getProcessModel().setPaymentSentMessageStateSeller(MessageState.STORED_IN_MAILBOX); + trade.setStateIfValidTransitionTo(Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG); super.setStateStoredInMailbox(); } @Override protected void setStateFault() { - trade.getProcessModel().setPaymentSentMessageStateSeller(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.getProcessModel().isPaymentSentMessageAckedBySeller(); - } } 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 fb17d60d4d..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; @@ -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() { @@ -151,7 +159,6 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas } 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/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java index b9936236c6..b19d6dcc26 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 @@ -200,7 +200,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel { onPayoutStateChanged(state); }); - messageStateSubscription = EasyBind.subscribe(trade.getProcessModel().getPaymentSentMessageStatePropertySeller(), this::onPaymentSentMessageStateChanged); + messageStateSubscription = EasyBind.subscribe(trade.getSeller().getPaymentSentMessageStateProperty(), this::onPaymentSentMessageStateChanged); } } } @@ -425,6 +425,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel Date: Thu, 13 Mar 2025 09:08:37 -0400 Subject: [PATCH 25/48] automatically cancel offers with duplicate key images --- .../haveno/core/api/CoreOffersService.java | 1 + .../haveno/core/offer/OpenOfferManager.java | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/core/src/main/java/haveno/core/api/CoreOffersService.java b/core/src/main/java/haveno/core/api/CoreOffersService.java index f036fb13ea..f9c450b825 100644 --- a/core/src/main/java/haveno/core/api/CoreOffersService.java +++ b/core/src/main/java/haveno/core/api/CoreOffersService.java @@ -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/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 32a1507f77..81d2b2b821 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; @@ -888,6 +889,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe List errorMessages = new ArrayList(); synchronized (processOffersLock) { List openOffers = getOpenOffers(); + removeOffersWithDuplicateKeyImages(openOffers); 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 @@ -919,6 +921,28 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe }, THREAD_ID); } + 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 processPendingOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { // skip if already processing From 07fa0b35e40d72ff4142abd6d0a7249bba8d6308 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Thu, 13 Mar 2025 10:42:54 -0400 Subject: [PATCH 26/48] fix error message if arbitrator fails to publish deposit txs --- .../main/java/haveno/core/trade/protocol/ProcessModel.java | 1 + .../protocol/tasks/ArbitratorProcessDepositRequest.java | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) 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 209e28afd5..ae6a27e7f0 100644 --- a/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java +++ b/core/src/main/java/haveno/core/trade/protocol/ProcessModel.java @@ -94,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. 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() { From b19724e33de95c465fe52acd6e68e7c9dd3ca19a Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Fri, 14 Mar 2025 08:26:11 -0400 Subject: [PATCH 27/48] fix summary info not populated on normal payout after dispute --- core/src/main/java/haveno/core/trade/Trade.java | 5 ++--- .../portfolio/pendingtrades/PendingTradesViewModel.java | 8 ++++++-- .../pendingtrades/steps/buyer/BuyerStep4View.java | 8 ++++---- .../pendingtrades/steps/seller/SellerStep4View.java | 2 +- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 72d77de7d6..57767a5757 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -1903,10 +1903,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]); 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 b19d6dcc26..1db4d944c0 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 @@ -416,8 +416,12 @@ public class PendingTradesViewModel extends ActivatableWithDataModel Date: Sat, 15 Mar 2025 06:45:40 -0400 Subject: [PATCH 28/48] share dispute opener's updated multisig info on dispute opened --- .../haveno/core/support/dispute/DisputeManager.java | 7 ++++--- .../support/dispute/messages/DisputeOpenedMessage.java | 10 +++++----- proto/src/main/proto/pb.proto | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) 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..92512b56a8 100644 --- a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java @@ -578,8 +578,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 +606,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/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/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index a814f71e4a..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; } From 51fc4d0c41d625174939687b8596ed64b5717786 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Sat, 15 Mar 2025 06:44:34 -0400 Subject: [PATCH 29/48] do not export multisig info on dispute opened --- .../java/haveno/core/support/dispute/DisputeManager.java | 7 ------- .../portfolio/pendingtrades/PendingTradesDataModel.java | 7 ------- 2 files changed, 14 deletions(-) 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 92512b56a8..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, 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(); From d7be2885bdd5d866cccd5342e63c4eeb3db77665 Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 20 Mar 2025 08:00:23 -0400 Subject: [PATCH 30/48] fix error fetching prices with --socks5ProxyXmrAddress config (#1658) --- p2p/src/main/java/haveno/network/Socks5ProxyProvider.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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)); } From 5711aabad83e1cbcab0d494fba47aac52b841f94 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Tue, 18 Mar 2025 10:18:15 -0400 Subject: [PATCH 31/48] remove outdated code for v1.0.7 update --- .../support/dispute/arbitration/ArbitrationManager.java | 9 --------- .../protocol/tasks/ProcessPaymentReceivedMessage.java | 8 -------- .../tasks/SellerPreparePaymentReceivedMessage.java | 7 ------- 3 files changed, 24 deletions(-) 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 Date: Sun, 16 Mar 2025 18:18:48 -0400 Subject: [PATCH 32/48] do not process payment confirmation messages if shut down started --- .../haveno/core/trade/protocol/TradeProtocol.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) 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 512db73983..372c26b5da 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -291,10 +291,11 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D 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.isBuyer() || trade.getBuyer().getPaymentSentMessage() == null || trade.getState().ordinal() >= Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal()) { + if (trade.isShutDownStarted() || trade.isBuyer() || trade.getBuyer().getPaymentSentMessage() == null || trade.getState().ordinal() >= Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal()) { return; } @@ -307,10 +308,11 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D 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.isSeller() || trade.getSeller().getPaymentReceivedMessage() == null || (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && trade.isPayoutPublished())) { + if (trade.isShutDownStarted() || trade.isSeller() || trade.getSeller().getPaymentReceivedMessage() == null || (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && trade.isPayoutPublished())) { return; } @@ -525,7 +527,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D trade.getBuyer().setPaymentSentMessage(message); trade.requestPersistence(); - if (!trade.isInitialized() || trade.isShutDown()) return; + if (!trade.isInitialized() || trade.isShutDownStarted()) return; if (!(trade instanceof SellerTrade || trade instanceof ArbitratorTrade)) { log.warn("Ignoring PaymentSentMessage since not seller or arbitrator"); return; @@ -537,7 +539,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); @@ -604,14 +606,14 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D trade.getSeller().setPaymentReceivedMessage(message); trade.requestPersistence(); - if (!trade.isInitialized() || trade.isShutDown()) return; + 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); From 79aa214f2282e167c8b81346180ce4e4f66d4816 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Mon, 17 Mar 2025 07:47:39 -0400 Subject: [PATCH 33/48] check failed state to determine if payment sent or received --- core/src/main/java/haveno/core/trade/Trade.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 57767a5757..0f6606c91a 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -2171,7 +2171,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() { @@ -2189,7 +2189,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() { From 5107c6ba578cd658f06b92e554eb0022d15944e0 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Mon, 17 Mar 2025 08:32:34 -0400 Subject: [PATCH 34/48] fixes to process payout tx and revert to payment sent state --- .../main/java/haveno/core/trade/Trade.java | 29 ++++++++++++++----- .../core/trade/protocol/SellerProtocol.java | 2 +- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 0f6606c91a..75ef6ead4c 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -644,7 +644,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // 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); + resetToPaymentSentState(); } // handle trade state events @@ -770,6 +770,12 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { isFullyInitialized = true; } + 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); @@ -1365,7 +1371,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"); @@ -1396,6 +1402,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(); @@ -1404,19 +1413,15 @@ 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); } - // 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()); @@ -1426,6 +1431,14 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { 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); } // save trade state 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 4daa800229..021de47450 100644 --- a/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java @@ -146,7 +146,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)) From 2af9019db0d5d8d1cfa4523b5dbd7faa8ae17fe0 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Mon, 17 Mar 2025 09:24:16 -0400 Subject: [PATCH 35/48] do not reset payment states if payout published --- .../main/java/haveno/core/trade/Trade.java | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 75ef6ead4c..5a4ddeb2a4 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -635,16 +635,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); - resetToPaymentSentState(); + // 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 From 26e3a153bc28ca34c222152ba9930b51075cc165 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Mon, 17 Mar 2025 10:14:33 -0400 Subject: [PATCH 36/48] process messages until trade is completely finished --- core/src/main/java/haveno/core/trade/Trade.java | 6 +++++- .../haveno/core/trade/protocol/TradeProtocol.java | 15 ++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 5a4ddeb2a4..19cab12662 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -620,7 +620,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // skip initialization if trade is complete // starting in v1.0.19, seller resends payment received message until acked or stored in mailbox - if (isPayoutUnlocked() && isCompleted() && !getProtocol().needsToResendPaymentReceivedMessages()) { + if (isFinished()) { clearAndShutDown(); return; } @@ -774,6 +774,10 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { 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); 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 372c26b5da..290e2cce6e 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -165,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 } } @@ -210,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()); } @@ -242,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()) { @@ -719,6 +714,9 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D private void onAckMessage(AckMessage ackMessage, NodeAddress sender) { + // ignore if trade is completely finished + if (trade.isFinished()) return; + // get trade peer TradePeer peer = trade.getTradePeer(sender); if (peer == null) { @@ -791,6 +789,9 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D 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) { From bee86daff3b06636251becd9ed5230dee03d1833 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Tue, 18 Mar 2025 13:40:11 -0400 Subject: [PATCH 37/48] increase grpc rate limit for offers and trades on testnet --- .../haveno/daemon/grpc/GrpcOffersService.java | 12 ++++++------ .../haveno/daemon/grpc/GrpcTradesService.java | 16 ++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcOffersService.java index 485ff38ca8..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() ? 30 : 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 0a5d2d39d2..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() ? 30 : 1, SECONDS)); - put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getConfirmPaymentSentMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getCompleteTradeMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 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() ? 30 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getSendChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 30 : 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)); }} ))); } From b5f9bc307baf2974f740b0134e476ef4074ed66c Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Thu, 20 Mar 2025 08:04:30 -0400 Subject: [PATCH 38/48] verify offer versions when signing --- .../main/java/haveno/common/app/Version.java | 19 +++++++++++++ .../haveno/core/app/DomainInitialisation.java | 7 ++--- .../haveno/core/filter/FilterManager.java | 4 +++ .../haveno/core/offer/OpenOfferManager.java | 28 ++++++++++++++++++- .../core/xmr/wallet/XmrWalletService.java | 4 +-- 5 files changed, 55 insertions(+), 7 deletions(-) diff --git a/common/src/main/java/haveno/common/app/Version.java b/common/src/main/java/haveno/common/app/Version.java index 74f3ceb9d6..1d7b99f747 100644 --- a/common/src/main/java/haveno/common/app/Version.java +++ b/common/src/main/java/haveno/common/app/Version.java @@ -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); 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/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 81d2b2b821..91e2284d46 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -737,7 +737,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); @@ -1396,6 +1396,32 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe 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) { 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 97aef9545f..7bfc37f8e2 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -767,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; @@ -785,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); } From c95a26e043ce4c5c77c9a2515242a6128356e897 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Tue, 18 Mar 2025 18:38:47 -0400 Subject: [PATCH 39/48] re-sign offers on protocol version update --- .../haveno/core/offer/OpenOfferManager.java | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 91e2284d46..6ae03a7042 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -1876,27 +1876,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(); From 028ced70212353fb3cafe46f5e4581f89f63b629 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Tue, 18 Mar 2025 18:44:30 -0400 Subject: [PATCH 40/48] bump offer protocol version, do not verify miner fee for outdated trades --- .../main/java/haveno/common/app/Version.java | 5 +++-- .../main/java/haveno/core/trade/Trade.java | 22 +++++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/common/src/main/java/haveno/common/app/Version.java b/common/src/main/java/haveno/common/app/Version.java index 1d7b99f747..3dc04889b8 100644 --- a/common/src/main/java/haveno/common/app/Version.java +++ b/common/src/main/java/haveno/common/app/Version.java @@ -110,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/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index 19cab12662..eaec78a6b0 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -1430,15 +1430,19 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { throw new IllegalStateException(e); } - // 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); + // 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); From a7d8e4560f84d4f31460dacbb8ab6ab7fdace2e2 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Tue, 18 Mar 2025 19:10:56 -0400 Subject: [PATCH 41/48] enable resending payment received msgs based on offer protocol version --- .../main/java/haveno/core/trade/protocol/SellerProtocol.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 021de47450..2a01c5a2ca 100644 --- a/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java @@ -54,8 +54,6 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class SellerProtocol extends DisputeProtocol { - private static final long RESEND_PAYMENT_RECEIVED_MSGS_AFTER = 1741629525730L; // Mar 10 2025 17:58 UTC - enum SellerEvent implements FluentProtocol.Event { STARTUP, DEPOSIT_TXS_CONFIRMED, @@ -100,7 +98,7 @@ public class SellerProtocol extends DisputeProtocol { } private boolean resendPaymentReceivedMessagesEnabled() { - return trade.getTakeOfferDate().getTime() > RESEND_PAYMENT_RECEIVED_MSGS_AFTER; + return trade.getOffer().getOfferPayload().getProtocolVersion() >= 2; } @Override From ce27818f437963b441c9d15706e61217c38b08e2 Mon Sep 17 00:00:00 2001 From: woodser <13068859+woodser@users.noreply.github.com> Date: Thu, 20 Mar 2025 08:02:01 -0400 Subject: [PATCH 42/48] recover payment sent message state on startup if undefined --- .../main/java/haveno/core/trade/Trade.java | 29 +++++++++++++++++++ .../pendingtrades/PendingTradesViewModel.java | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index eaec78a6b0..114edcabe5 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -46,6 +46,7 @@ import haveno.common.taskrunner.Model; import haveno.common.util.Utilities; import haveno.core.monetary.Price; import haveno.core.monetary.Volume; +import haveno.core.network.MessageState; import haveno.core.offer.Offer; import haveno.core.offer.OfferDirection; import haveno.core.offer.OpenOffer; @@ -738,6 +739,15 @@ 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 mismatch in v1.0.19, but can this check can be removed? + if (isBuyer()) { + MessageState expectedState = getPaymentSentMessageState(); + 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(); @@ -2074,6 +2084,25 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { throw new IllegalArgumentException("Trade is not buyer, seller, or arbitrator"); } + private MessageState getPaymentSentMessageState() { + if (isPaymentReceived()) return MessageState.ACKNOWLEDGED; + if (getSeller().getPaymentSentMessageStateProperty().get() == MessageState.ACKNOWLEDGED) return MessageState.ACKNOWLEDGED; + switch (state) { + case BUYER_SENT_PAYMENT_SENT_MSG: + return MessageState.SENT; + case BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG: + return MessageState.ARRIVED; + case BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG: + return MessageState.STORED_IN_MAILBOX; + case SELLER_RECEIVED_PAYMENT_SENT_MSG: + return MessageState.ACKNOWLEDGED; + case BUYER_SEND_FAILED_PAYMENT_SENT_MSG: + return MessageState.FAILED; + default: + return null; + } + } + public String getPeerRole(TradePeer peer) { if (peer == getBuyer()) return "Buyer"; if (peer == getSeller()) return "Seller"; 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 1db4d944c0..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 @@ -424,7 +424,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel Date: Sat, 22 Mar 2025 07:57:31 -0400 Subject: [PATCH 43/48] limit offer extra info to 1500 characters --- .../haveno/core/offer/OpenOfferManager.java | 8 + .../haveno/core/xmr/wallet/Restrictions.java | 1 + .../resources/i18n/displayStrings.properties | 1 + .../desktop/components/InputTextArea.java | 140 ++++++++++++++++++ .../src/main/java/haveno/desktop/haveno.css | 6 +- .../desktop/main/offer/MutableOfferView.java | 14 +- .../main/offer/MutableOfferViewModel.java | 55 ++++--- 7 files changed, 195 insertions(+), 30 deletions(-) create mode 100644 desktop/src/main/java/haveno/desktop/components/InputTextArea.java diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 6ae03a7042..a08b767a47 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -1396,6 +1396,14 @@ 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(); 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/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index cb6a632270..82a09919ba 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -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 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/main/offer/MutableOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java index e3f9132a84..1ed15ad845 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"), @@ -592,6 +591,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 +713,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 +1097,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 +1109,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 e32869afe2..6d087b27ea 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); @@ -500,11 +501,7 @@ public abstract class MutableOfferViewModel ext }; extraInfoStringListener = (ov, oldValue, newValue) -> { - if (newValue != null) { - extraInfo.set(newValue); - } else { - extraInfo.set(""); - } + onExtraInfoTextAreaChanged(); }; isWalletFundedListener = (ov, oldValue, newValue) -> updateButtonDisableState(); @@ -531,7 +528,7 @@ public abstract class MutableOfferViewModel ext tradeFee.set(HavenoUtils.formatXmr(makerFee)); tradeFeeInXmrWithFiat.set(OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil, dataModel.getMaxMakerFee(), - btcFormatter)); + xmrFormatter)); } @@ -836,8 +833,16 @@ public abstract class MutableOfferViewModel ext } } - public void onFocusOutExtraInfoTextField(boolean oldValue, boolean newValue) { + public void onFocusOutExtraInfoTextArea(boolean oldValue, boolean newValue) { if (oldValue && !newValue) { + onExtraInfoTextAreaChanged(); + } + } + + public void onExtraInfoTextAreaChanged() { + extraInfoValidationResult.set(getExtraInfoValidationResult()); + updateButtonDisableState(); + if (extraInfoValidationResult.get().isValid) { dataModel.setExtraInfo(extraInfo.get()); } } @@ -1045,8 +1050,8 @@ public abstract class MutableOfferViewModel ext .show(); } - CoinFormatter getBtcFormatter() { - return btcFormatter; + CoinFormatter getXmrFormatter() { + return xmrFormatter; } public boolean isShownAsBuyOffer() { @@ -1064,7 +1069,7 @@ public abstract class MutableOfferViewModel ext public String getTradeAmount() { return OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil, dataModel.getAmount().get(), - btcFormatter); + xmrFormatter); } public String getSecurityDepositLabel() { @@ -1084,7 +1089,7 @@ public abstract class MutableOfferViewModel ext return OfferViewModelUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil, dataModel.getSecurityDeposit(), dataModel.getAmount().get(), - btcFormatter + xmrFormatter ); } @@ -1097,7 +1102,7 @@ public abstract class MutableOfferViewModel ext return OfferViewModelUtil.getTradeFeeWithFiatEquivalentAndPercentage(offerUtil, dataModel.getMaxMakerFee(), dataModel.getAmount().get(), - btcFormatter); + xmrFormatter); } public String getMakerFeePercentage() { @@ -1108,7 +1113,7 @@ public abstract class MutableOfferViewModel ext public String getTotalToPayInfo() { return OfferViewModelUtil.getTradeFeeWithFiatEquivalent(offerUtil, dataModel.totalToPay.get(), - btcFormatter); + xmrFormatter); } public String getFundsStructure() { @@ -1181,7 +1186,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 +1207,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 +1348,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); From 82728aef6965e96203a4146af039e0372a16d466 Mon Sep 17 00:00:00 2001 From: boldsuck Date: Sat, 22 Mar 2025 12:58:05 +0100 Subject: [PATCH 44/48] Change http links to https in LICENSE (#1660) --- LICENSE | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 -. +. From b3174518d9058701b7ce1f0837b6b9329ee7703e Mon Sep 17 00:00:00 2001 From: woodser Date: Sat, 22 Mar 2025 08:34:50 -0400 Subject: [PATCH 45/48] add vscode to IDE documentation --- docs/import-haveno.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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. From ad809ff20e331353976244f1e297395d2d49b060 Mon Sep 17 00:00:00 2001 From: boldsuck Date: Sun, 23 Mar 2025 13:00:26 +0100 Subject: [PATCH 46/48] Add price node (#1661) --- .../main/java/haveno/core/provider/ProvidersRepository.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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; From 207ff5416c9e7be551779e43975804ce8d57a6a1 Mon Sep 17 00:00:00 2001 From: Brandon Trussell Date: Tue, 25 Mar 2025 10:49:26 +0000 Subject: [PATCH 47/48] Support linux aarch64 (#1665) --- build.gradle | 2 +- gradle/verification-metadata.xml | 53 +++++++++++++++++--------------- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/build.gradle b/build.gradle index 3c3d95d074..cb1cc172b2 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 = 'd4f9d0ce24' // Tor browser version 14.0.7 and tor binary version: 0.4.8.14 + 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' diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index cbfb839d9f..15194cfc52 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -205,44 +205,49 @@
- - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + + + + + + + From 29e6540234046c9d3b4627ed9fc5c6db44aa09f7 Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 26 Mar 2025 09:42:00 -0400 Subject: [PATCH 48/48] fix missing edit icon on offers view --- desktop/src/main/java/haveno/desktop/images.css | 4 ---- .../haveno/desktop/main/offer/offerbook/OfferBookView.java | 5 +++-- 2 files changed, 3 insertions(+), 6 deletions(-) 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/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);