From c08b4d077263e47771e611d57bff335f28601edb Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 12 Sep 2024 11:41:26 -0400 Subject: [PATCH 01/21] print stack trace on error closing dispute ticket --- .../desktop/main/overlays/windows/DisputeSummaryWindow.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java index 5f7fef32..7a906ec7 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java @@ -678,7 +678,8 @@ public class DisputeSummaryWindow extends Overlay { closeTicketButton.disableProperty().unbind(); hide(); }, (errMessage, err) -> { - log.error(errMessage); + log.error("Error closing dispute ticket: " + errMessage); + err.printStackTrace(); new Popup().error(err.toString()).show(); }); } From 1ea9a6f750465806dd635b87c2252881226b3591 Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 12 Sep 2024 09:28:20 -0400 Subject: [PATCH 02/21] add notes for tails installation --- scripts/install_tails/README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/install_tails/README.md b/scripts/install_tails/README.md index aa9f81ce..5619efe0 100644 --- a/scripts/install_tails/README.md +++ b/scripts/install_tails/README.md @@ -2,7 +2,7 @@ Install Haveno on Tails by following these steps: -1. Enable persistent storage dotfiles and admin password before starting tails. +1. Enable persistent storage dotfiles and admin password before starting Tails. 2. Download [haveno-install.sh](haveno-install.sh): ``` @@ -22,3 +22,9 @@ Install Haveno on Tails by following these steps: ``` 4. Upon successful execution of the script (no errors), the Haveno release will be installed to persistent storage and can be launched via the desktop shortcut in the 'Other' section of the start menu. + +> [!note] +> If you have already installed Haveno on Tails, we recommend moving your data directory (/home/amnesia/Persistent/Haveno-example) to the new default location (/home/amnesia/Persistent/haveno/Data/Haveno-example), to retain your history and for future support. + +> [!note] +> Modern versions of Tails will invoke `curl` over Tor, but if your installation does not, then you can add `--socks5-hostname 127.0.0.1:9050` when invoking the install script. \ No newline at end of file From 7306972d19cff82633395776b6808bf92bbf9460 Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 13 Sep 2024 12:17:37 -0400 Subject: [PATCH 03/21] fix reference to backup cache file --- .../java/haveno/core/xmr/wallet/XmrWalletService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 325bdc13..55b09682 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -1503,15 +1503,15 @@ public class XmrWalletService extends XmrWalletBase { } // handle success or failure + File originalCacheBackup = new File(cachePath + ".backup"); if (retrySuccessful) { - originalCacheFile.delete(); // delete original wallet cache backup + if (originalCacheBackup.exists()) originalCacheBackup.delete(); // delete original wallet cache backup } else { // restore original wallet cache log.warn("Failed to open full wallet using backup cache, restoring original cache"); File cacheFile = new File(cachePath); if (cacheFile.exists()) cacheFile.delete(); - File originalCacheBackup = new File(cachePath + ".backup"); if (originalCacheBackup.exists()) originalCacheBackup.renameTo(new File(cachePath)); // throw exception @@ -1607,15 +1607,15 @@ public class XmrWalletService extends XmrWalletBase { } // handle success or failure + File originalCacheBackup = new File(cachePath + ".backup"); if (retrySuccessful) { - originalCacheFile.delete(); // delete original wallet cache backup + if (originalCacheBackup.exists()) originalCacheBackup.delete(); // delete original wallet cache backup } else { // restore original wallet cache log.warn("Failed to open RPC wallet using backup cache, restoring original cache"); File cacheFile = new File(cachePath); if (cacheFile.exists()) cacheFile.delete(); - File originalCacheBackup = new File(cachePath + ".backup"); if (originalCacheBackup.exists()) originalCacheBackup.renameTo(new File(cachePath)); // throw exception From a4e43f104518971a1fad524908f93068a6891e52 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 16 Sep 2024 20:04:57 -0400 Subject: [PATCH 04/21] update to monero-java v0.8.33 --- build.gradle | 2 +- gradle/verification-metadata.xml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 83d42efb..22657efd 100644 --- a/build.gradle +++ b/build.gradle @@ -49,7 +49,7 @@ configure(subprojects) { gsonVersion = '2.8.5' guavaVersion = '32.1.1-jre' guiceVersion = '7.0.0' - moneroJavaVersion = '0.8.31' + moneroJavaVersion = '0.8.33' httpclient5Version = '5.0' hamcrestVersion = '2.2' httpclientVersion = '4.5.12' diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index dc626c93..5bc11c01 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -878,9 +878,9 @@ - - - + + + From d4f1dc5b8e7bce05339166a441db259e0acc5c62 Mon Sep 17 00:00:00 2001 From: preland <89992615+preland@users.noreply.github.com> Date: Tue, 17 Sep 2024 11:44:20 -0400 Subject: [PATCH 05/21] Create error log file --- .../src/main/java/haveno/common/app/Log.java | 49 +++++++++++++------ 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/common/src/main/java/haveno/common/app/Log.java b/common/src/main/java/haveno/common/app/Log.java index 1922a519..58dc95e1 100644 --- a/common/src/main/java/haveno/common/app/Log.java +++ b/common/src/main/java/haveno/common/app/Log.java @@ -21,6 +21,7 @@ import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.encoder.PatternLayoutEncoder; +import ch.qos.logback.classic.filter.ThresholdFilter; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.rolling.FixedWindowRollingPolicy; import ch.qos.logback.core.rolling.RollingFileAppender; @@ -52,11 +53,12 @@ public class Log { SizeBasedTriggeringPolicy triggeringPolicy = new SizeBasedTriggeringPolicy<>(); triggeringPolicy.setMaxFileSize(FileSize.valueOf("10MB")); + triggeringPolicy.setContext(loggerContext); triggeringPolicy.start(); PatternLayoutEncoder encoder = new PatternLayoutEncoder(); encoder.setContext(loggerContext); - encoder.setPattern("%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{15}: %msg %xEx%n"); + encoder.setPattern("%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{15}: %msg%n"); encoder.start(); appender.setEncoder(encoder); @@ -64,25 +66,44 @@ public class Log { appender.setTriggeringPolicy(triggeringPolicy); appender.start(); - logbackLogger = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); - logbackLogger.addAppender(appender); - logbackLogger.setLevel(Level.INFO); - // log errors in separate file // not working as expected still.... damn logback... - /* FileAppender errorAppender = new FileAppender(); - errorAppender.setEncoder(encoder); + PatternLayoutEncoder errorEncoder = new PatternLayoutEncoder(); + errorEncoder.setContext(loggerContext); + errorEncoder.setPattern("%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger: %msg%n%ex"); + errorEncoder.start(); + + RollingFileAppender errorAppender = new RollingFileAppender<>(); + errorAppender.setEncoder(errorEncoder); errorAppender.setName("Error"); errorAppender.setContext(loggerContext); errorAppender.setFile(fileName + "_error.log"); - LevelFilter levelFilter = new LevelFilter(); - levelFilter.setLevel(Level.ERROR); - levelFilter.setOnMatch(FilterReply.ACCEPT); - levelFilter.setOnMismatch(FilterReply.DENY); - levelFilter.start(); - errorAppender.addFilter(levelFilter); + + FixedWindowRollingPolicy errorRollingPolicy = new FixedWindowRollingPolicy(); + errorRollingPolicy.setContext(loggerContext); + errorRollingPolicy.setParent(errorAppender); + errorRollingPolicy.setFileNamePattern(fileName + "_error_%i.log"); + errorRollingPolicy.setMinIndex(1); + errorRollingPolicy.setMaxIndex(20); + errorRollingPolicy.start(); + + SizeBasedTriggeringPolicy errorTriggeringPolicy = new SizeBasedTriggeringPolicy<>(); + errorTriggeringPolicy.setMaxFileSize(FileSize.valueOf("10MB")); + errorTriggeringPolicy.start(); + + ThresholdFilter thresholdFilter = new ThresholdFilter(); + thresholdFilter.setLevel("WARN"); + thresholdFilter.start(); + + errorAppender.setRollingPolicy(errorRollingPolicy); + errorAppender.setTriggeringPolicy(errorTriggeringPolicy); + errorAppender.addFilter(thresholdFilter); errorAppender.start(); - logbackLogger.addAppender(errorAppender);*/ + + logbackLogger = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); + logbackLogger.addAppender(errorAppender); + logbackLogger.addAppender(appender); + logbackLogger.setLevel(Level.INFO); } public static void setCustomLogLevel(String pattern, Level logLevel) { From 2f310b420dc2901a65eb00eea9c54b96a2d71cee Mon Sep 17 00:00:00 2001 From: woodser Date: Tue, 17 Sep 2024 12:00:35 -0400 Subject: [PATCH 06/21] use error level for error log file --- common/src/main/java/haveno/common/app/Log.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/common/src/main/java/haveno/common/app/Log.java b/common/src/main/java/haveno/common/app/Log.java index 58dc95e1..d02870c9 100644 --- a/common/src/main/java/haveno/common/app/Log.java +++ b/common/src/main/java/haveno/common/app/Log.java @@ -67,7 +67,6 @@ public class Log { appender.start(); // log errors in separate file - // not working as expected still.... damn logback... PatternLayoutEncoder errorEncoder = new PatternLayoutEncoder(); errorEncoder.setContext(loggerContext); errorEncoder.setPattern("%d{MMM-dd HH:mm:ss.SSS} [%thread] %-5level %logger: %msg%n%ex"); @@ -92,7 +91,7 @@ public class Log { errorTriggeringPolicy.start(); ThresholdFilter thresholdFilter = new ThresholdFilter(); - thresholdFilter.setLevel("WARN"); + thresholdFilter.setLevel("ERROR"); thresholdFilter.start(); errorAppender.setRollingPolicy(errorRollingPolicy); From 2a9bc87f6526bb7ec23295acb6f91b983957fb41 Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 12 Sep 2024 10:41:10 -0400 Subject: [PATCH 07/21] check multisig state on initialization --- .../protocol/tasks/ProcessInitMultisigRequest.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessInitMultisigRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessInitMultisigRequest.java index 78fafed6..62aacb4f 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessInitMultisigRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessInitMultisigRequest.java @@ -32,6 +32,7 @@ import haveno.network.p2p.NodeAddress; import haveno.network.p2p.SendDirectMessageListener; import lombok.extern.slf4j.Slf4j; import monero.wallet.MoneroWallet; +import monero.wallet.model.MoneroMultisigInfo; import monero.wallet.model.MoneroMultisigInitResult; import java.util.Arrays; @@ -118,8 +119,17 @@ public class ProcessInitMultisigRequest extends TradeTask { if (processModel.getMultisigAddress() == null && peers[0].getExchangedMultisigHex() != null && peers[1].getExchangedMultisigHex() != null) { log.info("Importing exchanged multisig hex for trade {}", trade.getId()); MoneroMultisigInitResult result = multisigWallet.exchangeMultisigKeys(Arrays.asList(peers[0].getExchangedMultisigHex(), peers[1].getExchangedMultisigHex()), xmrWalletService.getWalletPassword()); + + // check multisig state + MoneroMultisigInfo multisigInfo = multisigWallet.getMultisigInfo(); + if (!multisigInfo.isMultisig()) throw new RuntimeException("Multisig wallet is not multisig on completion"); + if (!multisigInfo.isReady()) throw new RuntimeException("Multisig wallet is not ready on completion"); + if (multisigInfo.getThreshold() != 2) throw new RuntimeException("Multisig wallet has unexpected threshold: " + multisigInfo.getThreshold()); + if (multisigInfo.getNumParticipants() != 3) throw new RuntimeException("Multisig wallet has unexpected number of participants: " + multisigInfo.getNumParticipants()); + + // set final address and save processModel.setMultisigAddress(result.getAddress()); - new Thread(() -> trade.saveWallet()).start(); // save multisig wallet off thread on completion + new Thread(() -> trade.saveWallet()).start(); // save off thread on completion trade.setStateIfValidTransitionTo(Trade.State.MULTISIG_COMPLETED); } From 0ed640be1629ad041a8d4497f48baff7c710d7a4 Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 12 Sep 2024 13:28:33 -0400 Subject: [PATCH 08/21] save multisig wallet on same thread when initialized --- .../core/trade/protocol/tasks/ProcessInitMultisigRequest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessInitMultisigRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessInitMultisigRequest.java index 62aacb4f..71b53c5d 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessInitMultisigRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessInitMultisigRequest.java @@ -129,7 +129,7 @@ public class ProcessInitMultisigRequest extends TradeTask { // set final address and save processModel.setMultisigAddress(result.getAddress()); - new Thread(() -> trade.saveWallet()).start(); // save off thread on completion + trade.saveWallet(); trade.setStateIfValidTransitionTo(Trade.State.MULTISIG_COMPLETED); } From c04fc7b2dbfda85e96e527967c1a0600edbaa691 Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 12 Sep 2024 13:35:04 -0400 Subject: [PATCH 09/21] save multisig wallet on same thread in trade wallet operations --- core/src/main/java/haveno/core/trade/Trade.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index ca3df853..b9910627 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -1051,7 +1051,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { synchronized (HavenoUtils.getWalletFunctionLock()) { MoneroTxWallet tx = wallet.createTx(txConfig); exportMultisigHex(); - requestSaveWallet(); + saveWallet(); return tx; } } @@ -1152,7 +1152,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { throw e; } } - requestSaveWallet(); + saveWallet(); } log.info("Done importing multisig hexes for {} {} in {} ms, count={}", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime, multisigHexes.size()); } @@ -1365,6 +1365,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // verify fee is within tolerance by recreating payout tx // TODO (monero-project): creating tx will require exchanging updated multisig hex if message needs reprocessed. provide weight with describe_transfer so fee can be estimated? log.info("Creating fee estimate tx for {} {}", getClass().getSimpleName(), getId()); + saveWallet(); // save wallet before creating fee estimate tx MoneroTxWallet feeEstimateTx = createPayoutTx(); BigInteger feeEstimate = feeEstimateTx.getFee(); double feeDiff = payoutTx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal? @@ -1373,6 +1374,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { } // save trade state + saveWallet(); requestPersistence(); // submit payout tx From 8d55abe3b92d29e4efaa823cef0ef9ff8155fb6a Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 23 Sep 2024 09:35:11 -0400 Subject: [PATCH 10/21] add deprecated tails support as backup --- scripts/install_tails/deprecated/README.md | 11 +++ .../deprecated/haveno-install.sh | 77 +++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 scripts/install_tails/deprecated/README.md create mode 100644 scripts/install_tails/deprecated/haveno-install.sh diff --git a/scripts/install_tails/deprecated/README.md b/scripts/install_tails/deprecated/README.md new file mode 100644 index 00000000..0345bd1e --- /dev/null +++ b/scripts/install_tails/deprecated/README.md @@ -0,0 +1,11 @@ +# Steps to use (This has serious security concerns to tails threat model only run when you need to access haveno) + +## 1. Enable persistent storage and admin password before starting tails + +## 2. Get your haveno deb file in persistent storage (amd64 version for tails) + +## 3. Edit the path to the haveno deb file if necessary then run ```sudo ./haveno-install.sh``` +## 4. As amnesia run ```source ~/.bashrc``` +## 5. Start haveno using ```haveno-tails``` + +## You will need to run this script after each reset, but your data will be saved persistently in /home/amnesia/Persistence/Haveno \ No newline at end of file diff --git a/scripts/install_tails/deprecated/haveno-install.sh b/scripts/install_tails/deprecated/haveno-install.sh new file mode 100644 index 00000000..247354ff --- /dev/null +++ b/scripts/install_tails/deprecated/haveno-install.sh @@ -0,0 +1,77 @@ +#!/bin/bash + +############################################################################# +# Written by BrandyJson, with heavy inspiration from bisq.wiki tails script # +############################################################################# +echo "Installing dpkg from persistent, (1.07-1, if this is out of date change the deb path in the script or manually install after running" +dpkg -i "/home/amnesia/Persistent/haveno_1.0.7-1_amd64.deb" +echo -e "Allowing amnesia to read tor control port cookie, only run this script when you actually want to use haveno\n\n!!! not secure !!!\n" +chmod o+r /var/run/tor/control.authcookie +echo "Updating apparmor-profile" +echo "--- +- apparmor-profiles: + - '/opt/haveno/bin/Haveno' + users: + - 'amnesia' + commands: + AUTHCHALLENGE: + - 'SAFECOOKIE .*' + SETEVENTS: + - 'CIRC ORCONN INFO NOTICE WARN ERR HS_DESC HS_DESC_CONTENT' + GETINFO: + - pattern: 'status/bootstrap-phase' + response: + - pattern: '250-status/bootstrap-phase=*' + replacement: '250-status/bootstrap-phase=NOTICE BOOTSTRAP PROGRESS=100 TAG=done SUMMARY="Done"' + - 'net/listeners/socks' + ADD_ONION: + - pattern: 'NEW:(\S+) Port=9999,(\S+)' + replacement: 'NEW:{} Port=9999,{client-address}:{}' + - pattern: '(\S+):(\S+) Port=9999,(\S+)' + replacement: '{}:{} Port=9999,{client-address}:{}' + DEL_ONION: + - '.+' + HSFETCH: + - '.+' + events: + CIRC: + suppress: true + ORCONN: + suppress: true + INFO: + suppress: true + NOTICE: + suppress: true + WARN: + suppress: true + ERR: + suppress: true + HS_DESC: + response: + - pattern: '650 HS_DESC CREATED (\S+) (\S+) (\S+) \S+ (.+)' + replacement: '650 HS_DESC CREATED {} {} {} redacted {}' + - pattern: '650 HS_DESC UPLOAD (\S+) (\S+) .*' + replacement: '650 HS_DESC UPLOAD {} {} redacted redacted' + - pattern: '650 HS_DESC UPLOADED (\S+) (\S+) .+' + replacement: '650 HS_DESC UPLOADED {} {} redacted' + - pattern: '650 HS_DESC REQUESTED (\S+) NO_AUTH' + replacement: '650 HS_DESC REQUESTED {} NO_AUTH' + - pattern: '650 HS_DESC REQUESTED (\S+) NO_AUTH \S+ \S+' + replacement: '650 HS_DESC REQUESTED {} NO_AUTH redacted redacted' + - pattern: '650 HS_DESC RECEIVED (\S+) NO_AUTH \S+ \S+' + replacement: '650 HS_DESC RECEIVED {} NO_AUTH redacted redacted' + - pattern: '.*' + replacement: '' + HS_DESC_CONTENT: + suppress: true" > /etc/onion-grater.d/haveno.yml +echo "Adding rule to iptables to allow for monero-wallet-rpc to work" +iptables -I OUTPUT 2 -p tcp -d 127.0.0.1 -m tcp --dport 18081 -m owner --uid-owner 1855 -j ACCEPT +echo "Updating torsocks to allow for inbound connection" +sed -i 's/#AllowInbound/AllowInbound/g' /etc/tor/torsocks.conf + +echo "Restarting onion-grater service" + +systemctl restart onion-grater.service + +echo "alias haveno-tails='torsocks /opt/haveno/bin/Haveno --torControlPort 951 --torControlCookieFile=/var/run/tor/control.authcookie --torControlUseSafeCookieAuth --useTorForXmr=ON --userDataDir=/home/amnesia/Persistent/'" >> /home/amnesia/.bashrc +echo -e "Everything is set up just run\n\nsource ~/.bashrc\n\nThen you can start haveno using haveno-tails" From 50f3bd510a67efc9316f993309a51e756406dae3 Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 20 Sep 2024 11:03:33 -0400 Subject: [PATCH 11/21] log stack traces at warn or error level --- .../main/java/haveno/apitest/ApiTestMain.java | 2 +- build.gradle | 1 + .../src/main/java/haveno/common/app/Log.java | 2 +- .../java/haveno/common/file/FileUtil.java | 8 ++--- .../java/haveno/common/setup/CommonSetup.java | 9 ++---- .../haveno/common/taskrunner/TaskRunner.java | 6 ++-- .../java/haveno/common/util/Utilities.java | 3 +- .../haveno/core/api/CoreDisputesService.java | 5 ++- .../haveno/core/api/CoreTradesService.java | 4 ++- .../haveno/core/api/XmrConnectionService.java | 12 ++++--- .../haveno/core/app/HavenoExecutable.java | 10 +++--- .../java/haveno/core/app/HavenoSetup.java | 3 +- .../main/java/haveno/core/app/TorSetup.java | 6 ++-- .../app/misc/ExecutableForAppWithP2p.java | 5 ++- .../haveno/core/offer/OpenOfferManager.java | 8 ++--- .../haveno/core/provider/fee/FeeProvider.java | 3 +- .../core/provider/price/PriceProvider.java | 3 +- .../core/support/dispute/DisputeManager.java | 12 ++++--- .../dispute/DisputeSummaryVerification.java | 4 +-- .../arbitration/ArbitrationManager.java | 7 ++-- .../main/java/haveno/core/trade/Trade.java | 9 ++---- .../java/haveno/core/trade/TradeManager.java | 12 +++---- .../ArbitratorProcessDepositRequest.java | 7 ++-- .../tasks/ArbitratorProcessReserveTx.java | 4 ++- ...eResendDisputeClosedMessageWithPayout.java | 2 +- .../ProcessDepositsConfirmedMessage.java | 2 +- .../core/trade/protocol/tasks/TradeTask.java | 2 +- .../haveno/core/xmr/wallet/XmrWalletBase.java | 4 ++- .../core/xmr/wallet/XmrWalletService.java | 32 +++++++++---------- .../windows/DisputeSummaryWindow.java | 3 +- .../portfolio/pendingtrades/TradeSubView.java | 3 +- .../haveno/desktop/main/shared/ChatView.java | 13 +++----- .../haveno/network/Socks5ProxyProvider.java | 4 +-- .../java/haveno/network/p2p/P2PService.java | 9 +++--- .../p2p/mailbox/MailboxMessageList.java | 3 +- .../p2p/mailbox/MailboxMessageService.java | 6 ++-- .../network/p2p/network/Connection.java | 11 +++---- .../p2p/network/LocalhostNetworkNode.java | 3 +- .../haveno/network/p2p/network/Server.java | 5 ++- .../network/p2p/storage/P2PDataStorage.java | 3 +- .../p2p/storage/persistence/StoreService.java | 3 +- .../java/haveno/seednode/SeedNodeMain.java | 2 +- 42 files changed, 117 insertions(+), 138 deletions(-) diff --git a/apitest/src/main/java/haveno/apitest/ApiTestMain.java b/apitest/src/main/java/haveno/apitest/ApiTestMain.java index 4da4b920..ad383ff1 100644 --- a/apitest/src/main/java/haveno/apitest/ApiTestMain.java +++ b/apitest/src/main/java/haveno/apitest/ApiTestMain.java @@ -78,7 +78,7 @@ public class ApiTestMain { } catch (Throwable ex) { err.println("Fault: An unexpected error occurred. " + - "Please file a report at https://haveno.exchange/issues"); + "Please file a report at https://github.com/haveno-dex/haveno/issues"); ex.printStackTrace(err); exit(EXIT_FAILURE); } diff --git a/build.gradle b/build.gradle index 22657efd..bcd7d082 100644 --- a/build.gradle +++ b/build.gradle @@ -334,6 +334,7 @@ configure(project(':p2p')) { implementation "com.google.protobuf:protobuf-java:$protobufVersion" implementation "org.fxmisc.easybind:easybind:$easybindVersion" implementation "org.slf4j:slf4j-api:$slf4jVersion" + implementation "org.apache.commons:commons-lang3:$langVersion" implementation("com.github.haveno-dex.netlayer:tor.external:$netlayerVersion") { exclude(module: 'slf4j-api') } diff --git a/common/src/main/java/haveno/common/app/Log.java b/common/src/main/java/haveno/common/app/Log.java index d02870c9..67ca2ab9 100644 --- a/common/src/main/java/haveno/common/app/Log.java +++ b/common/src/main/java/haveno/common/app/Log.java @@ -91,7 +91,7 @@ public class Log { errorTriggeringPolicy.start(); ThresholdFilter thresholdFilter = new ThresholdFilter(); - thresholdFilter.setLevel("ERROR"); + thresholdFilter.setLevel("WARN"); thresholdFilter.start(); errorAppender.setRollingPolicy(errorRollingPolicy); diff --git a/common/src/main/java/haveno/common/file/FileUtil.java b/common/src/main/java/haveno/common/file/FileUtil.java index 449faea6..27058f30 100644 --- a/common/src/main/java/haveno/common/file/FileUtil.java +++ b/common/src/main/java/haveno/common/file/FileUtil.java @@ -68,8 +68,7 @@ public class FileUtil { pruneBackup(backupFileDir, numMaxBackupFiles); } catch (IOException e) { - log.error("Backup key failed: " + e.getMessage()); - e.printStackTrace(); + log.error("Backup key failed: {}\n", e.getMessage(), e); } } } @@ -97,7 +96,7 @@ public class FileUtil { try { FileUtils.deleteDirectory(backupFileDir); } catch (IOException e) { - e.printStackTrace(); + log.error("Delete backup key failed: {}\n", e.getMessage(), e); } } @@ -173,8 +172,7 @@ public class FileUtil { } } } catch (Throwable t) { - log.error(t.toString()); - t.printStackTrace(); + log.error("Could not delete file, error={}\n", t.getMessage(), t); throw new IOException(t); } } diff --git a/common/src/main/java/haveno/common/setup/CommonSetup.java b/common/src/main/java/haveno/common/setup/CommonSetup.java index f606d3b5..0c929b3a 100644 --- a/common/src/main/java/haveno/common/setup/CommonSetup.java +++ b/common/src/main/java/haveno/common/setup/CommonSetup.java @@ -69,11 +69,7 @@ public class CommonSetup { "The system tray is not supported on the current platform.".equals(throwable.getMessage())) { log.warn(throwable.getMessage()); } else { - log.error("Uncaught Exception from thread " + Thread.currentThread().getName()); - log.error("throwableMessage= " + throwable.getMessage()); - log.error("throwableClass= " + throwable.getClass()); - log.error("Stack trace:\n" + ExceptionUtils.getStackTrace(throwable)); - throwable.printStackTrace(); + log.error("Uncaught Exception from thread {}, error={}\n", Thread.currentThread().getName(), throwable.getMessage(), throwable); UserThread.execute(() -> uncaughtExceptionHandler.handleUncaughtException(throwable, false)); } }; @@ -113,8 +109,7 @@ public class CommonSetup { if (!pathOfCodeSource.endsWith("classes")) log.info("Path to Haveno jar file: " + pathOfCodeSource); } catch (URISyntaxException e) { - log.error(e.toString()); - e.printStackTrace(); + log.error(ExceptionUtils.getStackTrace(e)); } } } diff --git a/common/src/main/java/haveno/common/taskrunner/TaskRunner.java b/common/src/main/java/haveno/common/taskrunner/TaskRunner.java index e49b4ccd..087ffce7 100644 --- a/common/src/main/java/haveno/common/taskrunner/TaskRunner.java +++ b/common/src/main/java/haveno/common/taskrunner/TaskRunner.java @@ -25,6 +25,8 @@ import java.util.Arrays; import java.util.Queue; import java.util.concurrent.LinkedBlockingQueue; +import org.apache.commons.lang3.exception.ExceptionUtils; + @Slf4j public class TaskRunner { private final Queue>> tasks = new LinkedBlockingQueue<>(); @@ -67,8 +69,8 @@ public class TaskRunner { log.info("Run task: " + currentTask.getSimpleName()); currentTask.getDeclaredConstructor(TaskRunner.class, sharedModelClass).newInstance(this, sharedModel).run(); } catch (Throwable throwable) { - throwable.printStackTrace(); - handleErrorMessage("Error at taskRunner: " + throwable.getMessage()); + log.error(ExceptionUtils.getStackTrace(throwable)); + handleErrorMessage("Error at taskRunner, error=" + throwable.getMessage()); } } else { resultHandler.handleResult(); diff --git a/common/src/main/java/haveno/common/util/Utilities.java b/common/src/main/java/haveno/common/util/Utilities.java index b4afe417..240ea49e 100644 --- a/common/src/main/java/haveno/common/util/Utilities.java +++ b/common/src/main/java/haveno/common/util/Utilities.java @@ -331,8 +331,7 @@ public class Utilities { clipboard.setContent(clipboardContent); } } catch (Throwable e) { - log.error("copyToClipboard failed " + e.getMessage()); - e.printStackTrace(); + log.error("copyToClipboard failed: {}\n", e.getMessage(), e); } } diff --git a/core/src/main/java/haveno/core/api/CoreDisputesService.java b/core/src/main/java/haveno/core/api/CoreDisputesService.java index 7edbed9b..f193287e 100644 --- a/core/src/main/java/haveno/core/api/CoreDisputesService.java +++ b/core/src/main/java/haveno/core/api/CoreDisputesService.java @@ -52,6 +52,9 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Optional; + +import org.apache.commons.lang3.exception.ExceptionUtils; + import lombok.extern.slf4j.Slf4j; @@ -204,7 +207,7 @@ public class CoreDisputesService { throw new IllegalStateException(errMessage, err); }); } catch (Exception e) { - e.printStackTrace(); + log.error(ExceptionUtils.getStackTrace(e)); throw new IllegalStateException(e.getMessage() == null ? ("Error resolving dispute for trade " + trade.getId()) : e.getMessage()); } } diff --git a/core/src/main/java/haveno/core/api/CoreTradesService.java b/core/src/main/java/haveno/core/api/CoreTradesService.java index 16fa22a8..431ab9a6 100644 --- a/core/src/main/java/haveno/core/api/CoreTradesService.java +++ b/core/src/main/java/haveno/core/api/CoreTradesService.java @@ -66,6 +66,8 @@ import java.util.List; import java.util.Optional; import java.util.function.Consumer; import lombok.extern.slf4j.Slf4j; + +import org.apache.commons.lang3.exception.ExceptionUtils; import org.bitcoinj.core.Coin; @Singleton @@ -161,7 +163,7 @@ class CoreTradesService { errorMessageHandler ); } catch (Exception e) { - e.printStackTrace(); + log.error(ExceptionUtils.getStackTrace(e)); errorMessageHandler.handleErrorMessage(e.getMessage()); } } diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index 4465fd2a..2541fb9f 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -41,6 +41,9 @@ import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; + +import org.apache.commons.lang3.exception.ExceptionUtils; + import javafx.beans.property.IntegerProperty; import javafx.beans.property.LongProperty; import javafx.beans.property.ObjectProperty; @@ -464,7 +467,7 @@ public final class XmrConnectionService { log.info(getClass() + ".onAccountOpened() called"); initialize(); } catch (Exception e) { - e.printStackTrace(); + log.error("Error initializing connection service after account opened, error={}\n", e.getMessage(), e); throw new RuntimeException(e); } } @@ -622,8 +625,7 @@ public final class XmrConnectionService { log.info("Starting local node"); xmrLocalNode.startMoneroNode(); } catch (Exception e) { - log.warn("Unable to start local monero node: " + e.getMessage()); - e.printStackTrace(); + log.error("Unable to start local monero node, error={}\n", e.getMessage(), e); } } } @@ -721,8 +723,8 @@ public final class XmrConnectionService { // log error message periodically if (lastLogPollErrorTimestamp == null || System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS) { - log.warn("Failed to fetch daemon info, trying to switch to best connection: " + e.getMessage()); - if (DevEnv.isDevMode()) e.printStackTrace(); + log.warn("Failed to fetch daemon info, trying to switch to best connection, error={}", e.getMessage()); + if (DevEnv.isDevMode()) log.error(ExceptionUtils.getStackTrace(e)); lastLogPollErrorTimestamp = System.currentTimeMillis(); } diff --git a/core/src/main/java/haveno/core/app/HavenoExecutable.java b/core/src/main/java/haveno/core/app/HavenoExecutable.java index e213bdd6..aa25b12d 100644 --- a/core/src/main/java/haveno/core/app/HavenoExecutable.java +++ b/core/src/main/java/haveno/core/app/HavenoExecutable.java @@ -124,7 +124,7 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven System.exit(EXIT_FAILURE); } catch (Throwable ex) { System.err.println("fault: An unexpected error occurred. " + - "Please file a report at https://haveno.exchange/issues"); + "Please file a report at https://github.com/haveno-dex/haveno/issues"); ex.printStackTrace(System.err); System.exit(EXIT_FAILURE); } @@ -201,8 +201,7 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven startApplication(); } } catch (InterruptedException | ExecutionException e) { - log.error("An error occurred: {}", e.getMessage()); - e.printStackTrace(); + log.error("An error occurred: {}\n", e.getMessage(), e); } }); } @@ -362,7 +361,7 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven try { ThreadUtils.awaitTasks(tasks, tasks.size(), 90000l); // run in parallel with timeout } catch (Exception e) { - e.printStackTrace(); + log.error("Failed to notify all services to prepare for shutdown: {}\n", e.getMessage(), e); } injector.getInstance(TradeManager.class).shutDown(); @@ -397,8 +396,7 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven }); }); } catch (Throwable t) { - log.error("App shutdown failed with exception {}", t.toString()); - t.printStackTrace(); + log.error("App shutdown failed with exception: {}\n", t.getMessage(), t); completeShutdown(resultHandler, EXIT_FAILURE, systemExit); } } diff --git a/core/src/main/java/haveno/core/app/HavenoSetup.java b/core/src/main/java/haveno/core/app/HavenoSetup.java index 0d1ee2b9..2c2e95fd 100644 --- a/core/src/main/java/haveno/core/app/HavenoSetup.java +++ b/core/src/main/java/haveno/core/app/HavenoSetup.java @@ -376,8 +376,7 @@ public class HavenoSetup { moneroWalletRpcFile.setExecutable(true); } } catch (Exception e) { - e.printStackTrace(); - log.warn("Failed to install Monero binaries: " + e.toString()); + log.warn("Failed to install Monero binaries: {}\n", e.getMessage(), e); } } diff --git a/core/src/main/java/haveno/core/app/TorSetup.java b/core/src/main/java/haveno/core/app/TorSetup.java index d878464a..c28e509e 100644 --- a/core/src/main/java/haveno/core/app/TorSetup.java +++ b/core/src/main/java/haveno/core/app/TorSetup.java @@ -28,6 +28,9 @@ import java.io.File; import java.io.IOException; import java.nio.file.Paths; import javax.annotation.Nullable; + +import org.apache.commons.lang3.exception.ExceptionUtils; + import lombok.extern.slf4j.Slf4j; @Slf4j @@ -48,8 +51,7 @@ public class TorSetup { if (resultHandler != null) resultHandler.run(); } catch (IOException e) { - e.printStackTrace(); - log.error(e.toString()); + log.error(ExceptionUtils.getStackTrace(e)); if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage(e.toString()); } diff --git a/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java b/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java index 8086d563..725ccd87 100644 --- a/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java +++ b/core/src/main/java/haveno/core/app/misc/ExecutableForAppWithP2p.java @@ -123,7 +123,7 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable { try { ThreadUtils.awaitTasks(tasks, tasks.size(), 120000l); // run in parallel with timeout } catch (Exception e) { - e.printStackTrace(); + log.error("Error awaiting tasks to complete: {}\n", e.getMessage(), e); } JsonFileManager.shutDownAllInstances(); @@ -177,8 +177,7 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable { }, 1); } } catch (Throwable t) { - log.debug("App shutdown failed with exception"); - t.printStackTrace(); + log.info("App shutdown failed with exception: {}\n", t.getMessage(), t); PersistenceManager.flushAllDataToDiskAtShutdown(() -> { resultHandler.handleResult(); log.info("Graceful shutdown resulted in an error. Exiting now."); diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index aa561f12..4824579d 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -977,7 +977,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // handle result resultHandler.handleResult(null); } catch (Exception e) { - if (!openOffer.isCanceled()) e.printStackTrace(); + if (!openOffer.isCanceled()) log.error("Error processing pending offer: {}\n", e.getMessage(), e); errorMessageHandler.handleErrorMessage(e.getMessage()); } }).start(); @@ -1365,9 +1365,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe }); result = true; } catch (Exception e) { - e.printStackTrace(); errorMessage = "Exception at handleSignOfferRequest " + e.getMessage(); - log.error(errorMessage); + log.error(errorMessage + "\n", e); } finally { sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), result, errorMessage); } @@ -1519,8 +1518,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe result = true; } catch (Throwable t) { errorMessage = "Exception at handleRequestIsOfferAvailableMessage " + t.getMessage(); - log.error(errorMessage); - t.printStackTrace(); + log.error(errorMessage + "\n", t); } finally { sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), result, errorMessage); } diff --git a/core/src/main/java/haveno/core/provider/fee/FeeProvider.java b/core/src/main/java/haveno/core/provider/fee/FeeProvider.java index 30d140f8..18838cee 100644 --- a/core/src/main/java/haveno/core/provider/fee/FeeProvider.java +++ b/core/src/main/java/haveno/core/provider/fee/FeeProvider.java @@ -59,8 +59,7 @@ public class FeeProvider extends HttpClientProvider { map.put(Config.BTC_TX_FEE, btcTxFee); map.put(Config.BTC_MIN_TX_FEE, btcMinTxFee); } catch (Throwable t) { - log.error(t.toString()); - t.printStackTrace(); + log.error("Error getting fees: {}\n", t.getMessage(), t); } return new Tuple2<>(tsMap, map); } diff --git a/core/src/main/java/haveno/core/provider/price/PriceProvider.java b/core/src/main/java/haveno/core/provider/price/PriceProvider.java index 871151a9..17bef33a 100644 --- a/core/src/main/java/haveno/core/provider/price/PriceProvider.java +++ b/core/src/main/java/haveno/core/provider/price/PriceProvider.java @@ -68,8 +68,7 @@ public class PriceProvider extends HttpClientProvider { long timestampSec = MathUtils.doubleToLong((Double) treeMap.get("timestampSec")); marketPriceMap.put(currencyCode, new MarketPrice(currencyCode, price, timestampSec, true)); } catch (Throwable t) { - log.error(t.toString()); - t.printStackTrace(); + log.error("Error getting all prices: {}\n", t.getMessage(), t); } }); diff --git a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java index 192516b5..bd124fda 100644 --- a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java @@ -83,6 +83,9 @@ import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroTxWallet; import javax.annotation.Nullable; + +import org.apache.commons.lang3.exception.ExceptionUtils; + import java.math.BigInteger; import java.security.KeyPair; import java.time.Instant; @@ -523,7 +526,7 @@ public abstract class DisputeManager> extends Sup DisputeValidation.validateSenderNodeAddress(dispute, message.getSenderNodeAddress(), config); //DisputeValidation.testIfDisputeTriesReplay(dispute, disputeList.getList()); } catch (DisputeValidation.ValidationException e) { - e.printStackTrace(); + log.error(ExceptionUtils.getStackTrace(e)); validationExceptions.add(e); throw e; } @@ -532,9 +535,9 @@ public abstract class DisputeManager> extends Sup try { DisputeValidation.validatePaymentAccountPayload(dispute); // TODO: add field to dispute details: valid, invalid, missing } catch (Exception e) { - e.printStackTrace(); - log.warn(e.getMessage()); + log.error(ExceptionUtils.getStackTrace(e)); trade.prependErrorMessage(e.getMessage()); + throw e; } // get sender @@ -606,9 +609,8 @@ public abstract class DisputeManager> extends Sup } } } catch (Exception e) { - e.printStackTrace(); + log.error(ExceptionUtils.getStackTrace(e)); errorMessage = e.getMessage(); - log.warn(errorMessage); if (trade != null) trade.setErrorMessage(errorMessage); } diff --git a/core/src/main/java/haveno/core/support/dispute/DisputeSummaryVerification.java b/core/src/main/java/haveno/core/support/dispute/DisputeSummaryVerification.java index a2f2f2f2..5fbd64db 100644 --- a/core/src/main/java/haveno/core/support/dispute/DisputeSummaryVerification.java +++ b/core/src/main/java/haveno/core/support/dispute/DisputeSummaryVerification.java @@ -71,7 +71,7 @@ public class DisputeSummaryVerification { disputeAgent = arbitratorManager.getDisputeAgentByNodeAddress(nodeAddress).orElse(null); checkNotNull(disputeAgent, "Dispute agent is null"); } catch (Throwable e) { - e.printStackTrace(); + log.error("Error verifying signature: {}\n", e.getMessage(), e); throw new IllegalArgumentException(Res.get("support.sigCheck.popup.invalidFormat")); } @@ -93,7 +93,7 @@ public class DisputeSummaryVerification { throw new IllegalArgumentException(Res.get("support.sigCheck.popup.failed")); } } catch (Throwable e) { - e.printStackTrace(); + log.error("Error verifying signature with agent pub key ring: {}\n", e.getMessage(), e); throw new IllegalArgumentException(Res.get("support.sigCheck.popup.invalidFormat")); } } diff --git a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java index 8082aad4..719ac462 100644 --- a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java +++ b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java @@ -94,6 +94,8 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import org.apache.commons.lang3.exception.ExceptionUtils; + import static com.google.common.base.Preconditions.checkNotNull; @Slf4j @@ -355,7 +357,7 @@ public final class ArbitrationManager extends DisputeManager { completeAux(); }, (errMessage, err) -> { - err.printStackTrace(); + log.error("Failed to close dispute ticket for trade {}: {}\n", trade.getId(), errMessage, err); failed(err); }); ticketClosed = true; 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 a43c667d..c11df74f 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 @@ -70,7 +70,7 @@ public class ProcessDepositsConfirmedMessage extends TradeTask { try { trade.importMultisigHex(); } catch (Exception e) { - e.printStackTrace(); + log.warn("Error importing multisig hex on deposits confirmed for trade " + trade.getId() + ": " + e.getMessage() + "\n", e); } }); } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/TradeTask.java b/core/src/main/java/haveno/core/trade/protocol/tasks/TradeTask.java index 231e5482..293b74f9 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/TradeTask.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/TradeTask.java @@ -61,7 +61,7 @@ public abstract class TradeTask extends Task { @Override protected void failed(Throwable t) { - t.printStackTrace(); + log.error("Trade task failed, error={}\n", t.getMessage(), t); appendExceptionToErrorMessage(t); trade.setErrorMessage(errorMessage); processModel.getTradeManager().requestPersistence(); diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java index 0cbc41b2..877eb387 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java @@ -6,6 +6,8 @@ import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.exception.ExceptionUtils; + import haveno.common.Timer; import haveno.common.UserThread; import haveno.core.api.XmrConnectionService; @@ -106,7 +108,7 @@ public class XmrWalletBase { height = wallet.getHeight(); // can get read timeout while syncing } catch (Exception e) { log.warn("Error getting wallet height while syncing with progress: " + e.getMessage()); - if (wallet != null && !isShutDownStarted) e.printStackTrace(); + if (wallet != null && !isShutDownStarted) log.warn(ExceptionUtils.getStackTrace(e)); // stop polling and release latch syncProgressError = e; 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 55b09682..1a9ead35 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -818,7 +818,7 @@ public class XmrWalletService extends XmrWalletBase { MoneroFeeEstimate feeEstimates = getDaemon().getFeeEstimate(); BigInteger baseFeeEstimate = feeEstimates.getFees().get(2); // get elevated fee per kB BigInteger qmask = feeEstimates.getQuantizationMask(); - log.info("Monero base fee estimate={}, qmask={}: " + baseFeeEstimate, qmask); + log.info("Monero base fee estimate={}, qmask={}", baseFeeEstimate, qmask); // get tx base fee BigInteger baseFee = baseFeeEstimate.multiply(BigInteger.valueOf(txWeight)); @@ -922,8 +922,7 @@ public class XmrWalletService extends XmrWalletBase { try { ThreadUtils.awaitTask(shutDownTask, SHUTDOWN_TIMEOUT_MS); } catch (Exception e) { - log.warn("Error shutting down {}: {}", getClass().getSimpleName(), e.getMessage()); - e.printStackTrace(); + log.warn("Error shutting down {}: {}\n", getClass().getSimpleName(), e.getMessage(), e); // force close wallet forceCloseMainWallet(); @@ -945,8 +944,7 @@ public class XmrWalletService extends XmrWalletBase { List unusedAddressEntries = getUnusedAddressEntries(); if (!unusedAddressEntries.isEmpty()) return xmrAddressEntryList.swapAvailableToAddressEntryWithOfferId(unusedAddressEntries.get(0), context, offerId); } catch (Exception e) { - log.warn("Error getting new address entry based on incoming transactions"); - e.printStackTrace(); + log.warn("Error getting new address entry based on incoming transactions: {}\n", e.getMessage(), e); } // create new entry @@ -1172,8 +1170,7 @@ public class XmrWalletService extends XmrWalletBase { try { balanceListener.onBalanceChanged(balance); } catch (Exception e) { - log.warn("Failed to notify balance listener of change"); - e.printStackTrace(); + log.warn("Failed to notify balance listener of change: {}\n", e.getMessage(), e); } }); } @@ -1309,8 +1306,7 @@ public class XmrWalletService extends XmrWalletBase { try { doMaybeInitMainWallet(sync, MAX_SYNC_ATTEMPTS); } catch (Exception e) { - log.warn("Error initializing main wallet: " + e.getMessage()); - e.printStackTrace(); + log.warn("Error initializing main wallet: {}\n", e.getMessage(), e); HavenoUtils.setTopError(e.getMessage()); throw e; } @@ -1459,9 +1455,10 @@ public class XmrWalletService extends XmrWalletBase { log.info("Done creating full wallet " + config.getPath() + " in " + (System.currentTimeMillis() - time) + " ms"); return walletFull; } catch (Exception e) { - e.printStackTrace(); + String errorMsg = "Could not create wallet '" + config.getPath() + "': " + e.getMessage(); + log.warn(errorMsg + "\n", e); if (walletFull != null) forceCloseWallet(walletFull, config.getPath()); - throw new IllegalStateException("Could not create wallet '" + config.getPath() + "'"); + throw new IllegalStateException(errorMsg); } } @@ -1525,9 +1522,10 @@ public class XmrWalletService extends XmrWalletBase { log.info("Done opening full wallet " + config.getPath()); return walletFull; } catch (Exception e) { - e.printStackTrace(); + String errorMsg = "Could not open full wallet '" + config.getPath() + "': " + e.getMessage(); + log.warn(errorMsg + "\n", e); if (walletFull != null) forceCloseWallet(walletFull, config.getPath()); - throw new IllegalStateException("Could not open full wallet '" + config.getPath() + "'"); + throw new IllegalStateException(errorMsg); } } @@ -1557,7 +1555,7 @@ public class XmrWalletService extends XmrWalletBase { log.info("Done creating RPC wallet " + config.getPath() + " in " + (System.currentTimeMillis() - time) + " ms"); return walletRpc; } catch (Exception e) { - e.printStackTrace(); + log.warn("Could not create wallet '" + config.getPath() + "': " + e.getMessage() + "\n", e); if (walletRpc != null) forceCloseWallet(walletRpc, config.getPath()); throw new IllegalStateException("Could not create wallet '" + config.getPath() + "'. Please close Haveno, stop all monero-wallet-rpc processes in your task manager, and restart Haveno."); } @@ -1629,7 +1627,7 @@ public class XmrWalletService extends XmrWalletBase { log.info("Done opening RPC wallet " + config.getPath()); return walletRpc; } catch (Exception e) { - e.printStackTrace(); + log.warn("Could not open wallet '" + config.getPath() + "': " + e.getMessage() + "\n", e); if (walletRpc != null) forceCloseWallet(walletRpc, config.getPath()); throw new IllegalStateException("Could not open wallet '" + config.getPath() + "'. Please close Haveno, stop all monero-wallet-rpc processes in your task manager, and restart Haveno.\n\nError message: " + e.getMessage()); } @@ -1733,7 +1731,7 @@ public class XmrWalletService extends XmrWalletBase { wallet.changePassword(oldPassword, newPassword); saveMainWallet(); } catch (Exception e) { - e.printStackTrace(); + log.warn("Error changing main wallet password: " + e.getMessage() + "\n", e); throw e; } }); @@ -1916,7 +1914,7 @@ public class XmrWalletService extends XmrWalletBase { cacheWalletInfo(); requestSaveMainWallet(); } catch (Exception e) { - e.printStackTrace(); + log.warn("Error caching wallet info: " + e.getMessage() + "\n", e); } } } diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java index 7a906ec7..997a7242 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/DisputeSummaryWindow.java @@ -678,8 +678,7 @@ public class DisputeSummaryWindow extends Overlay { closeTicketButton.disableProperty().unbind(); hide(); }, (errMessage, err) -> { - log.error("Error closing dispute ticket: " + errMessage); - err.printStackTrace(); + log.error("Error closing dispute ticket: " + errMessage + "\n", err); new Popup().error(err.toString()).show(); }); } diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/TradeSubView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/TradeSubView.java index eeee3877..cef43e79 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/TradeSubView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/TradeSubView.java @@ -153,8 +153,7 @@ public abstract class TradeSubView extends HBox { tradeStepView.setChatCallback(chatCallback); tradeStepView.activate(); } catch (Exception e) { - log.error("Creating viewClass {} caused an error {}", viewClass, e.getMessage()); - e.printStackTrace(); + log.error("Creating viewClass {} caused an error {}\n", viewClass, e.getMessage(), e); } } diff --git a/desktop/src/main/java/haveno/desktop/main/shared/ChatView.java b/desktop/src/main/java/haveno/desktop/main/shared/ChatView.java index 396d6026..236f8471 100644 --- a/desktop/src/main/java/haveno/desktop/main/shared/ChatView.java +++ b/desktop/src/main/java/haveno/desktop/main/shared/ChatView.java @@ -65,6 +65,7 @@ import javafx.scene.text.TextAlignment; import javafx.geometry.Insets; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; @@ -565,12 +566,10 @@ public class ChatView extends AnchorPane { inputTextArea.setText(inputTextArea.getText() + "\n[" + Res.get("support.attachment") + " " + result.getName() + "]"); } } catch (java.io.IOException e) { - e.printStackTrace(); - log.error(e.getMessage()); + log.error(ExceptionUtils.getStackTrace(e)); } } catch (MalformedURLException e2) { - e2.printStackTrace(); - log.error(e2.getMessage()); + log.error(ExceptionUtils.getStackTrace(e2)); } } } else { @@ -593,8 +592,7 @@ public class ChatView extends AnchorPane { inputTextArea.setText(inputTextArea.getText() + "\n[" + Res.get("support.attachment") + " " + name + "]"); } } catch (Exception e) { - log.error(e.toString()); - e.printStackTrace(); + log.error(ExceptionUtils.getStackTrace(e)); } } @@ -629,8 +627,7 @@ public class ChatView extends AnchorPane { try (FileOutputStream fileOutputStream = new FileOutputStream(file.getAbsolutePath())) { fileOutputStream.write(attachment.getBytes()); } catch (IOException e) { - e.printStackTrace(); - System.out.println(e.getMessage()); + log.error("Error opening attachment: {}\n", e.getMessage(), e); } } } diff --git a/p2p/src/main/java/haveno/network/Socks5ProxyProvider.java b/p2p/src/main/java/haveno/network/Socks5ProxyProvider.java index 8bb3e1d4..f9c498f0 100644 --- a/p2p/src/main/java/haveno/network/Socks5ProxyProvider.java +++ b/p2p/src/main/java/haveno/network/Socks5ProxyProvider.java @@ -24,6 +24,7 @@ import haveno.common.config.Config; import haveno.network.p2p.network.NetworkNode; import java.net.UnknownHostException; import javax.annotation.Nullable; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -96,8 +97,7 @@ public class Socks5ProxyProvider { try { return new Socks5Proxy(tokens[0], Integer.valueOf(tokens[1])); } catch (UnknownHostException e) { - log.error(e.getMessage()); - e.printStackTrace(); + log.error(ExceptionUtils.getStackTrace(e)); } } else { log.error("Incorrect format for socks5ProxyAddress. Should be: host:port.\n" + diff --git a/p2p/src/main/java/haveno/network/p2p/P2PService.java b/p2p/src/main/java/haveno/network/p2p/P2PService.java index a8028d0e..117ed4a4 100644 --- a/p2p/src/main/java/haveno/network/p2p/P2PService.java +++ b/p2p/src/main/java/haveno/network/p2p/P2PService.java @@ -57,6 +57,8 @@ import javafx.beans.property.ReadOnlyIntegerProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; import lombok.Getter; + +import org.apache.commons.lang3.exception.ExceptionUtils; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; import org.fxmisc.easybind.monadic.MonadicBinding; @@ -433,15 +435,12 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis @Override public void onFailure(@NotNull Throwable throwable) { - log.error(throwable.toString()); - throwable.printStackTrace(); + log.error(ExceptionUtils.getStackTrace(throwable)); sendDirectMessageListener.onFault(throwable.toString()); } }, MoreExecutors.directExecutor()); } catch (CryptoException e) { - e.printStackTrace(); - log.error(message.toString()); - log.error(e.toString()); + log.error("Error sending encrypted direct message, message={}, error={}\n", message.toString(), e.getMessage(), e); sendDirectMessageListener.onFault(e.toString()); } } diff --git a/p2p/src/main/java/haveno/network/p2p/mailbox/MailboxMessageList.java b/p2p/src/main/java/haveno/network/p2p/mailbox/MailboxMessageList.java index a9d04949..451d3e7e 100644 --- a/p2p/src/main/java/haveno/network/p2p/mailbox/MailboxMessageList.java +++ b/p2p/src/main/java/haveno/network/p2p/mailbox/MailboxMessageList.java @@ -63,8 +63,7 @@ public class MailboxMessageList extends PersistableList { try { return MailboxItem.fromProto(e, networkProtoResolver); } catch (ProtobufferException protobufferException) { - protobufferException.printStackTrace(); - log.error("Error at MailboxItem.fromProto: {}", protobufferException.toString()); + log.error("Error at MailboxItem.fromProto: {}", protobufferException.toString(), protobufferException); return null; } }) diff --git a/p2p/src/main/java/haveno/network/p2p/mailbox/MailboxMessageService.java b/p2p/src/main/java/haveno/network/p2p/mailbox/MailboxMessageService.java index 131b5372..c447b3fc 100644 --- a/p2p/src/main/java/haveno/network/p2p/mailbox/MailboxMessageService.java +++ b/p2p/src/main/java/haveno/network/p2p/mailbox/MailboxMessageService.java @@ -335,8 +335,7 @@ public class MailboxMessageService implements HashMapChangedListener, PersistedD } }, MoreExecutors.directExecutor()); } catch (CryptoException e) { - log.error("sendEncryptedMessage failed"); - e.printStackTrace(); + log.error("sendEncryptedMessage failed: {}\n", e.getMessage(), e); sendMailboxMessageListener.onFault("sendEncryptedMailboxMessage failed " + e); } } @@ -644,8 +643,7 @@ public class MailboxMessageService implements HashMapChangedListener, PersistedD log.info("The mailboxEntry was already removed earlier."); } } catch (CryptoException e) { - e.printStackTrace(); - log.error("Could not remove ProtectedMailboxStorageEntry from network. Error: {}", e.toString()); + log.error("Could not remove ProtectedMailboxStorageEntry from network. Error: {}\n", e.toString(), e); } } diff --git a/p2p/src/main/java/haveno/network/p2p/network/Connection.java b/p2p/src/main/java/haveno/network/p2p/network/Connection.java index 92eddb06..1a7f1b84 100644 --- a/p2p/src/main/java/haveno/network/p2p/network/Connection.java +++ b/p2p/src/main/java/haveno/network/p2p/network/Connection.java @@ -91,6 +91,8 @@ import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import lombok.Getter; import lombok.extern.slf4j.Slf4j; + +import org.apache.commons.lang3.exception.ExceptionUtils; import org.jetbrains.annotations.Nullable; /** @@ -511,8 +513,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { Uninterruptibles.sleepUninterruptibly(200, TimeUnit.MILLISECONDS); } catch (Throwable t) { - log.error(t.getMessage()); - t.printStackTrace(); + log.error(ExceptionUtils.getStackTrace(t)); } finally { stopped = true; ThreadUtils.execute(() -> doShutDown(closeConnectionReason, shutDownCompleteHandler), THREAD_ID); @@ -537,16 +538,14 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { } catch (SocketException e) { log.trace("SocketException at shutdown might be expected {}", e.getMessage()); } catch (IOException e) { - log.error("Exception at shutdown. " + e.getMessage()); - e.printStackTrace(); + log.error("Exception at shutdown. {}\n", e.getMessage(), e); } finally { capabilitiesListeners.clear(); try { protoInputStream.close(); } catch (IOException e) { - log.error(e.getMessage()); - e.printStackTrace(); + log.error(ExceptionUtils.getStackTrace(e)); } Utilities.shutdownAndAwaitTermination(executorService, SHUTDOWN_TIMEOUT, TimeUnit.MILLISECONDS); diff --git a/p2p/src/main/java/haveno/network/p2p/network/LocalhostNetworkNode.java b/p2p/src/main/java/haveno/network/p2p/network/LocalhostNetworkNode.java index 9254c9af..26005988 100644 --- a/p2p/src/main/java/haveno/network/p2p/network/LocalhostNetworkNode.java +++ b/p2p/src/main/java/haveno/network/p2p/network/LocalhostNetworkNode.java @@ -76,8 +76,7 @@ public class LocalhostNetworkNode extends NetworkNode { try { startServer(new ServerSocket(servicePort)); } catch (IOException e) { - e.printStackTrace(); - log.error("Exception at startServer: " + e.getMessage()); + log.error("Exception at startServer: {}\n", e.getMessage(), e); } setupListeners.stream().forEach(SetupListener::onHiddenServicePublished); }, simulateTorDelayTorNode, TimeUnit.MILLISECONDS); diff --git a/p2p/src/main/java/haveno/network/p2p/network/Server.java b/p2p/src/main/java/haveno/network/p2p/network/Server.java index 9cf39f57..437e3d2f 100644 --- a/p2p/src/main/java/haveno/network/p2p/network/Server.java +++ b/p2p/src/main/java/haveno/network/p2p/network/Server.java @@ -97,11 +97,10 @@ class Server implements Runnable { } } catch (IOException e) { if (isServerActive()) - e.printStackTrace(); + log.error("Error executing server loop: {}\n", e.getMessage(), e); } } catch (Throwable t) { - log.error("Executing task failed. " + t.getMessage()); - t.printStackTrace(); + log.error("Executing task failed: {}\n", t.getMessage(), t); } } diff --git a/p2p/src/main/java/haveno/network/p2p/storage/P2PDataStorage.java b/p2p/src/main/java/haveno/network/p2p/storage/P2PDataStorage.java index a9c0f6ad..40be21ef 100644 --- a/p2p/src/main/java/haveno/network/p2p/storage/P2PDataStorage.java +++ b/p2p/src/main/java/haveno/network/p2p/storage/P2PDataStorage.java @@ -974,8 +974,7 @@ public class P2PDataStorage implements MessageListener, ConnectionListener, Pers broadcaster.broadcast(refreshTTLMessage, sender); } catch (IllegalArgumentException e) { - log.error("refreshTTL failed, missing data: {}", e.toString()); - e.printStackTrace(); + log.error("refreshTTL failed, missing data: {}\n", e.toString(), e); return false; } return true; diff --git a/p2p/src/main/java/haveno/network/p2p/storage/persistence/StoreService.java b/p2p/src/main/java/haveno/network/p2p/storage/persistence/StoreService.java index 6d9f3df1..17c940ad 100644 --- a/p2p/src/main/java/haveno/network/p2p/storage/persistence/StoreService.java +++ b/p2p/src/main/java/haveno/network/p2p/storage/persistence/StoreService.java @@ -116,8 +116,7 @@ public abstract class StoreService { log.debug("Could not find resourceFile " + resourceFileName + ". That is expected if none is provided yet."); } catch (Throwable e) { log.error("Could not copy resourceFile " + resourceFileName + " to " + - destinationFile.getAbsolutePath() + ".\n" + e.getMessage()); - e.printStackTrace(); + destinationFile.getAbsolutePath() + ".\n", e); } } else { log.debug("No resource file was copied. {} exists already.", fileName); diff --git a/seednode/src/main/java/haveno/seednode/SeedNodeMain.java b/seednode/src/main/java/haveno/seednode/SeedNodeMain.java index 5659ab2e..455f1809 100644 --- a/seednode/src/main/java/haveno/seednode/SeedNodeMain.java +++ b/seednode/src/main/java/haveno/seednode/SeedNodeMain.java @@ -75,7 +75,7 @@ public class SeedNodeMain extends ExecutableForAppWithP2p { seedNode = new SeedNode(); UserThread.execute(this::onApplicationLaunched); } catch (Exception e) { - e.printStackTrace(); + log.error("Error launching seed node: {}\n", e.toString(), e); } }); } From 6c640ddbefcfef62806775134ae82240a4a17b8b Mon Sep 17 00:00:00 2001 From: woodser Date: Thu, 26 Sep 2024 08:28:35 -0400 Subject: [PATCH 12/21] resize donation qrs --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 465e04f9..7f4d1eb2 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ To bring Haveno to life, we need resources. If you have the possibility, please ### Monero

