From 61a62a1d942babbdb57da9eedc966376a29b0fdd Mon Sep 17 00:00:00 2001
From: boldsuck <git@boldsuck.de>
Date: Sun, 9 Mar 2025 01:42:53 +0100
Subject: [PATCH 01/21] 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/21] 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<T extends Overlay<T>> {
                 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<T extends Overlay<T>> {
         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<QRCodeWindow> {
     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<QRCodeWindow> {
         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<QRCodeWindow> {
         applyStyles();
         display();
     }
+
+    public String getClipboardText() {
+        return moneroUri;
+    }
 }

From e5f729d12f8389a50a0a3b6113ec23f56ac3ed44 Mon Sep 17 00:00:00 2001
From: boldsuck <git@boldsuck.de>
Date: Sun, 9 Mar 2025 19:32:32 +0100
Subject: [PATCH 03/21] 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 @@
             <sha256 value="d4ea711258c783e0accb8feaaa204f0414781551b0159fa17e5f1869200f96f7" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <component group="com.github.haveno-dex.netlayer" name="tor" version="700ec94f0f">
-         <artifact name="tor-700ec94f0f.jar">
-            <sha256 value="63eafc2bf43ae2556d5a24f23b5ddfe371c1ac01b7bc595d6fdb7eadcba37d52" origin="Generated by Gradle"/>
+      <component group="com.github.haveno-dex.netlayer" name="tor" version="d4f9d0ce24">
+         <artifact name="tor-d4f9d0ce24.jar">
+            <sha256 value="58c7df7cab675ec3db4f80b456282d49e8d983676af7f053c7514fc5dfb83cee" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <component group="com.github.haveno-dex.netlayer" name="tor.external" version="700ec94f0f">
-         <artifact name="tor.external-700ec94f0f.jar">
-            <sha256 value="01b506ec84697a08abfad2ab1928a8ba8bda36f588f05fbb14e5b9cacdbd0e3d" origin="Generated by Gradle"/>
+      <component group="com.github.haveno-dex.netlayer" name="tor.external" version="d4f9d0ce24">
+         <artifact name="tor.external-d4f9d0ce24.jar">
+            <sha256 value="1231429367e83f7c7536cf6febd4df2e95fec81a776f199791d5b9790aa1d25e" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <component group="com.github.haveno-dex.netlayer" name="tor.native" version="700ec94f0f">
-         <artifact name="tor.native-700ec94f0f.jar">
-            <sha256 value="51886b73f9c41d1d16ab7995af9846f67d6f7942def0c182b56a2a801ae301d3" origin="Generated by Gradle"/>
+      <component group="com.github.haveno-dex.netlayer" name="tor.native" version="d4f9d0ce24">
+         <artifact name="tor.native-d4f9d0ce24.jar">
+            <sha256 value="5d83e8fb429e0fe79df55b220731567b9290f84bd2f1ee03b2a194284290e052" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <component group="com.github.haveno-dex.tor-binary" name="tor-binary-geoip" version="6bdcb0303ae92f4b7bd7102706467f10fcac4d9e">
-         <artifact name="tor-binary-geoip-6bdcb0303ae92f4b7bd7102706467f10fcac4d9e.jar">
-            <sha256 value="7bd0fa5e818825d8f0c87a52cc0c468a06fd7850c825b9b36ba82d7a3d9f2fa5" origin="Generated by Gradle"/>
+      <component group="com.github.haveno-dex.tor-binary" name="tor-binary-geoip" version="462c44c157cbf0b7574b7ab14d0bf231df770a63">
+         <artifact name="tor-binary-geoip-462c44c157cbf0b7574b7ab14d0bf231df770a63.jar">
+            <sha256 value="af2c7a517d45c7640f11c28fa5987201ddcb4ea139ef4d56fbcff17336a83289" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <component group="com.github.haveno-dex.tor-binary" name="tor-binary-linux32" version="6bdcb0303ae92f4b7bd7102706467f10fcac4d9e">
-         <artifact name="tor-binary-linux32-6bdcb0303ae92f4b7bd7102706467f10fcac4d9e.jar">
-            <sha256 value="4b4b3d822d8ad88f874450385751d0b26b41e2724d0d9b703acd9e4b73b3ba5d" origin="Generated by Gradle"/>
+      <component group="com.github.haveno-dex.tor-binary" name="tor-binary-linux32" version="462c44c157cbf0b7574b7ab14d0bf231df770a63">
+         <artifact name="tor-binary-linux32-462c44c157cbf0b7574b7ab14d0bf231df770a63.jar">
+            <sha256 value="6ee4cb8f9cd33bb255fa0e9991e9be49338f574999270434a4499b4554e5e714" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <component group="com.github.haveno-dex.tor-binary" name="tor-binary-linux64" version="6bdcb0303ae92f4b7bd7102706467f10fcac4d9e">
-         <artifact name="tor-binary-linux64-6bdcb0303ae92f4b7bd7102706467f10fcac4d9e.jar">
-            <sha256 value="512b6d52217feed0efe84c1f43888fc8a8ba32a8998486c32e233a031dddbd94" origin="Generated by Gradle"/>
+      <component group="com.github.haveno-dex.tor-binary" name="tor-binary-linux64" version="462c44c157cbf0b7574b7ab14d0bf231df770a63">
+         <artifact name="tor-binary-linux64-462c44c157cbf0b7574b7ab14d0bf231df770a63.jar">
+            <sha256 value="7da86fc024976ab46b6e92f4f5c0046b7df1f800cd09f297232698b1a6b7f961" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <component group="com.github.haveno-dex.tor-binary" name="tor-binary-macos" version="6bdcb0303ae92f4b7bd7102706467f10fcac4d9e">
-         <artifact name="tor-binary-macos-6bdcb0303ae92f4b7bd7102706467f10fcac4d9e.jar">
-            <sha256 value="f68f9c6a3f56d084bd9426ff3834bcc90b07a4489357b04ef8151465a73c8783" origin="Generated by Gradle"/>
+      <component group="com.github.haveno-dex.tor-binary" name="tor-binary-macos" version="462c44c157cbf0b7574b7ab14d0bf231df770a63">
+         <artifact name="tor-binary-macos-462c44c157cbf0b7574b7ab14d0bf231df770a63.jar">
+            <sha256 value="f1cd51f9acf7562190fc1ea327325116fcd006f98e16eaef279554f20cca86c3" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <component group="com.github.haveno-dex.tor-binary" name="tor-binary-windows" version="6bdcb0303ae92f4b7bd7102706467f10fcac4d9e">
-         <artifact name="tor-binary-windows-6bdcb0303ae92f4b7bd7102706467f10fcac4d9e.jar">
-            <sha256 value="94f7090f34dc6f12cdd5e5a247f7f0c4aeb40693258fd5365db41a2b8a71b197" origin="Generated by Gradle"/>
+      <component group="com.github.haveno-dex.tor-binary" name="tor-binary-windows" version="462c44c157cbf0b7574b7ab14d0bf231df770a63">
+         <artifact name="tor-binary-windows-462c44c157cbf0b7574b7ab14d0bf231df770a63.jar">
+            <sha256 value="39ae9a91803dae23fc5573f46f136b4ee973babfd72c3495963f1c430d20f162" origin="Generated by Gradle"/>
          </artifact>
       </component>
       <component group="com.github.johnrengelman" name="shadow" version="8.1.1">

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/21] 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 @@
   </content_rating>
 
   <releases>
-    <release version="1.0.18" date="2025-01-19"/>
+    <release version="1.0.19" date="2025-03-10"/>
   </releases>
 </component>
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 @@
         <!-- See: https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -->
 
         <key>CFBundleVersion</key>
-        <string>1.0.18</string>
+        <string>1.0.19</string>
 
         <key>CFBundleShortVersionString</key>
-        <string>1.0.18</string>
+        <string>1.0.19</string>
 
         <key>CFBundleExecutable</key>
         <string>Haveno</string>
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
 
 <b>Set the mandatory minimum version for trading (optional)</b>
 
-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".
 
 <b>Send update alert</b>
 
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 <woodser@protonmail.com>
Date: Sun, 9 Mar 2025 17:02:15 -0400
Subject: [PATCH 05/21] 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<PrivateNotificationPayload> displayPrivateNotificationHandler;
     @Setter
     @Nullable
-    private Runnable showPopupIfInvalidBtcConfigHandler;
+    private Runnable showPopupIfInvalidXmrConfigHandler;
     @Setter
     @Nullable
     private Consumer<List<RevolutAccount>> 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<String> 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/21] 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/21] 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/21] 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<Trade> 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/21] 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<OpenOffer> openOfferOptional = getOpenOfferById(offer.getId());
