From a4e43f104518971a1fad524908f93068a6891e52 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 16 Sep 2024 20:04:57 -0400 Subject: [PATCH 01/17] 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 83d42efbbe..22657efd6f 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 dc626c93e1..5bc11c0134 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 02/17] 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 1922a519eb..58dc95e1df 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 03/17] 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 58dc95e1df..d02870c990 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 04/17] 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 78fafed62b..62aacb4f41 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 05/17] 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 62aacb4f41..71b53c5dc5 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 06/17] 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 ca3df85358..b9910627f7 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 07/17] 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 0000000000..0345bd1ea4 --- /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 0000000000..247354ff87 --- /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 08/17] 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 4da4b92061..ad383ff1ef 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 22657efd6f..bcd7d082b0 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 d02870c990..67ca2ab9ed 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 449faea64b..27058f3025 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 f606d3b534..0c929b3ae4 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 e49b4ccd91..087ffce702 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 b4afe417e8..240ea49ee9 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 7edbed9b01..f193287edc 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 16fa22a8db..431ab9a652 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 4465fd2a35..2541fb9fc5 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 e213bdd688..aa25b12dc6 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 0d1ee2b996..2c2e95fd3b 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 d878464af2..c28e509ecc 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 8086d563d1..725ccd877c 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 aa561f127d..4824579dbf 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 30d140f83e..18838cee38 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 871151a9e1..17bef33abb 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 192516b59f..bd124fda1f 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 a2f2f2f281..5fbd64dba5 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 8082aad475..719ac46284 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 a43c667d22..c11df74fae 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 231e54824a..293b74f99d 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 0cbc41b234..877eb387a2 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 55b09682bd..1a9ead3597 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 7a906ec7d9..997a724224 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 eeee387774..cef43e795c 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 396d6026a1..236f8471e9 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 8bb3e1d418..f9c498f08e 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 a8028d0e2e..117ed4a494 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 a9d0494908..451d3e7e7f 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 131b537217..c447b3fccf 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 92eddb0614..1a7f1b84df 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 9254c9af20..26005988e2 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 9cf39f570d..437e3d2f88 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 a9c0f6ad16..40be21ef32 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 6d9f3df16a..17c940add8 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 5659ab2e0b..455f180934 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 09/17] resize donation qrs --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 465e04f912..7f4d1eb2eb 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 10/17] 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 c9ef1d6cfe..f0baf8c922 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 8cd53a17e3..7c5da97808 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 f5cca952d6..44fdab6bc0 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 11/17] 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 42a08d2b41..3a0d85c125 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 12/17] 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 3a0d85c125..3e961db094 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 13/17] 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 d76154485d..10cbfdafaf 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 bd124fda1f..eb49e0c509 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 719ac46284..50be387c76 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 14/17] 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 d6b937ac4b..95fa4bbd63 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 b72b89fe28..909aa945dc 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 f2ad96c2e0..2c314ea5d8 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 15/17] 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 25f98c3470..9afc4ee2b9 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 2541fb9fc5..ffeacbfd38 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 80682e9a34..cd5ed266f1 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 0db9868f04..9802f036c3 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 03cc6b2305..3daf8d370a 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 16/17] 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 6c40d7498b..484208d06d 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 2a9e23cd42..76e661aea6 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 2c2e95fd3b..d80ca807cc 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 deb1408623..3032492b89 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 d3919919f3..5f51e8eadd 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 9959a73607..55acf63d87 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 311bd3108b..b57b5708ee 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 b0a2fd7d4a..5484c514ea 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 6958c8288c..fe49b941fd 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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