- Donate Monero
+ Donate Monero
42sjokkT9FmiWPqVzrWPFE5NCJXwt96bkBozHf4vgLR9hXyJDqKHEHKVscAARuD7in5wV1meEcSTJTanCTDzidTe2cFXS1F

@@ -81,6 +81,6 @@ If you are using a wallet that supports OpenAlias (like the 'official' CLI and G ### Bitcoin

- Donate Bitcoin
+ Donate Bitcoin
1AKq3CE1yBAnxGmHXbNFfNYStcByNDc5gQ

From 1329902a553455efd6771b73760684e61e7bfbf9 Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 25 Sep 2024 11:34:12 -0400 Subject: [PATCH 13/21] add transaction fee column to funds > transactions Co-authored-by: niyid --- .../transactions/TransactionsListItem.java | 10 +++++ .../funds/transactions/TransactionsView.fxml | 3 +- .../funds/transactions/TransactionsView.java | 42 ++++++++++++++++--- 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsListItem.java b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsListItem.java index c9ef1d6c..f0baf8c9 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsListItem.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsListItem.java @@ -54,6 +54,7 @@ class TransactionsListItem { private boolean received; private boolean detailsAvailable; private BigInteger amount = BigInteger.ZERO; + private BigInteger txFee = BigInteger.ZERO; private String memo = ""; private long confirmations = 0; @Getter @@ -107,6 +108,7 @@ class TransactionsListItem { amount = valueSentFromMe.multiply(BigInteger.valueOf(-1)); received = false; direction = Res.get("funds.tx.direction.sentTo"); + txFee = tx.getFee().multiply(BigInteger.valueOf(-1)); } if (optionalTradable.isPresent()) { @@ -201,6 +203,14 @@ class TransactionsListItem { return amount; } + public BigInteger getTxFee() { + return txFee; + } + + public String getTxFeeStr() { + return txFee.equals(BigInteger.ZERO) ? "" : HavenoUtils.formatXmr(txFee); + } + public String getAddressString() { return addressString; } diff --git a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.fxml b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.fxml index 8cd53a17..7c5da978 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.fxml +++ b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.fxml @@ -36,7 +36,8 @@ - + + diff --git a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.java b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.java index f5cca952..44fdab6b 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/transactions/TransactionsView.java @@ -70,7 +70,7 @@ public class TransactionsView extends ActivatableView { @FXML TableView tableView; @FXML - TableColumn dateColumn, detailsColumn, addressColumn, transactionColumn, amountColumn, memoColumn, confidenceColumn, revertTxColumn; + TableColumn dateColumn, detailsColumn, addressColumn, transactionColumn, amountColumn, txFeeColumn, memoColumn, confidenceColumn, revertTxColumn; @FXML Label numItems; @FXML @@ -89,7 +89,7 @@ public class TransactionsView extends ActivatableView { private EventHandler keyEventEventHandler; private Scene scene; - private TransactionsUpdater transactionsUpdater = new TransactionsUpdater(); + private final TransactionsUpdater transactionsUpdater = new TransactionsUpdater(); private class TransactionsUpdater extends MoneroWalletListener { @Override @@ -129,11 +129,12 @@ public class TransactionsView extends ActivatableView { addressColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.address"))); transactionColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.txId", Res.getBaseCurrencyCode()))); amountColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amountWithCur", Res.getBaseCurrencyCode()))); + txFeeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.txFee", Res.getBaseCurrencyCode()))); memoColumn.setGraphic(new AutoTooltipLabel(Res.get("funds.tx.memo"))); confidenceColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.confirmations", Res.getBaseCurrencyCode()))); revertTxColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.revert", Res.getBaseCurrencyCode()))); - tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN); tableView.setPlaceholder(new AutoTooltipLabel(Res.get("funds.tx.noTxAvailable"))); setDateColumnCellFactory(); @@ -141,6 +142,7 @@ public class TransactionsView extends ActivatableView { setAddressColumnCellFactory(); setTransactionColumnCellFactory(); setAmountColumnCellFactory(); + setTxFeeColumnCellFactory(); setMemoColumnCellFactory(); setConfidenceColumnCellFactory(); setRevertTxColumnCellFactory(); @@ -156,7 +158,7 @@ public class TransactionsView extends ActivatableView { addressColumn.setComparator(Comparator.comparing(item -> item.getDirection() + item.getAddressString())); transactionColumn.setComparator(Comparator.comparing(TransactionsListItem::getTxId)); amountColumn.setComparator(Comparator.comparing(TransactionsListItem::getAmount)); - confidenceColumn.setComparator(Comparator.comparingLong(item -> item.getNumConfirmations())); + confidenceColumn.setComparator(Comparator.comparingLong(TransactionsListItem::getNumConfirmations)); memoColumn.setComparator(Comparator.comparing(TransactionsListItem::getMemo)); dateColumn.setSortType(TableColumn.SortType.DESCENDING); @@ -216,8 +218,9 @@ public class TransactionsView extends ActivatableView { columns[2] = item.getDirection() + " " + item.getAddressString(); columns[3] = item.getTxId(); columns[4] = item.getAmountStr(); - columns[5] = item.getMemo() == null ? "" : item.getMemo(); - columns[6] = String.valueOf(item.getNumConfirmations()); + columns[5] = item.getTxFeeStr(); + columns[6] = item.getMemo() == null ? "" : item.getMemo(); + columns[7] = String.valueOf(item.getNumConfirmations()); return columns; }; @@ -414,6 +417,33 @@ public class TransactionsView extends ActivatableView { }); } + + private void setTxFeeColumnCellFactory() { + txFeeColumn.setCellValueFactory((addressListItem) -> + new ReadOnlyObjectWrapper<>(addressListItem.getValue())); + txFeeColumn.setCellFactory( + new Callback<>() { + + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + + @Override + public void updateItem(final TransactionsListItem item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + setGraphic(new AutoTooltipLabel(item.getTxFeeStr())); + } else { + setGraphic(null); + } + } + }; + } + }); + } + private void setMemoColumnCellFactory() { memoColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); From 11c0f7613ba1aa28c90d2dda479700ac316612aa Mon Sep 17 00:00:00 2001 From: "justynboyer@gmail.com" Date: Wed, 25 Sep 2024 23:56:28 +0100 Subject: [PATCH 14/21] fix(ci): pin ubuntu ci version --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 42a08d2b..3a0d85c1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ jobs: build: strategy: matrix: - os: [ubuntu-latest, macos-13, windows-latest] + os: [ubuntu-22.04, macos-13, windows-latest] fail-fast: false runs-on: ${{ matrix.os }} steps: From 60b91d3d237b4b7e2d92f31e51033a980622a1ad Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 30 Sep 2024 09:19:16 -0400 Subject: [PATCH 15/21] fix build artifacts for ubuntu-22.04 --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3a0d85c1..3e961db0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,7 +37,7 @@ jobs: name: cached-localnet path: .localnet - name: Install dependencies - if: ${{ matrix.os == 'ubuntu-latest' }} + if: ${{ matrix.os == 'ubuntu-22.04' }} run: | sudo apt update sudo apt install -y rpm @@ -53,10 +53,10 @@ jobs: ./gradlew packageInstallers working-directory: . - name: Move Release Files on Unix - if: ${{ matrix.os == 'ubuntu-latest' || matrix.os == 'macos-13' }} + if: ${{ matrix.os == 'ubuntu-22.04' || matrix.os == 'macos-13' }} run: | mkdir ${{ github.workspace }}/release - if [ "${{ matrix.os }}" == "ubuntu-latest" ]; then + if [ "${{ matrix.os }}" == "ubuntu-22.04" ]; then mv desktop/build/temp-*/binaries/haveno-*.rpm ${{ github.workspace }}/release mv desktop/build/temp-*/binaries/haveno_*.deb ${{ github.workspace }}/release else From 3e3f3085f885a5dd26455e98a3bb8bd965844d15 Mon Sep 17 00:00:00 2001 From: woodser Date: Wed, 25 Sep 2024 09:41:25 -0400 Subject: [PATCH 16/21] fix not enough signers on process payout tx --- .../haveno/core/support/SupportManager.java | 2 +- .../core/support/dispute/DisputeManager.java | 2 +- .../arbitration/ArbitrationManager.java | 2 +- .../java/haveno/core/trade/HavenoUtils.java | 16 ++++++++++---- .../main/java/haveno/core/trade/Trade.java | 21 ++++++++++--------- .../tasks/ProcessPaymentReceivedMessage.java | 3 ++- .../SellerPreparePaymentReceivedMessage.java | 4 ++-- 7 files changed, 30 insertions(+), 20 deletions(-) diff --git a/core/src/main/java/haveno/core/support/SupportManager.java b/core/src/main/java/haveno/core/support/SupportManager.java index d7615448..10cbfdaf 100644 --- a/core/src/main/java/haveno/core/support/SupportManager.java +++ b/core/src/main/java/haveno/core/support/SupportManager.java @@ -199,7 +199,7 @@ public abstract class SupportManager { if (dispute.isClosed()) dispute.reOpen(); trade.advanceDisputeState(Trade.DisputeState.DISPUTE_OPENED); } else if (dispute.isClosed()) { - trade.pollWalletNormallyForMs(30000); // sync to check for payout + trade.pollWalletNormallyForMs(60000); // sync to check for payout } } } diff --git a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java index bd124fda..eb49e0c5 100644 --- a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java @@ -854,7 +854,7 @@ public abstract class DisputeManager> extends Sup // the state, as that is displayed to the user and we only persist that msg disputeResult.getChatMessage().setArrived(true); trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG); - trade.pollWalletNormallyForMs(30000); + trade.pollWalletNormallyForMs(60000); requestPersistence(trade); resultHandler.handleResult(); } diff --git a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java index 719ac462..50be387c 100644 --- a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java +++ b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java @@ -361,7 +361,7 @@ public final class ArbitrationManager extends DisputeManager Date: Thu, 26 Sep 2024 12:58:19 -0400 Subject: [PATCH 17/21] fix account export and import without key ring #1221 --- .../cryptoaccounts/CryptoAccountsDataModel.java | 10 +++------- .../TraditionalAccountsDataModel.java | 10 +++------- .../src/main/java/haveno/desktop/util/GUIUtil.java | 11 ++++------- 3 files changed, 10 insertions(+), 21 deletions(-) diff --git a/desktop/src/main/java/haveno/desktop/main/account/content/cryptoaccounts/CryptoAccountsDataModel.java b/desktop/src/main/java/haveno/desktop/main/account/content/cryptoaccounts/CryptoAccountsDataModel.java index d6b937ac..95fa4bbd 100644 --- a/desktop/src/main/java/haveno/desktop/main/account/content/cryptoaccounts/CryptoAccountsDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/account/content/cryptoaccounts/CryptoAccountsDataModel.java @@ -18,7 +18,6 @@ package haveno.desktop.main.account.content.cryptoaccounts; import com.google.inject.Inject; -import haveno.common.crypto.KeyRing; import haveno.common.file.CorruptedStorageFileHandler; import haveno.common.proto.persistable.PersistenceProtoResolver; import haveno.core.account.witness.AccountAgeWitnessService; @@ -55,7 +54,6 @@ class CryptoAccountsDataModel extends ActivatableDataModel { private final String accountsFileName = "CryptoPaymentAccounts"; private final PersistenceProtoResolver persistenceProtoResolver; private final CorruptedStorageFileHandler corruptedStorageFileHandler; - private final KeyRing keyRing; @Inject public CryptoAccountsDataModel(User user, @@ -64,8 +62,7 @@ class CryptoAccountsDataModel extends ActivatableDataModel { TradeManager tradeManager, AccountAgeWitnessService accountAgeWitnessService, PersistenceProtoResolver persistenceProtoResolver, - CorruptedStorageFileHandler corruptedStorageFileHandler, - KeyRing keyRing) { + CorruptedStorageFileHandler corruptedStorageFileHandler) { this.user = user; this.preferences = preferences; this.openOfferManager = openOfferManager; @@ -73,7 +70,6 @@ class CryptoAccountsDataModel extends ActivatableDataModel { this.accountAgeWitnessService = accountAgeWitnessService; this.persistenceProtoResolver = persistenceProtoResolver; this.corruptedStorageFileHandler = corruptedStorageFileHandler; - this.keyRing = keyRing; setChangeListener = change -> fillAndSortPaymentAccounts(); } @@ -157,12 +153,12 @@ class CryptoAccountsDataModel extends ActivatableDataModel { ArrayList accounts = new ArrayList<>(user.getPaymentAccounts().stream() .filter(paymentAccount -> paymentAccount instanceof AssetAccount) .collect(Collectors.toList())); - GUIUtil.exportAccounts(accounts, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler, keyRing); + GUIUtil.exportAccounts(accounts, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler); } } public void importAccounts(Stage stage) { - GUIUtil.importAccounts(user, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler, keyRing); + GUIUtil.importAccounts(user, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler); } public int getNumPaymentAccounts() { diff --git a/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsDataModel.java b/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsDataModel.java index b72b89fe..909aa945 100644 --- a/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsDataModel.java @@ -18,7 +18,6 @@ package haveno.desktop.main.account.content.traditionalaccounts; import com.google.inject.Inject; -import haveno.common.crypto.KeyRing; import haveno.common.file.CorruptedStorageFileHandler; import haveno.common.proto.persistable.PersistenceProtoResolver; import haveno.core.account.witness.AccountAgeWitnessService; @@ -56,7 +55,6 @@ class TraditionalAccountsDataModel extends ActivatableDataModel { private final String accountsFileName = "FiatPaymentAccounts"; private final PersistenceProtoResolver persistenceProtoResolver; private final CorruptedStorageFileHandler corruptedStorageFileHandler; - private final KeyRing keyRing; @Inject public TraditionalAccountsDataModel(User user, @@ -65,8 +63,7 @@ class TraditionalAccountsDataModel extends ActivatableDataModel { TradeManager tradeManager, AccountAgeWitnessService accountAgeWitnessService, PersistenceProtoResolver persistenceProtoResolver, - CorruptedStorageFileHandler corruptedStorageFileHandler, - KeyRing keyRing) { + CorruptedStorageFileHandler corruptedStorageFileHandler) { this.user = user; this.preferences = preferences; this.openOfferManager = openOfferManager; @@ -74,7 +71,6 @@ class TraditionalAccountsDataModel extends ActivatableDataModel { this.accountAgeWitnessService = accountAgeWitnessService; this.persistenceProtoResolver = persistenceProtoResolver; this.corruptedStorageFileHandler = corruptedStorageFileHandler; - this.keyRing = keyRing; setChangeListener = change -> fillAndSortPaymentAccounts(); } @@ -159,12 +155,12 @@ class TraditionalAccountsDataModel extends ActivatableDataModel { ArrayList accounts = new ArrayList<>(user.getPaymentAccounts().stream() .filter(paymentAccount -> !(paymentAccount instanceof AssetAccount)) .collect(Collectors.toList())); - GUIUtil.exportAccounts(accounts, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler, keyRing); + GUIUtil.exportAccounts(accounts, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler); } } public void importAccounts(Stage stage) { - GUIUtil.importAccounts(user, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler, keyRing); + GUIUtil.importAccounts(user, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler); } public int getNumPaymentAccounts() { diff --git a/desktop/src/main/java/haveno/desktop/util/GUIUtil.java b/desktop/src/main/java/haveno/desktop/util/GUIUtil.java index f2ad96c2..2c314ea5 100644 --- a/desktop/src/main/java/haveno/desktop/util/GUIUtil.java +++ b/desktop/src/main/java/haveno/desktop/util/GUIUtil.java @@ -28,7 +28,6 @@ import com.googlecode.jcsv.writer.internal.CSVWriterBuilder; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import haveno.common.UserThread; import haveno.common.config.Config; -import haveno.common.crypto.KeyRing; import haveno.common.file.CorruptedStorageFileHandler; import haveno.common.persistence.PersistenceManager; import haveno.common.proto.persistable.PersistableEnvelope; @@ -168,12 +167,11 @@ public class GUIUtil { Preferences preferences, Stage stage, PersistenceProtoResolver persistenceProtoResolver, - CorruptedStorageFileHandler corruptedStorageFileHandler, - KeyRing keyRing) { + CorruptedStorageFileHandler corruptedStorageFileHandler) { if (!accounts.isEmpty()) { String directory = getDirectoryFromChooser(preferences, stage); if (!directory.isEmpty()) { - PersistenceManager persistenceManager = new PersistenceManager<>(new File(directory), persistenceProtoResolver, corruptedStorageFileHandler, keyRing); + PersistenceManager persistenceManager = new PersistenceManager<>(new File(directory), persistenceProtoResolver, corruptedStorageFileHandler, null); PaymentAccountList paymentAccounts = new PaymentAccountList(accounts); persistenceManager.initialize(paymentAccounts, fileName, PersistenceManager.Source.PRIVATE_LOW_PRIO); persistenceManager.persistNow(() -> { @@ -193,8 +191,7 @@ public class GUIUtil { Preferences preferences, Stage stage, PersistenceProtoResolver persistenceProtoResolver, - CorruptedStorageFileHandler corruptedStorageFileHandler, - KeyRing keyRing) { + CorruptedStorageFileHandler corruptedStorageFileHandler) { FileChooser fileChooser = new FileChooser(); File initDir = new File(preferences.getDirectoryChooserPath()); if (initDir.isDirectory()) { @@ -207,7 +204,7 @@ public class GUIUtil { if (Paths.get(path).getFileName().toString().equals(fileName)) { String directory = Paths.get(path).getParent().toString(); preferences.setDirectoryChooserPath(directory); - PersistenceManager persistenceManager = new PersistenceManager<>(new File(directory), persistenceProtoResolver, corruptedStorageFileHandler, keyRing); + PersistenceManager persistenceManager = new PersistenceManager<>(new File(directory), persistenceProtoResolver, corruptedStorageFileHandler, null); persistenceManager.readPersisted(fileName, persisted -> { StringBuilder msg = new StringBuilder(); HashSet paymentAccounts = new HashSet<>(); From b2a6708ac17aba2f7fc2a45594c7205dc25892bc Mon Sep 17 00:00:00 2001 From: woodser Date: Fri, 27 Sep 2024 12:09:35 -0400 Subject: [PATCH 18/21] sync blockchain depending on last used local node --- core/src/main/java/haveno/core/api/CoreApi.java | 4 ++-- .../java/haveno/core/api/XmrConnectionService.java | 8 +++++++- .../src/main/java/haveno/core/api/XmrLocalNode.java | 13 +++++++++---- .../main/java/haveno/core/xmr/XmrNodeSettings.java | 6 +++++- proto/src/main/proto/pb.proto | 1 + 5 files changed, 24 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/haveno/core/api/CoreApi.java b/core/src/main/java/haveno/core/api/CoreApi.java index 25f98c34..9afc4ee2 100644 --- a/core/src/main/java/haveno/core/api/CoreApi.java +++ b/core/src/main/java/haveno/core/api/CoreApi.java @@ -260,11 +260,11 @@ public class CoreApi { } public void startXmrNode(XmrNodeSettings settings) throws IOException { - xmrLocalNode.startNode(settings); + xmrLocalNode.start(settings); } public void stopXmrNode() { - xmrLocalNode.stopNode(); + xmrLocalNode.stop(); } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index 2541fb9f..ffeacbfd 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -623,7 +623,7 @@ public final class XmrConnectionService { if (connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored()) { try { log.info("Starting local node"); - xmrLocalNode.startMoneroNode(); + xmrLocalNode.start(); } catch (Exception e) { log.error("Unable to start local monero node, error={}\n", e.getMessage(), e); } @@ -736,6 +736,12 @@ public final class XmrConnectionService { // connected to daemon isConnected = true; + // determine if blockchain is syncing locally + boolean blockchainSyncing = lastInfo.getHeight().equals(lastInfo.getHeightWithoutBootstrap()) || (lastInfo.getTargetHeight().equals(0l) && lastInfo.getHeightWithoutBootstrap().equals(0l)); // blockchain is syncing if height equals height without bootstrap, or target height and height without bootstrap both equal 0 + + // write sync status to preferences + preferences.getXmrNodeSettings().setSyncBlockchain(blockchainSyncing); + // throttle warnings if daemon not synced if (!isSyncedWithinTolerance() && System.currentTimeMillis() - lastLogDaemonNotSyncedTimestamp > HavenoUtils.LOG_DAEMON_NOT_SYNCED_WARN_PERIOD_MS) { log.warn("Our chain height: {} is out of sync with peer nodes chain height: {}", chainHeight.get(), getTargetHeight()); diff --git a/core/src/main/java/haveno/core/api/XmrLocalNode.java b/core/src/main/java/haveno/core/api/XmrLocalNode.java index 80682e9a..cd5ed266 100644 --- a/core/src/main/java/haveno/core/api/XmrLocalNode.java +++ b/core/src/main/java/haveno/core/api/XmrLocalNode.java @@ -150,16 +150,16 @@ public class XmrLocalNode { /** * Start a local Monero node from settings. */ - public void startMoneroNode() throws IOException { + public void start() throws IOException { var settings = preferences.getXmrNodeSettings(); - this.startNode(settings); + this.start(settings); } /** * Start local Monero node. Throws MoneroError if the node cannot be started. * Persist the settings to preferences if the node started successfully. */ - public void startNode(XmrNodeSettings settings) throws IOException { + public void start(XmrNodeSettings settings) throws IOException { if (isDetected()) throw new IllegalStateException("Local Monero node already online"); log.info("Starting local Monero node: " + settings); @@ -177,6 +177,11 @@ public class XmrLocalNode { args.add("--bootstrap-daemon-address=" + bootstrapUrl); } + var syncBlockchain = settings.getSyncBlockchain(); + if (syncBlockchain != null && !syncBlockchain) { + args.add("--no-sync"); + } + var flags = settings.getStartupFlags(); if (flags != null) { args.addAll(flags); @@ -191,7 +196,7 @@ public class XmrLocalNode { * Stop the current local Monero node if we own its process. * Does not remove the last XmrNodeSettings. */ - public void stopNode() { + public void stop() { if (!isDetected()) throw new IllegalStateException("Local Monero node is not running"); if (daemon.getProcess() == null || !daemon.getProcess().isAlive()) throw new IllegalStateException("Cannot stop local Monero node because we don't own its process"); // TODO (woodser): remove isAlive() check after monero-java 0.5.4 which nullifies internal process daemon.stopProcess(); diff --git a/core/src/main/java/haveno/core/xmr/XmrNodeSettings.java b/core/src/main/java/haveno/core/xmr/XmrNodeSettings.java index 0db9868f..9802f036 100644 --- a/core/src/main/java/haveno/core/xmr/XmrNodeSettings.java +++ b/core/src/main/java/haveno/core/xmr/XmrNodeSettings.java @@ -35,6 +35,8 @@ public class XmrNodeSettings implements PersistableEnvelope { String bootstrapUrl; @Nullable List startupFlags; + @Nullable + Boolean syncBlockchain; public XmrNodeSettings() { } @@ -43,7 +45,8 @@ public class XmrNodeSettings implements PersistableEnvelope { return new XmrNodeSettings( proto.getBlockchainPath(), proto.getBootstrapUrl(), - proto.getStartupFlagsList()); + proto.getStartupFlagsList(), + proto.getSyncBlockchain()); } @Override @@ -52,6 +55,7 @@ public class XmrNodeSettings implements PersistableEnvelope { Optional.ofNullable(blockchainPath).ifPresent(e -> builder.setBlockchainPath(blockchainPath)); Optional.ofNullable(bootstrapUrl).ifPresent(e -> builder.setBootstrapUrl(bootstrapUrl)); Optional.ofNullable(startupFlags).ifPresent(e -> builder.addAllStartupFlags(startupFlags)); + Optional.ofNullable(syncBlockchain).ifPresent(e -> builder.setSyncBlockchain(syncBlockchain)); return builder.build(); } } diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 03cc6b23..3daf8d37 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -1754,6 +1754,7 @@ message XmrNodeSettings { string blockchain_path = 1; string bootstrap_url = 2; repeated string startup_flags = 3; + bool sync_blockchain = 4; } /////////////////////////////////////////////////////////////////////////////////////////// From b940021d999895f91a08ab27678ae934ef9b5b8e Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 30 Sep 2024 10:13:21 -0400 Subject: [PATCH 19/21] play sounds on notifications #1284 --- .../core/api/CoreNotificationService.java | 15 +++- .../haveno/core/api/model/XmrBalanceInfo.java | 2 +- .../java/haveno/core/app/HavenoSetup.java | 7 ++ .../java/haveno/core/trade/HavenoUtils.java | 75 ++++++++++++++++++ .../main/java/haveno/core/trade/Trade.java | 8 ++ .../java/haveno/core/trade/TradeManager.java | 14 +--- .../java/haveno/core/user/Preferences.java | 14 ++++ .../haveno/core/user/PreferencesPayload.java | 3 + .../main/java/haveno/core/xmr/Balances.java | 23 +++++- core/src/main/resources/cash_register.wav | Bin 0 -> 815986 bytes core/src/main/resources/chime.wav | Bin 0 -> 435608 bytes .../resources/i18n/displayStrings.properties | 1 + .../i18n/displayStrings_cs.properties | 1 + .../i18n/displayStrings_de.properties | 1 + .../i18n/displayStrings_es.properties | 1 + .../i18n/displayStrings_fa.properties | 1 + .../i18n/displayStrings_fr.properties | 1 + .../i18n/displayStrings_it.properties | 1 + .../i18n/displayStrings_ja.properties | 1 + .../i18n/displayStrings_pt-br.properties | 1 + .../i18n/displayStrings_pt.properties | 1 + .../i18n/displayStrings_ru.properties | 1 + .../i18n/displayStrings_th.properties | 1 + .../i18n/displayStrings_tr.properties | 1 + .../i18n/displayStrings_vi.properties | 1 + .../i18n/displayStrings_zh-hans.properties | 1 + .../i18n/displayStrings_zh-hant.properties | 1 + .../settings/preferences/PreferencesView.java | 11 ++- proto/src/main/proto/pb.proto | 1 + 29 files changed, 173 insertions(+), 16 deletions(-) create mode 100644 core/src/main/resources/cash_register.wav create mode 100644 core/src/main/resources/chime.wav diff --git a/core/src/main/java/haveno/core/api/CoreNotificationService.java b/core/src/main/java/haveno/core/api/CoreNotificationService.java index 6c40d749..484208d0 100644 --- a/core/src/main/java/haveno/core/api/CoreNotificationService.java +++ b/core/src/main/java/haveno/core/api/CoreNotificationService.java @@ -3,7 +3,11 @@ package haveno.core.api; import com.google.inject.Singleton; import haveno.core.api.model.TradeInfo; import haveno.core.support.messages.ChatMessage; +import haveno.core.trade.HavenoUtils; +import haveno.core.trade.MakerTrade; +import haveno.core.trade.SellerTrade; import haveno.core.trade.Trade; +import haveno.core.trade.Trade.Phase; import haveno.proto.grpc.NotificationMessage; import haveno.proto.grpc.NotificationMessage.NotificationType; import java.util.Iterator; @@ -46,7 +50,15 @@ public class CoreNotificationService { .build()); } - public void sendTradeNotification(Trade trade, String title, String message) { + public void sendTradeNotification(Trade trade, Phase phase, String title, String message) { + + // play chime when maker's trade is taken + if (trade instanceof MakerTrade && phase == Trade.Phase.DEPOSITS_PUBLISHED) HavenoUtils.playChimeSound(); + + // play chime when seller sees buyer confirm payment sent + if (trade instanceof SellerTrade && phase == Trade.Phase.PAYMENT_SENT) HavenoUtils.playChimeSound(); + + // send notification sendNotification(NotificationMessage.newBuilder() .setType(NotificationType.TRADE_UPDATE) .setTrade(TradeInfo.toTradeInfo(trade).toProtoMessage()) @@ -57,6 +69,7 @@ public class CoreNotificationService { } public void sendChatNotification(ChatMessage chatMessage) { + HavenoUtils.playChimeSound(); sendNotification(NotificationMessage.newBuilder() .setType(NotificationType.CHAT_MESSAGE) .setTimestamp(System.currentTimeMillis()) diff --git a/core/src/main/java/haveno/core/api/model/XmrBalanceInfo.java b/core/src/main/java/haveno/core/api/model/XmrBalanceInfo.java index 2a9e23cd..76e661ae 100644 --- a/core/src/main/java/haveno/core/api/model/XmrBalanceInfo.java +++ b/core/src/main/java/haveno/core/api/model/XmrBalanceInfo.java @@ -98,7 +98,7 @@ public class XmrBalanceInfo implements Payload { public String toString() { return "XmrBalanceInfo{" + "balance=" + balance + - "unlockedBalance=" + availableBalance + + ", unlockedBalance=" + availableBalance + ", lockedBalance=" + pendingBalance + ", reservedOfferBalance=" + reservedOfferBalance + ", reservedTradeBalance=" + reservedTradeBalance + diff --git a/core/src/main/java/haveno/core/app/HavenoSetup.java b/core/src/main/java/haveno/core/app/HavenoSetup.java index 2c2e95fd..d80ca807 100644 --- a/core/src/main/java/haveno/core/app/HavenoSetup.java +++ b/core/src/main/java/haveno/core/app/HavenoSetup.java @@ -53,6 +53,7 @@ import haveno.core.alert.Alert; import haveno.core.alert.AlertManager; import haveno.core.alert.PrivateNotificationManager; import haveno.core.alert.PrivateNotificationPayload; +import haveno.core.api.CoreContext; import haveno.core.api.XmrConnectionService; import haveno.core.api.XmrLocalNode; import haveno.core.locale.Res; @@ -131,7 +132,10 @@ public class HavenoSetup { private final Preferences preferences; private final User user; private final AlertManager alertManager; + @Getter private final Config config; + @Getter + private final CoreContext coreContext; private final AccountAgeWitnessService accountAgeWitnessService; private final TorSetup torSetup; private final CoinFormatter formatter; @@ -228,6 +232,7 @@ public class HavenoSetup { User user, AlertManager alertManager, Config config, + CoreContext coreContext, AccountAgeWitnessService accountAgeWitnessService, TorSetup torSetup, @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, @@ -253,6 +258,7 @@ public class HavenoSetup { this.user = user; this.alertManager = alertManager; this.config = config; + this.coreContext = coreContext; this.accountAgeWitnessService = accountAgeWitnessService; this.torSetup = torSetup; this.formatter = formatter; @@ -263,6 +269,7 @@ public class HavenoSetup { this.arbitrationManager = arbitrationManager; HavenoUtils.havenoSetup = this; + HavenoUtils.preferences = preferences; } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/haveno/core/trade/HavenoUtils.java b/core/src/main/java/haveno/core/trade/HavenoUtils.java index deb14086..3032492b 100644 --- a/core/src/main/java/haveno/core/trade/HavenoUtils.java +++ b/core/src/main/java/haveno/core/trade/HavenoUtils.java @@ -27,7 +27,9 @@ import haveno.common.crypto.Hash; import haveno.common.crypto.KeyRing; import haveno.common.crypto.PubKeyRing; import haveno.common.crypto.Sig; +import haveno.common.file.FileUtil; import haveno.common.util.Utilities; +import haveno.core.api.CoreNotificationService; import haveno.core.api.XmrConnectionService; import haveno.core.app.HavenoSetup; import haveno.core.offer.OfferPayload; @@ -36,9 +38,12 @@ import haveno.core.support.dispute.arbitration.ArbitrationManager; import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator; import haveno.core.trade.messages.PaymentReceivedMessage; import haveno.core.trade.messages.PaymentSentMessage; +import haveno.core.user.Preferences; import haveno.core.util.JsonUtil; import haveno.core.xmr.wallet.XmrWalletService; import haveno.network.p2p.NodeAddress; + +import java.io.File; import java.math.BigDecimal; import java.math.BigInteger; import java.net.InetAddress; @@ -53,6 +58,13 @@ import java.util.Date; import java.util.List; import java.util.Locale; import java.util.concurrent.CountDownLatch; + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.DataLine; +import javax.sound.sampled.SourceDataLine; + import lombok.extern.slf4j.Slf4j; import monero.common.MoneroRpcConnection; import monero.daemon.model.MoneroOutput; @@ -110,11 +122,18 @@ public class HavenoUtils { public static XmrWalletService xmrWalletService; public static XmrConnectionService xmrConnectionService; public static OpenOfferManager openOfferManager; + public static CoreNotificationService notificationService; + public static Preferences preferences; public static boolean isSeedNode() { return havenoSetup == null; } + public static boolean isDaemon() { + if (isSeedNode()) return true; + return havenoSetup.getCoreContext().isApiUser(); + } + @SuppressWarnings("unused") public static Date getReleaseDate() { if (RELEASE_DATE == null) return null; @@ -533,4 +552,60 @@ public class HavenoUtils { public static boolean isIllegal(Throwable e) { return e instanceof IllegalArgumentException || e instanceof IllegalStateException; } + + public static void playChimeSound() { + playAudioFile("chime.wav"); + } + + public static void playCashRegisterSound() { + playAudioFile("cash_register.wav"); + } + + private static void playAudioFile(String fileName) { + if (isDaemon()) return; // ignore if running as daemon + if (!preferences.getUseSoundForNotificationsProperty().get()) return; // ignore if sounds disabled + new Thread(() -> { + try { + + // get audio file + File wavFile = new File(havenoSetup.getConfig().appDataDir, fileName); + if (!wavFile.exists()) FileUtil.resourceToFile(fileName, wavFile); + AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(wavFile); + + // get original format + AudioFormat baseFormat = audioInputStream.getFormat(); + + // set target format: PCM_SIGNED, 16-bit + AudioFormat targetFormat = new AudioFormat( + AudioFormat.Encoding.PCM_SIGNED, + baseFormat.getSampleRate(), + 16, // 16-bit instead of 32-bit float + baseFormat.getChannels(), + baseFormat.getChannels() * 2, // Frame size: 2 bytes per channel (16-bit) + baseFormat.getSampleRate(), + false // Little-endian + ); + + // convert audio to target format + AudioInputStream convertedStream = AudioSystem.getAudioInputStream(targetFormat, audioInputStream); + + // play audio + DataLine.Info info = new DataLine.Info(SourceDataLine.class, targetFormat); + SourceDataLine sourceLine = (SourceDataLine) AudioSystem.getLine(info); + sourceLine.open(targetFormat); + sourceLine.start(); + byte[] buffer = new byte[1024]; + int bytesRead = 0; + while ((bytesRead = convertedStream.read(buffer, 0, buffer.length)) != -1) { + sourceLine.write(buffer, 0, bytesRead); + } + sourceLine.drain(); + sourceLine.close(); + convertedStream.close(); + audioInputStream.close(); + } catch (Exception e) { + e.printStackTrace(); + } + }).start(); + } } diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index d3919919..5f51e8ea 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -649,6 +649,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { ThreadUtils.submitToPool(() -> { if (newValue == Trade.Phase.DEPOSIT_REQUESTED) startPolling(); if (newValue == Trade.Phase.DEPOSITS_PUBLISHED) onDepositsPublished(); + if (newValue == Trade.Phase.PAYMENT_SENT) onPaymentSent(); if (isDepositsPublished() && !isPayoutUnlocked()) updatePollPeriod(); if (isPaymentReceived()) { UserThread.execute(() -> { @@ -2833,6 +2834,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { // close open offer or reset address entries if (this instanceof MakerTrade) { processModel.getOpenOfferManager().closeOpenOffer(getOffer()); + HavenoUtils.notificationService.sendTradeNotification(this, Phase.DEPOSITS_PUBLISHED, "Offer Taken", "Your offer " + offer.getId() + " has been accepted"); // TODO (woodser): use language translation } else { getXmrWalletService().resetAddressEntriesForOpenOffer(getId()); } @@ -2841,6 +2843,12 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model { ThreadUtils.submitToPool(() -> xmrWalletService.freezeOutputs(getSelf().getReserveTxKeyImages())); } + private void onPaymentSent() { + if (this instanceof SellerTrade) { + HavenoUtils.notificationService.sendTradeNotification(this, Phase.PAYMENT_SENT, "Payment Sent", "The buyer has sent the payment"); // TODO (woodser): use language translation + } + } + /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index 9959a736..55acf63d 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -68,7 +68,6 @@ import haveno.core.support.dispute.mediation.mediator.MediatorManager; import haveno.core.support.dispute.messages.DisputeClosedMessage; import haveno.core.support.dispute.messages.DisputeOpenedMessage; import haveno.core.trade.Trade.DisputeState; -import haveno.core.trade.Trade.Phase; import haveno.core.trade.failed.FailedTradesManager; import haveno.core.trade.handlers.TradeResultHandler; import haveno.core.trade.messages.DepositRequest; @@ -134,7 +133,6 @@ import lombok.Setter; import monero.daemon.model.MoneroTx; import org.bitcoinj.core.Coin; import org.bouncycastle.crypto.params.KeyParameter; -import org.fxmisc.easybind.EasyBind; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -258,7 +256,9 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi failedTradesManager.setUnFailTradeCallback(this::unFailTrade); - xmrWalletService.setTradeManager(this); + // TODO: better way to set references + xmrWalletService.setTradeManager(this); // TODO: set reference in HavenoUtils for consistency + HavenoUtils.notificationService = notificationService; } @@ -599,14 +599,6 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi initTradeAndProtocol(trade, createTradeProtocol(trade)); addTrade(trade); - // notify on phase changes - // TODO (woodser): save subscription, bind on startup - EasyBind.subscribe(trade.statePhaseProperty(), phase -> { - if (phase == Phase.DEPOSITS_PUBLISHED) { - notificationService.sendTradeNotification(trade, "Offer Taken", "Your offer " + offer.getId() + " has been accepted"); // TODO (woodser): use language translation - } - }); - // process with protocol ((MakerProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> { log.warn("Maker error during trade initialization: " + errorMessage); diff --git a/core/src/main/java/haveno/core/user/Preferences.java b/core/src/main/java/haveno/core/user/Preferences.java index 311bd310..b57b5708 100644 --- a/core/src/main/java/haveno/core/user/Preferences.java +++ b/core/src/main/java/haveno/core/user/Preferences.java @@ -132,6 +132,8 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid private final String xmrNodesFromOptions; @Getter private final BooleanProperty useStandbyModeProperty = new SimpleBooleanProperty(prefPayload.isUseStandbyMode()); + @Getter + private final BooleanProperty useSoundForNotificationsProperty = new SimpleBooleanProperty(prefPayload.isUseSoundForNotifications()); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor @@ -162,6 +164,11 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid requestPersistence(); }); + useSoundForNotificationsProperty.addListener((ov) -> { + prefPayload.setUseSoundForNotifications(useSoundForNotificationsProperty.get()); + requestPersistence(); + }); + traditionalCurrenciesAsObservable.addListener((javafx.beans.Observable ov) -> { prefPayload.getTraditionalCurrencies().clear(); prefPayload.getTraditionalCurrencies().addAll(traditionalCurrenciesAsObservable); @@ -259,6 +266,7 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid // set all properties useAnimationsProperty.set(prefPayload.isUseAnimations()); useStandbyModeProperty.set(prefPayload.isUseStandbyMode()); + useSoundForNotificationsProperty.set(prefPayload.isUseSoundForNotifications()); cssThemeProperty.set(prefPayload.getCssTheme()); @@ -697,6 +705,10 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid this.useStandbyModeProperty.set(useStandbyMode); } + public void setUseSoundForNotifications(boolean useSoundForNotifications) { + this.useSoundForNotificationsProperty.set(useSoundForNotifications); + } + public void setTakeOfferSelectedPaymentAccountId(String value) { prefPayload.setTakeOfferSelectedPaymentAccountId(value); requestPersistence(); @@ -946,6 +958,8 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid void setUseStandbyMode(boolean useStandbyMode); + void setUseSoundForNotifications(boolean useSoundForNotifications); + void setTakeOfferSelectedPaymentAccountId(String value); void setIgnoreDustThreshold(int value); diff --git a/core/src/main/java/haveno/core/user/PreferencesPayload.java b/core/src/main/java/haveno/core/user/PreferencesPayload.java index b0a2fd7d..5484c514 100644 --- a/core/src/main/java/haveno/core/user/PreferencesPayload.java +++ b/core/src/main/java/haveno/core/user/PreferencesPayload.java @@ -108,6 +108,7 @@ public final class PreferencesPayload implements PersistableEnvelope { private boolean useMarketNotifications = true; private boolean usePriceNotifications = true; private boolean useStandbyMode = false; + private boolean useSoundForNotifications = true; @Nullable private String rpcUser; @Nullable @@ -185,6 +186,7 @@ public final class PreferencesPayload implements PersistableEnvelope { .setUseMarketNotifications(useMarketNotifications) .setUsePriceNotifications(usePriceNotifications) .setUseStandbyMode(useStandbyMode) + .setUseSoundForNotifications(useSoundForNotifications) .setBuyerSecurityDepositAsPercent(buyerSecurityDepositAsPercent) .setIgnoreDustThreshold(ignoreDustThreshold) .setClearDataAfterDays(clearDataAfterDays) @@ -280,6 +282,7 @@ public final class PreferencesPayload implements PersistableEnvelope { proto.getUseMarketNotifications(), proto.getUsePriceNotifications(), proto.getUseStandbyMode(), + proto.getUseSoundForNotifications(), proto.getRpcUser().isEmpty() ? null : proto.getRpcUser(), proto.getRpcPw().isEmpty() ? null : proto.getRpcPw(), proto.getTakeOfferSelectedPaymentAccountId().isEmpty() ? null : proto.getTakeOfferSelectedPaymentAccountId(), diff --git a/core/src/main/java/haveno/core/xmr/Balances.java b/core/src/main/java/haveno/core/xmr/Balances.java index 6958c828..fe49b941 100644 --- a/core/src/main/java/haveno/core/xmr/Balances.java +++ b/core/src/main/java/haveno/core/xmr/Balances.java @@ -111,6 +111,7 @@ public class Balances { public XmrBalanceInfo getBalances() { synchronized (this) { + if (availableBalance == null) return null; return new XmrBalanceInfo(availableBalance.longValue() + pendingBalance.longValue(), availableBalance.longValue(), pendingBalance.longValue(), @@ -127,6 +128,9 @@ public class Balances { synchronized (this) { synchronized (HavenoUtils.xmrWalletService.getWalletLock()) { + // get non-trade balance before + BigInteger balanceSumBefore = getNonTradeBalanceSum(); + // get wallet balances BigInteger balance = xmrWalletService.getWallet() == null ? BigInteger.ZERO : xmrWalletService.getBalance(); availableBalance = xmrWalletService.getWallet() == null ? BigInteger.ZERO : xmrWalletService.getAvailableBalance(); @@ -160,8 +164,25 @@ public class Balances { reservedBalance = reservedOfferBalance.add(reservedTradeBalance); // notify balance update - UserThread.execute(() -> updateCounter.set(updateCounter.get() + 1)); + UserThread.execute(() -> { + + // check if funds received + boolean fundsReceived = balanceSumBefore != null && getNonTradeBalanceSum().compareTo(balanceSumBefore) > 0; + if (fundsReceived) { + HavenoUtils.playCashRegisterSound(); + } + + // increase counter to notify listeners + updateCounter.set(updateCounter.get() + 1); + }); } } } + + private BigInteger getNonTradeBalanceSum() { + synchronized (this) { + if (availableBalance == null) return null; + return availableBalance.add(pendingBalance).add(reservedOfferBalance); + } + } } diff --git a/core/src/main/resources/cash_register.wav b/core/src/main/resources/cash_register.wav new file mode 100644 index 0000000000000000000000000000000000000000..c11d9146554d27b9c531217c07c801f10d575648 GIT binary patch literal 815986 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