+                Optional<OpenOffer> 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<OpenOffer> openOfferOptional = getOpenOfferById(offer.getId());
+        Optional<OpenOffer> 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<OpenOffer> openOfferOptional = getOpenOfferById(editedOffer.getId());
+        Optional<OpenOffer> 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<OpenOffer> getOpenOfferById(String offerId) {
+    public Optional<OpenOffer> 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<SignedOffer> getSignedOfferById(String offerId) {
@@ -1575,7 +1575,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
         }
 
         try {
-            Optional<OpenOffer> openOfferOptional = getOpenOfferById(request.offerId);
+            Optional<OpenOffer> 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<MediationDisputeList>
                 tradeManager.requestPersistence();
             }
         } else {
-            Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(tradeId);
+            Optional<OpenOffer> 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<RefundDisputeList> {
                 tradeManager.requestPersistence();
             }
         } else {
-            Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(tradeId);
+            Optional<OpenOffer> 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<RefundDisputeList> {
         if (tradeManager.getOpenTrade(tradeId).isPresent()) {
             tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.REFUND_REQUEST_CLOSED);
         } else {
-            Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(tradeId);
+            Optional<OpenOffer> 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> openOffer = processModel.getOpenOfferManager().getOpenOfferById(this.getId());
+        Optional<OpenOffer> 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<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(request.getOfferId());
+            Optional<OpenOffer> 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<XmrAddressEntry> getAddressEntriesForAvailableBalanceStream() {
         Stream<XmrAddressEntry> 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<VBox, Void> {
         Optional<Trade> 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<VBox, Void> {
         Optional<Trade> 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.<Tradable>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<M extends MutableOfferDataModel> ext
         createOfferRequested = false;
         createOfferCanceled = true;
         OpenOfferManager openOfferManager = HavenoUtils.openOfferManager;
-        Optional<OpenOffer> openOffer = openOfferManager.getOpenOfferById(offer.getId());
+        Optional<OpenOffer> 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<OfferDetailsWindow> {
         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/21] 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<OpenOffer> 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<OpenOffer> 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<OpenOffer> 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<PlaceOfferModel> {
                             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<PlaceOfferModel> {
                     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/21] 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<OpenOffer> 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<OpenOffer> 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/21] 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/21] 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<MessageState> paymentSentMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED);
+    private ObjectProperty<MessageState> paymentSentMessageStatePropertySeller = new SimpleObjectProperty<>(MessageState.UNDEFINED);
     @Setter
     private ObjectProperty<MessageState> paymentSentMessageStatePropertyArbitrator = new SimpleObjectProperty<>(MessageState.UNDEFINED);
     private ObjectProperty<Boolean> 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<PendingTrad
     private final ObjectProperty<BuyerState> buyerState = new SimpleObjectProperty<>();
     private final ObjectProperty<SellerState> sellerState = new SimpleObjectProperty<>();
     @Getter
-    private final ObjectProperty<MessageState> messageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED);
+    private final ObjectProperty<MessageState> paymentSentMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED);
     private Subscription tradeStateSubscription;
     private Subscription paymentAccountDecryptedSubscription;
     private Subscription payoutStateSubscription;
@@ -186,7 +186,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
 
             if (messageStateSubscription != null) {
                 messageStateSubscription.unsubscribe();
-                messageStateProperty.set(MessageState.UNDEFINED);
+                paymentSentMessageStateProperty.set(MessageState.UNDEFINED);
             }
 
             if (selectedItem != null) {
@@ -200,7 +200,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
                 payoutStateSubscription = EasyBind.subscribe(trade.payoutStateProperty(), state -> {
                     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<PendingTrad
         });
     }
 
-    private void onMessageStateChanged(MessageState messageState) {
-        messageStateProperty.set(messageState);
+    private void onPaymentSentMessageStateChanged(MessageState messageState) {
+        paymentSentMessageStateProperty.set(messageState);
     }
 
     ///////////////////////////////////////////////////////////////////////////////////////////
diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep3View.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep3View.java
index 57fda229f3..b28eda4a10 100644
--- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep3View.java
+++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep3View.java
@@ -52,7 +52,7 @@ public class BuyerStep3View extends TradeStepView {
     public void activate() {
         super.activate();
 
-        model.getMessageStateProperty().addListener(messageStateChangeListener);
+        model.getPaymentSentMessageStateProperty().addListener(messageStateChangeListener);
 
         updateMessageStateInfo();
     }
@@ -60,7 +60,7 @@ public class BuyerStep3View extends TradeStepView {
     public void deactivate() {
         super.deactivate();
 
-        model.getMessageStateProperty().removeListener(messageStateChangeListener);
+        model.getPaymentSentMessageStateProperty().removeListener(messageStateChangeListener);
     }
 
 
@@ -87,7 +87,7 @@ public class BuyerStep3View extends TradeStepView {
     }
 
     private void updateMessageStateInfo() {
-        MessageState messageState = model.getMessageStateProperty().get();
+        MessageState messageState = model.getPaymentSentMessageStateProperty().get();
         textFieldWithIcon.setText(Res.get("message.state." + messageState.name()));
         Label iconLabel = textFieldWithIcon.getIconLabel();
         switch (messageState) {
diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto
index 04c32ee8dc..6436052333 100644
--- a/proto/src/main/proto/pb.proto
+++ b/proto/src/main/proto/pb.proto
@@ -1568,7 +1568,7 @@ message ProcessModel {
     bytes payout_tx_signature = 4;
     bool use_savings_wallet = 5;
     int64 funds_needed_for_trade = 6;
-    string payment_sent_message_state = 7;
+    string payment_sent_message_state_seller = 7;
     string payment_sent_message_state_arbitrator = 8;
     bytes maker_signature = 9;
     TradePeer maker = 10;

From 38c0855728b08c36bb7846d834bb53f3b2e315aa Mon Sep 17 00:00:00 2001
From: woodser <13068859+woodser@users.noreply.github.com>
Date: Mon, 3 Mar 2025 07:55:57 -0500
Subject: [PATCH 14/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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<BigInteger> 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 <woodser@protonmail.com>
Date: Tue, 11 Mar 2025 13:42:10 -0400
Subject: [PATCH 21/21] 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();