diff --git a/core/src/main/java/bisq/core/alert/PrivateNotificationManager.java b/core/src/main/java/bisq/core/alert/PrivateNotificationManager.java index 9476e21a17..0451c6a68b 100644 --- a/core/src/main/java/bisq/core/alert/PrivateNotificationManager.java +++ b/core/src/main/java/bisq/core/alert/PrivateNotificationManager.java @@ -22,6 +22,11 @@ import bisq.network.p2p.NodeAddress; import bisq.network.p2p.P2PService; import bisq.network.p2p.SendMailboxMessageListener; import bisq.network.p2p.mailbox.MailboxMessageService; +import bisq.network.p2p.network.Connection; +import bisq.network.p2p.network.MessageListener; +import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.peers.keepalive.messages.Ping; +import bisq.network.p2p.peers.keepalive.messages.Pong; import bisq.common.app.DevEnv; import bisq.common.config.Config; @@ -36,6 +41,10 @@ import javax.inject.Inject; import javax.inject.Named; import com.google.common.base.Charsets; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; @@ -45,16 +54,20 @@ import java.security.SignatureException; import java.math.BigInteger; +import java.util.Random; import java.util.UUID; +import java.util.function.Consumer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.jetbrains.annotations.NotNull; + import javax.annotation.Nullable; import static org.bitcoinj.core.Utils.HEX; -public class PrivateNotificationManager { +public class PrivateNotificationManager implements MessageListener { private static final Logger log = LoggerFactory.getLogger(PrivateNotificationManager.class); private final P2PService p2PService; @@ -68,6 +81,8 @@ public class PrivateNotificationManager { private ECKey privateNotificationSigningKey; @Nullable private PrivateNotificationMessage privateNotificationMessage; + private final NetworkNode networkNode; + private Consumer pingResponseHandler = null; /////////////////////////////////////////////////////////////////////////////////////////// @@ -76,11 +91,13 @@ public class PrivateNotificationManager { @Inject public PrivateNotificationManager(P2PService p2PService, + NetworkNode networkNode, MailboxMessageService mailboxMessageService, KeyRing keyRing, @Named(Config.IGNORE_DEV_MSG) boolean ignoreDevMsg, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { this.p2PService = p2PService; + this.networkNode = networkNode; this.mailboxMessageService = mailboxMessageService; this.keyRing = keyRing; @@ -173,5 +190,38 @@ public class PrivateNotificationManager { } } + public void sendPing(NodeAddress peersNodeAddress, Consumer resultHandler) { + Ping ping = new Ping(new Random().nextInt(), 0); + log.info("Send Ping to peer {}, nonce={}", peersNodeAddress, ping.getNonce()); + SettableFuture future = networkNode.sendMessage(peersNodeAddress, ping); + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(Connection connection) { + connection.addMessageListener(PrivateNotificationManager.this); + pingResponseHandler = resultHandler; + } + @Override + public void onFailure(@NotNull Throwable throwable) { + String errorMessage = "Sending ping to " + peersNodeAddress.getHostNameForDisplay() + + " failed. That is expected if the peer is offline.\n\tping=" + ping + + ".\n\tException=" + throwable.getMessage(); + log.info(errorMessage); + resultHandler.accept(errorMessage); + } + }, MoreExecutors.directExecutor()); + } + + @Override + public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { + if (networkEnvelope instanceof Pong) { + Pong pong = (Pong) networkEnvelope; + String key = connection.getPeersNodeAddressOptional().get().getFullAddress(); + log.info("Received Pong! {} from {}", pong, key); + connection.removeMessageListener(this); + if (pingResponseHandler != null) { + pingResponseHandler.accept("SUCCESS"); + } + } + } } diff --git a/core/src/main/java/bisq/core/app/HavenoHeadlessApp.java b/core/src/main/java/bisq/core/app/HavenoHeadlessApp.java index 7257dd92f9..5b22c8760a 100644 --- a/core/src/main/java/bisq/core/app/HavenoHeadlessApp.java +++ b/core/src/main/java/bisq/core/app/HavenoHeadlessApp.java @@ -96,7 +96,7 @@ public class HavenoHeadlessApp implements HeadlessApp { bisqSetup.setQubesOSInfoHandler(() -> log.info("setQubesOSInfoHandler")); bisqSetup.setDownGradePreventionHandler(lastVersion -> log.info("Downgrade from version {} to version {} is not supported", lastVersion, Version.VERSION)); - + bisqSetup.setTorAddressUpgradeHandler(() -> log.info("setTorAddressUpgradeHandler")); corruptedStorageFileHandler.getFiles().ifPresent(files -> log.warn("getCorruptedDatabaseFiles. files={}", files)); tradeManager.setTakeOfferRequestErrorMessageHandler(errorMessage -> log.error("Error taking offer: " + errorMessage)); } diff --git a/core/src/main/java/bisq/core/app/HavenoSetup.java b/core/src/main/java/bisq/core/app/HavenoSetup.java index 478637f0fc..0904c10df1 100644 --- a/core/src/main/java/bisq/core/app/HavenoSetup.java +++ b/core/src/main/java/bisq/core/app/HavenoSetup.java @@ -22,6 +22,7 @@ import bisq.core.account.sign.SignedWitnessStorageService; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.alert.Alert; import bisq.core.alert.AlertManager; +import bisq.core.alert.PrivateNotificationManager; import bisq.core.alert.PrivateNotificationPayload; import bisq.core.api.CoreMoneroNodeService; import bisq.core.btc.model.AddressEntry; @@ -37,6 +38,10 @@ import bisq.core.payment.AmazonGiftCardAccount; import bisq.core.payment.PaymentAccount; import bisq.core.payment.RevolutAccount; import bisq.core.payment.payload.PaymentMethod; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.arbitration.ArbitrationManager; +import bisq.core.support.dispute.mediation.MediationManager; +import bisq.core.support.dispute.refund.RefundManager; import bisq.core.trade.TradeManager; import bisq.core.trade.TradeTxException; import bisq.core.user.Preferences; @@ -45,8 +50,10 @@ import bisq.core.util.FormattingUtils; import bisq.core.util.coin.CoinFormatter; import bisq.network.Socks5ProxyProvider; +import bisq.network.p2p.NodeAddress; import bisq.network.p2p.P2PService; import bisq.network.p2p.storage.payload.PersistableNetworkPayload; +import bisq.network.utils.Utils; import bisq.common.Timer; import bisq.common.UserThread; @@ -86,6 +93,8 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Objects; +import java.util.Random; import java.util.Scanner; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -106,19 +115,6 @@ public class HavenoSetup { private static final String VERSION_FILE_NAME = "version"; private static final String RESYNC_SPV_FILE_NAME = "resyncSpv"; - public interface HavenoSetupListener { - default void onInitP2pNetwork() { - } - - default void onInitWallet() { - } - - default void onRequestWalletPassword() { - } - - void onSetupComplete(); - } - private static final long STARTUP_TIMEOUT_MINUTES = 4; private final DomainInitialisation domainInitialisation; @@ -129,6 +125,7 @@ public class HavenoSetup { private final BtcWalletService btcWalletService; private final XmrWalletService xmrWalletService; private final P2PService p2PService; + private final PrivateNotificationManager privateNotificationManager; private final SignedWitnessStorageService signedWitnessStorageService; private final TradeManager tradeManager; private final OpenOfferManager openOfferManager; @@ -141,7 +138,9 @@ public class HavenoSetup { private final CoinFormatter formatter; private final LocalBitcoinNode localBitcoinNode; private final AppStartupState appStartupState; - + private final MediationManager mediationManager; + private final RefundManager refundManager; + private final ArbitrationManager arbitrationManager; @Setter @Nullable private Consumer displayTacHandler; @@ -188,8 +187,10 @@ public class HavenoSetup { private Runnable qubesOSInfoHandler; @Setter @Nullable + private Runnable torAddressUpgradeHandler; + @Setter + @Nullable private Consumer downGradePreventionHandler; - @Getter final BooleanProperty newVersionAvailableProperty = new SimpleBooleanProperty(false); private BooleanProperty p2pNetworkReady; @@ -199,6 +200,19 @@ public class HavenoSetup { private MonadicBinding p2pNetworkAndWalletInitialized; private final List havenoSetupListeners = new ArrayList<>(); + public interface HavenoSetupListener { + default void onInitP2pNetwork() { + } + + default void onInitWallet() { + } + + default void onRequestWalletPassword() { + } + + void onSetupComplete(); + } + @Inject public HavenoSetup(DomainInitialisation domainInitialisation, P2PNetworkSetup p2PNetworkSetup, @@ -208,6 +222,7 @@ public class HavenoSetup { XmrWalletService xmrWalletService, BtcWalletService btcWalletService, P2PService p2PService, + PrivateNotificationManager privateNotificationManager, SignedWitnessStorageService signedWitnessStorageService, TradeManager tradeManager, OpenOfferManager openOfferManager, @@ -220,7 +235,10 @@ public class HavenoSetup { @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, LocalBitcoinNode localBitcoinNode, AppStartupState appStartupState, - Socks5ProxyProvider socks5ProxyProvider) { + Socks5ProxyProvider socks5ProxyProvider, + MediationManager mediationManager, + RefundManager refundManager, + ArbitrationManager arbitrationManager) { this.domainInitialisation = domainInitialisation; this.p2PNetworkSetup = p2PNetworkSetup; this.walletAppSetup = walletAppSetup; @@ -229,6 +247,7 @@ public class HavenoSetup { this.xmrWalletService = xmrWalletService; this.btcWalletService = btcWalletService; this.p2PService = p2PService; + this.privateNotificationManager = privateNotificationManager; this.signedWitnessStorageService = signedWitnessStorageService; this.tradeManager = tradeManager; this.openOfferManager = openOfferManager; @@ -241,6 +260,9 @@ public class HavenoSetup { this.formatter = formatter; this.localBitcoinNode = localBitcoinNode; this.appStartupState = appStartupState; + this.mediationManager = mediationManager; + this.refundManager = refundManager; + this.arbitrationManager = arbitrationManager; MemPoolSpaceTxBroadcaster.init(socks5ProxyProvider, preferences, localBitcoinNode); } @@ -313,6 +335,8 @@ public class HavenoSetup { maybeShowSecurityRecommendation(); maybeShowLocalhostRunningInfo(); maybeShowAccountSigningStateInfo(); + maybeShowTorAddressUpgradeInformation(); + checkInboundConnections(); } @@ -663,6 +687,37 @@ public class HavenoSetup { } } + /** + * Check if we have inbound connections. If not, try to ping ourselves. + * If Haveno cannot connect to its own onion address through Tor, display + * an informative message to let the user know to configure their firewall else + * their offers will not be reachable. + * Repeat this test hourly. + */ + private void checkInboundConnections() { + NodeAddress onionAddress = p2PService.getNetworkNode().nodeAddressProperty().get(); + if (onionAddress == null || !onionAddress.getFullAddress().contains("onion")) { + return; + } + + if (p2PService.getNetworkNode().upTime() > TimeUnit.HOURS.toMillis(1) && + p2PService.getNetworkNode().getInboundConnectionCount() == 0) { + // we've been online a while and did not find any inbound connections; lets try the self-ping check + log.info("no recent inbound connections found, starting the self-ping test"); + privateNotificationManager.sendPing(onionAddress, stringResult -> { + log.info(stringResult); + if (stringResult.contains("failed")) { + getP2PNetworkStatusIconId().set("flashing:image-yellow_circle"); + } + }); + } + + // schedule another inbound connection check for later + int nextCheckInMinutes = 30 + new Random().nextInt(30); + log.debug("next inbound connections check in {} minutes", nextCheckInMinutes); + UserThread.runAfter(this::checkInboundConnections, nextCheckInMinutes, TimeUnit.MINUTES); + } + private void maybeShowSecurityRecommendation() { if (user.getPaymentAccountsAsObservable() == null) return; String key = "remindPasswordAndBackup"; @@ -733,6 +788,30 @@ public class HavenoSetup { } } + private void maybeShowTorAddressUpgradeInformation() { + if (Config.baseCurrencyNetwork().isTestnet() || + Utils.isV3Address(Objects.requireNonNull(p2PService.getNetworkNode().getNodeAddress()).getHostName())) { + return; + } + + maybeRunTorNodeAddressUpgradeHandler(); + + tradeManager.getNumPendingTrades().addListener((observable, oldValue, newValue) -> { + long numPendingTrades = (long) newValue; + if (numPendingTrades == 0) { + maybeRunTorNodeAddressUpgradeHandler(); + } + }); + } + + private void maybeRunTorNodeAddressUpgradeHandler() { + if (mediationManager.getDisputesAsObservableList().stream().allMatch(Dispute::isClosed) && + refundManager.getDisputesAsObservableList().stream().allMatch(Dispute::isClosed) && + arbitrationManager.getDisputesAsObservableList().stream().allMatch(Dispute::isClosed) && + tradeManager.getNumPendingTrades().isEqualTo(0).get()) { + Objects.requireNonNull(torAddressUpgradeHandler).run(); + } + } /////////////////////////////////////////////////////////////////////////////////////////// // Getters @@ -776,6 +855,10 @@ public class HavenoSetup { return p2PNetworkSetup.getP2PNetworkIconId(); } + public StringProperty getP2PNetworkStatusIconId() { + return p2PNetworkSetup.getP2PNetworkStatusIconId(); + } + public BooleanProperty getUpdatedDataReceived() { return p2PNetworkSetup.getUpdatedDataReceived(); } diff --git a/core/src/main/java/bisq/core/app/P2PNetworkSetup.java b/core/src/main/java/bisq/core/app/P2PNetworkSetup.java index b9c5d76063..2c0e9073e7 100644 --- a/core/src/main/java/bisq/core/app/P2PNetworkSetup.java +++ b/core/src/main/java/bisq/core/app/P2PNetworkSetup.java @@ -63,6 +63,8 @@ public class P2PNetworkSetup { @Getter final StringProperty p2PNetworkIconId = new SimpleStringProperty(); @Getter + final StringProperty p2PNetworkStatusIconId = new SimpleStringProperty(); + @Getter final BooleanProperty splashP2PNetworkAnimationVisible = new SimpleBooleanProperty(true); @Getter final StringProperty p2pNetworkLabelId = new SimpleStringProperty("footer-pane"); @@ -118,10 +120,12 @@ public class P2PNetworkSetup { p2PService.getNetworkNode().addConnectionListener(new ConnectionListener() { @Override public void onConnection(Connection connection) { + updateNetworkStatusIndicator(); } @Override public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) { + updateNetworkStatusIndicator(); // We only check at seed nodes as they are running the latest version // Other disconnects might be caused by peers running an older version if (connection.getConnectionState().isSeedNode() && @@ -225,4 +229,14 @@ public class P2PNetworkSetup { public void setSplashP2PNetworkAnimationVisible(boolean value) { splashP2PNetworkAnimationVisible.set(value); } + + private void updateNetworkStatusIndicator() { + if (p2PService.getNetworkNode().getInboundConnectionCount() > 0) { + p2PNetworkStatusIconId.set("image-green_circle"); + } else if (p2PService.getNetworkNode().getOutboundConnectionCount() > 0) { + p2PNetworkStatusIconId.set("image-yellow_circle"); + } else { + p2PNetworkStatusIconId.set("image-alert-round"); + } + } } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index fef9445bfe..d3f4bc1300 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -287,6 +287,7 @@ mainView.walletServiceErrorMsg.rejectedTxException=A transaction was rejected fr mainView.networkWarning.allConnectionsLost=You lost the connection to all {0} network peers.\nMaybe you lost your internet connection or your computer was in standby mode. mainView.networkWarning.localhostBitcoinLost=You lost the connection to the localhost Monero node.\nPlease restart the Haveno application to connect to other Monero nodes or restart the localhost Monero node. mainView.version.update=(Update available) +mainView.status.connections=Inbound connections: {0}\nOutbound connections: {1} #################################################################### @@ -2136,6 +2137,12 @@ popup.info.shutDownWithTradeInit={0}\n\ This trade has not finished initializing; shutting down now will probably make it corrupted. Please wait a minute and try again. popup.info.qubesOSSetupInfo=It appears you are running Bisq on Qubes OS. \n\n\ Please make sure your Bisq qube is setup according to our Setup Guide at [HYPERLINK:https://bisq.wiki/Running_Bisq_on_Qubes]. +popup.info.p2pStatusIndicator.red={0}\n\n\ + Your node has no connection to the P2P network. Haveno cannot operate in this state. +popup.info.p2pStatusIndicator.yellow={0}\n\n\ + Your node has no inbound Tor connections. Haveno will function ok, but if this state persists for several hours it may be an indication of connectivity problems. +popup.info.p2pStatusIndicator.green={0}\n\n\ + Good news, your P2P connection state looks healthy! popup.info.firewallSetupInfo=It appears this machine blocks incoming Tor connections. \ This can happen in VM environments such as Qubes/VirtualBox/Whonix. \n\n\ Please set up your environment to accept incoming Tor connections, otherwise no-one will be able to take your offers. @@ -2196,13 +2203,17 @@ popup.accountSigning.unsignedPubKeys.signed=Pubkeys were signed popup.accountSigning.unsignedPubKeys.result.signed=Signed pubkeys popup.accountSigning.unsignedPubKeys.result.failed=Failed to sign +popup.info.torMigration.msg=Your Haveno node is probably using a deprecated Tor v2 address. \ + Please switch your Haveno node to a Tor v3 address. \ + Make sure to back up your data directory beforehand. + #################################################################### # Notifications #################################################################### notification.trade.headline=Notification for trade with ID {0} notification.ticket.headline=Support ticket for trade with ID {0} -notification.trade.completed=The trade is now completed and you can withdraw your funds. +notification.trade.completed=The trade is now completed, and you can withdraw your funds. notification.trade.accepted=Your offer has been accepted by a XMR {0}. notification.trade.unlocked=Your trade has been confirmed.\nYou can start the payment now. notification.trade.paymentStarted=The XMR buyer has started the payment. @@ -2232,7 +2243,7 @@ systemTray.tooltip=Haveno: A decentralized bitcoin exchange network #################################################################### guiUtil.miningFeeInfo=Please be sure that the mining fee used by your external wallet is \ -at least {0} satoshis/vbyte. Otherwise the trade transactions may not be confirmed in time and the trade will end up in a dispute. +at least {0} satoshis/vbyte. Otherwise, the trade transactions may not be confirmed in time and the trade will end up in a dispute. guiUtil.accountExport.savedToPath=Trading accounts saved to path:\n{0} guiUtil.accountExport.noAccountSetup=You don't have trading accounts set up for exporting. @@ -2447,7 +2458,7 @@ payment.altcoin.address=Cryptocurrency address payment.altcoin.tradeInstantCheckbox=Trade instant (within 1 hour) with this Cryptocurrency payment.altcoin.tradeInstant.popup=For instant trading it is required that both trading peers are online to be able \ to complete the trade in less than 1 hour.\n\n\ - If you have offers open and you are not available please disable \ + If you have offers open, and you are not available please disable \ those offers under the 'Portfolio' screen. payment.altcoin=Cryptocurrency payment.select.altcoin=Select or search Cryptocurrency @@ -2563,7 +2574,7 @@ payment.westernUnion.info=When using Western Union the XMR buyer has to send the payment.halCash.info=When using HalCash the XMR buyer needs to send the XMR seller the HalCash code via a text message from their mobile phone.\n\n\ Please make sure to not exceed the maximum amount your bank allows you to send with HalCash. \ The min. amount per withdrawal is 10 EUR and the max. amount is 600 EUR. For repeated withdrawals it is \ - 3000 EUR per receiver per day and 6000 EUR per receiver per month. Please cross check those limits with your \ + 3000 EUR per receiver per day and 6000 EUR per receiver per month. Please cross-check those limits with your \ bank to be sure they use the same limits as stated here.\n\n\ The withdrawal amount must be a multiple of 10 EUR as you cannot withdraw other amounts from an ATM. The \ UI in the create-offer and take-offer screen will adjust the XMR amount so that the EUR amount is correct. You cannot use market \ @@ -2645,9 +2656,9 @@ Please be aware there is a maximum of Rs. 200,000 that can be sent per transacti Some banks have different limits for their customers. payment.imps.info.buyer=Please send payment only to the account details provided in Bisq.\n\n\ The maximum trade size is Rs. 200,000 per transaction.\n\n\ -If your trade is over Rs. 200,000 you will have to make multiple transfers. However be aware their is a maximum limit of Rs. 1,000,000 that can be sent per day.\n\n\ +If your trade is over Rs. 200,000 you will have to make multiple transfers. However, be aware there is a maximum limit of Rs. 1,000,000 that can be sent per day.\n\n\ Please note some banks have different limits for their customers. -payment.imps.info.seller=If you intend to receive over Rs. 200,000 per trade you should expect the buyer to have to make multiple transfers. However be aware there is a maximum limit of Rs. 1,000,000 that can be sent per day.\n\n\ +payment.imps.info.seller=If you intend to receive over Rs. 200,000 per trade you should expect the buyer to have to make multiple transfers. However, be aware there is a maximum limit of Rs. 1,000,000 that can be sent per day.\n\n\ Please note some banks have different limits for their customers. payment.neft.info.account=Please make sure to include your:\n\n\ @@ -2865,7 +2876,7 @@ payment.strike.info.seller=Please make sure your payment is received from the BT The maximum trade size is $1,000 per payment.\n\n\ If you trade over the above limits your trade might be cancelled and there could be a penalty. -payment.transferwiseUsd.info.account=Due US banking regulation, sending and receiving USD payments has more restrictions \ +payment.transferwiseUsd.info.account=Due to US banking regulation, sending and receiving USD payments has more restrictions \ than most other currencies. For this reason USD was not added to Bisq TransferWise payment method.\n\n\ The TransferWise-USD payment method allows Bisq users to trade in USD.\n\n\ Anyone with a Wise, formally TransferWise account, can add TransferWise-USD as a payment method in Bisq. This will \ @@ -3246,7 +3257,7 @@ validation.iban.checkSumInvalid=IBAN checksum is invalid validation.iban.invalidLength=Number must have a length of 15 to 34 chars. validation.iban.sepaNotSupported=SEPA is not supported in this country validation.interacETransfer.invalidAreaCode=Non-Canadian area code -validation.interacETransfer.invalidPhone=Please enter a valid 11 digit phone number (ex: 1-123-456-7890) or an email address +validation.interacETransfer.invalidPhone=Please enter a valid 11-digit phone number (ex: 1-123-456-7890) or an email address validation.interacETransfer.invalidQuestion=Must contain only letters, numbers, spaces and/or the symbols ' _ , . ? - validation.interacETransfer.invalidAnswer=Must be one word and contain only letters, numbers, and/or the symbol - validation.inputTooLarge=Input must not be larger than {0} @@ -3263,7 +3274,7 @@ validation.mustBeDifferent=Your input must be different from the current value validation.cannotBeChanged=Parameter cannot be changed validation.numberFormatException=Number format exception {0} validation.mustNotBeNegative=Input must not be negative -validation.phone.missingCountryCode=Need two letter country code to validate phone number +validation.phone.missingCountryCode=Need two-letter country code to validate phone number validation.phone.invalidCharacters=Phone number {0} contains invalid characters validation.phone.insufficientDigits=There are not enough digits in {0} to be a valid phone number validation.phone.tooManyDigits=There are too many digits in {0} to be a valid phone number diff --git a/desktop/src/main/java/bisq/desktop/images.css b/desktop/src/main/java/bisq/desktop/images.css index b170875e79..65300f0850 100644 --- a/desktop/src/main/java/bisq/desktop/images.css +++ b/desktop/src/main/java/bisq/desktop/images.css @@ -21,6 +21,10 @@ -fx-image: url("../../images/green_circle.png"); } +#image-yellow_circle { + -fx-image: url("../../images/yellow_circle.png"); +} + #image-blue_circle { -fx-image: url("../../images/blue_circle.png"); } diff --git a/desktop/src/main/java/bisq/desktop/main/MainView.java b/desktop/src/main/java/bisq/desktop/main/MainView.java index fcebef4708..0ae4f458a2 100644 --- a/desktop/src/main/java/bisq/desktop/main/MainView.java +++ b/desktop/src/main/java/bisq/desktop/main/MainView.java @@ -34,6 +34,7 @@ import bisq.desktop.main.market.offerbook.OfferBookChartView; import bisq.desktop.main.offer.BuyOfferView; import bisq.desktop.main.offer.SellOfferView; import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.main.overlays.windows.TorNetworkSettingsWindow; import bisq.desktop.main.portfolio.PortfolioView; import bisq.desktop.main.settings.SettingsView; import bisq.desktop.main.shared.PriceFeedComboBoxItem; @@ -58,6 +59,10 @@ import com.jfoenix.controls.JFXBadge; import com.jfoenix.controls.JFXComboBox; import com.jfoenix.controls.JFXProgressBar; +import javafx.animation.Animation; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; + import javafx.scene.control.Button; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; @@ -91,6 +96,8 @@ import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; +import javafx.util.Duration; + import java.text.DecimalFormat; import java.text.NumberFormat; @@ -114,6 +121,22 @@ public class MainView extends InitializableView { private final static int SHOW_TOR_SETTINGS_DELAY_SEC = 90; @Setter private Runnable onApplicationStartedHandler; + private static Transitions transitions; + private static StackPane rootContainer; + private final ViewLoader viewLoader; + private final Navigation navigation; + private final ToggleGroup navButtons = new ToggleGroup(); + private ChangeListener walletServiceErrorMsgListener; + private ChangeListener btcSyncIconIdListener; + private ChangeListener splashP2PNetworkErrorMsgListener; + private ChangeListener splashP2PNetworkIconIdListener; + private ChangeListener splashP2PNetworkVisibleListener; + private BusyAnimation splashP2PNetworkBusyAnimation; + private Label splashP2PNetworkLabel; + private ProgressBar btcSyncIndicator, p2pNetworkProgressBar; + private Label btcSplashInfo; + private Popup p2PNetworkWarnMsgPopup, btcNetworkWarnMsgPopup; + private final TorNetworkSettingsWindow torNetworkSettingsWindow; public static StackPane getRootContainer() { return MainView.rootContainer; @@ -135,34 +158,17 @@ public class MainView extends InitializableView { transitions.removeEffect(MainView.rootContainer); } - private static Transitions transitions; - private static StackPane rootContainer; - - - private final ViewLoader viewLoader; - private final Navigation navigation; - - private final ToggleGroup navButtons = new ToggleGroup(); - private ChangeListener walletServiceErrorMsgListener; - private ChangeListener btcSyncIconIdListener; - private ChangeListener splashP2PNetworkErrorMsgListener; - private ChangeListener splashP2PNetworkIconIdListener; - private ChangeListener splashP2PNetworkVisibleListener; - private BusyAnimation splashP2PNetworkBusyAnimation; - private Label splashP2PNetworkLabel; - private ProgressBar btcSyncIndicator, p2pNetworkProgressBar; - private Label btcSplashInfo; - private Popup p2PNetworkWarnMsgPopup, btcNetworkWarnMsgPopup; - @Inject public MainView(MainViewModel model, CachingViewLoader viewLoader, Navigation navigation, - Transitions transitions) { + Transitions transitions, + TorNetworkSettingsWindow torNetworkSettingsWindow) { super(model); this.viewLoader = viewLoader; this.navigation = navigation; MainView.transitions = transitions; + this.torNetworkSettingsWindow = torNetworkSettingsWindow; } @Override @@ -596,6 +602,9 @@ public class MainView extends InitializableView { splashP2PNetworkIcon.setVisible(false); splashP2PNetworkIcon.setManaged(false); HBox.setMargin(splashP2PNetworkIcon, new Insets(0, 0, 5, 0)); + splashP2PNetworkIcon.setOnMouseClicked(e -> { + torNetworkSettingsWindow.show(); + }); Timer showTorNetworkSettingsTimer = UserThread.runAfter(() -> { showTorNetworkSettingsButton.setVisible(true); @@ -737,6 +746,40 @@ public class MainView extends InitializableView { p2PNetworkWarnMsgPopup.hide(); } }); + p2PNetworkIcon.setOnMouseClicked(e -> { + torNetworkSettingsWindow.show(); + }); + + ImageView p2PNetworkStatusIcon = new ImageView(); + setRightAnchor(p2PNetworkStatusIcon, 30d); + setBottomAnchor(p2PNetworkStatusIcon, 7d); + Tooltip p2pNetworkStatusToolTip = new Tooltip(); + Tooltip.install(p2PNetworkStatusIcon, p2pNetworkStatusToolTip); + p2PNetworkStatusIcon.setOnMouseEntered(e -> p2pNetworkStatusToolTip.setText(model.getP2pConnectionSummary())); + Timeline flasher = new Timeline( + new KeyFrame(Duration.seconds(0.5), e -> p2PNetworkStatusIcon.setOpacity(0.2)), + new KeyFrame(Duration.seconds(1.0), e -> p2PNetworkStatusIcon.setOpacity(1)) + ); + flasher.setCycleCount(Animation.INDEFINITE); + model.getP2PNetworkStatusIconId().addListener((ov, oldValue, newValue) -> { + if (newValue.equalsIgnoreCase("flashing:image-yellow_circle")) { + p2PNetworkStatusIcon.setId("image-yellow_circle"); + flasher.play(); + } else { + p2PNetworkStatusIcon.setId(newValue); + flasher.stop(); + p2PNetworkStatusIcon.setOpacity(1); + } + }); + p2PNetworkStatusIcon.setOnMouseClicked(e -> { + if (p2PNetworkStatusIcon.getId().equalsIgnoreCase("image-alert-round")) { + new Popup().warning(Res.get("popup.info.p2pStatusIndicator.red", model.getP2pConnectionSummary())).show(); + } else if (p2PNetworkStatusIcon.getId().equalsIgnoreCase("image-yellow_circle")) { + new Popup().information(Res.get("popup.info.p2pStatusIndicator.yellow", model.getP2pConnectionSummary())).show(); + } else { + new Popup().information(Res.get("popup.info.p2pStatusIndicator.green", model.getP2pConnectionSummary())).show(); + } + }); model.getUpdatedDataReceived().addListener((observable, oldValue, newValue) -> { UserThread.execute(() -> { @@ -752,10 +795,10 @@ public class MainView extends InitializableView { VBox vBox = new VBox(); vBox.setAlignment(Pos.CENTER_RIGHT); vBox.getChildren().addAll(p2PNetworkLabel, p2pNetworkProgressBar); - setRightAnchor(vBox, 33d); + setRightAnchor(vBox, 53d); setBottomAnchor(vBox, 5d); - return new AnchorPane(separator, btcInfoLabel, versionBox, vBox, p2PNetworkIcon) {{ + return new AnchorPane(separator, btcInfoLabel, versionBox, vBox, p2PNetworkStatusIcon, p2PNetworkIcon) {{ setId("footer-pane"); setMinHeight(30); setMaxHeight(30); diff --git a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java index b1ba52aa4f..e30a1e1733 100644 --- a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java @@ -17,9 +17,12 @@ package bisq.desktop.main; +import bisq.desktop.Navigation; import bisq.desktop.app.HavenoApp; import bisq.desktop.common.model.ViewModel; import bisq.desktop.components.TxIdTextField; +import bisq.desktop.main.account.AccountView; +import bisq.desktop.main.account.content.backup.BackupView; import bisq.desktop.main.overlays.Overlay; import bisq.desktop.main.overlays.notifications.NotificationCenter; import bisq.desktop.main.overlays.popups.Popup; @@ -133,6 +136,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener @Getter private final TorNetworkSettingsWindow torNetworkSettingsWindow; private final CorruptedStorageFileHandler corruptedStorageFileHandler; + private final Navigation navigation; @Getter private final BooleanProperty showAppScreen = new SimpleBooleanProperty(); @@ -175,7 +179,8 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener LocalBitcoinNode localBitcoinNode, AccountAgeWitnessService accountAgeWitnessService, TorNetworkSettingsWindow torNetworkSettingsWindow, - CorruptedStorageFileHandler corruptedStorageFileHandler) { + CorruptedStorageFileHandler corruptedStorageFileHandler, + Navigation navigation) { this.bisqSetup = bisqSetup; this.connectionService = connectionService; this.user = user; @@ -199,6 +204,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener this.accountAgeWitnessService = accountAgeWitnessService; this.torNetworkSettingsWindow = torNetworkSettingsWindow; this.corruptedStorageFileHandler = corruptedStorageFileHandler; + this.navigation = navigation; TxIdTextField.setPreferences(preferences); @@ -411,6 +417,12 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener .show(); }); + bisqSetup.setTorAddressUpgradeHandler(() -> new Popup().information(Res.get("popup.info.torMigration.msg")) + .actionButtonTextWithGoTo("navigation.account.backup") + .onAction(() -> { + navigation.setReturnPath(navigation.getCurrentPath()); + navigation.navigateTo(MainView.class, AccountView.class, BackupView.class); + }).show()); corruptedStorageFileHandler.getFiles().ifPresent(files -> new Popup() .warning(Res.get("popup.warning.incompatibleDB", files.toString(), config.appDataDir)) @@ -704,6 +716,10 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener return bisqSetup.getP2PNetworkIconId(); } + StringProperty getP2PNetworkStatusIconId() { + return bisqSetup.getP2PNetworkStatusIconId(); + } + BooleanProperty getUpdatedDataReceived() { return bisqSetup.getUpdatedDataReceived(); } @@ -767,4 +783,10 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener overlay.show(); } } + + public String getP2pConnectionSummary() { + return Res.get("mainView.status.connections", + p2PService.getNetworkNode().getInboundConnectionCount(), + p2PService.getNetworkNode().getOutboundConnectionCount()); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/TorNetworkSettingsWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/TorNetworkSettingsWindow.java index 6cb403677b..777c95f472 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/TorNetworkSettingsWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/TorNetworkSettingsWindow.java @@ -123,7 +123,7 @@ public class TorNetworkSettingsWindow extends Overlay headLine = Res.get("torNetworkSettingWindow.header"); width = 1068; - + rowIndex = 0; createGridPane(); gridPane.getColumnConstraints().get(0).setHalignment(HPos.LEFT); diff --git a/desktop/src/main/resources/images/yellow_circle.png b/desktop/src/main/resources/images/yellow_circle.png new file mode 100644 index 0000000000..44e5a272fa Binary files /dev/null and b/desktop/src/main/resources/images/yellow_circle.png differ diff --git a/p2p/src/main/java/bisq/network/p2p/NodeAddress.java b/p2p/src/main/java/bisq/network/p2p/NodeAddress.java index f6420eaa34..5e0f776132 100644 --- a/p2p/src/main/java/bisq/network/p2p/NodeAddress.java +++ b/p2p/src/main/java/bisq/network/p2p/NodeAddress.java @@ -79,6 +79,17 @@ public final class NodeAddress implements PersistablePayload, NetworkPayload, Us return hostName.replace(".onion", ""); } + // tor v3 onions are too long to display for example in a table grid, so this convenience method + // produces a display-friendly format which includes [first 7]..[last 7] characters. + // tor v2 and localhost will be displayed in full, as they are 16 chars or fewer. + public String getHostNameForDisplay() { + String work = getHostNameWithoutPostFix(); + if (work.length() > 16) { + return work.substring(0, 7) + ".." + work.substring(work.length() - 7); + } + return work; + } + // We use just a few chars from the full address to blur the potential receiver for sent network_messages public byte[] getAddressPrefixHash() { if (addressPrefixHash == null) diff --git a/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java b/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java index 83c3d24c0b..062f45c0e0 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java +++ b/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java @@ -39,12 +39,12 @@ import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.SimpleObjectProperty; -import java.net.ConnectException; import java.net.ServerSocket; import java.net.Socket; import java.io.IOException; +import java.util.Date; import java.util.HashSet; import java.util.Optional; import java.util.Set; @@ -127,10 +127,10 @@ public abstract class NetworkNode implements MessageListener { Thread.currentThread().setName("NetworkNode:SendMessage-to-" + peersNodeAddress.getFullAddress()); if (peersNodeAddress.equals(getNodeAddress())) { - throw new ConnectException("We do not send a message to ourselves"); + log.warn("We are sending a message to ourselves"); } - OutboundConnection outboundConnection = null; + OutboundConnection outboundConnection; try { // can take a while when using tor long startTs = System.currentTimeMillis(); @@ -145,7 +145,7 @@ public abstract class NetworkNode implements MessageListener { if (duration > CREATE_SOCKET_TIMEOUT) throw new TimeoutException("A timeout occurred when creating a socket."); - // Tor needs sometimes quite long to create a connection. To avoid that we get too many double + // Tor needs sometimes quite long to create a connection. To avoid that we get too many double- // sided connections we check again if we still don't have any connection for that node address. Connection existingConnection = getInboundConnection(peersNodeAddress); if (existingConnection == null) @@ -212,9 +212,7 @@ public abstract class NetworkNode implements MessageListener { return outboundConnection; } } catch (Throwable throwable) { - if (!(throwable instanceof ConnectException || - throwable instanceof IOException || - throwable instanceof TimeoutException)) { + if (!(throwable instanceof IOException || throwable instanceof TimeoutException)) { log.warn("Executing task failed. " + throwable.getMessage()); } throw throwable; @@ -389,7 +387,7 @@ public abstract class NetworkNode implements MessageListener { @Override public void onMessage(NetworkEnvelope networkEnvelope, Connection connection) { - messageListeners.stream().forEach(e -> e.onMessage(networkEnvelope, connection)); + messageListeners.forEach(e -> e.onMessage(networkEnvelope, connection)); } @@ -441,7 +439,7 @@ public abstract class NetworkNode implements MessageListener { if (!connection.isStopped()) { inBoundConnections.add((InboundConnection) connection); printInboundConnections(); - connectionListeners.stream().forEach(e -> e.onConnection(connection)); + connectionListeners.forEach(e -> e.onConnection(connection)); } } @@ -451,13 +449,13 @@ public abstract class NetworkNode implements MessageListener { //noinspection SuspiciousMethodCalls inBoundConnections.remove(connection); printInboundConnections(); - connectionListeners.stream().forEach(e -> e.onDisconnect(closeConnectionReason, connection)); + connectionListeners.forEach(e -> e.onDisconnect(closeConnectionReason, connection)); } @Override public void onError(Throwable throwable) { log.error("server.ConnectionListener.onError " + throwable.getMessage()); - connectionListeners.stream().forEach(e -> e.onError(throwable)); + connectionListeners.forEach(e -> e.onError(throwable)); } }; server = new Server(serverSocket, @@ -479,7 +477,7 @@ public abstract class NetworkNode implements MessageListener { private void printOutBoundConnections() { StringBuilder sb = new StringBuilder("outBoundConnections size()=") .append(outBoundConnections.size()).append("\n\toutBoundConnections="); - outBoundConnections.stream().forEach(e -> sb.append(e).append("\n\t")); + outBoundConnections.forEach(e -> sb.append(e).append("\n\t")); log.debug(sb.toString()); } @@ -494,7 +492,7 @@ public abstract class NetworkNode implements MessageListener { private void printInboundConnections() { StringBuilder sb = new StringBuilder("inBoundConnections size()=") .append(inBoundConnections.size()).append("\n\tinBoundConnections="); - inBoundConnections.stream().forEach(e -> sb.append(e).append("\n\t")); + inBoundConnections.forEach(e -> sb.append(e).append("\n\t")); log.debug(sb.toString()); } @@ -512,4 +510,22 @@ public abstract class NetworkNode implements MessageListener { .map(Connection::getCapabilities) .findAny(); } + + public long upTime() { + // how long Haveno has been running with at least one connection + // uptime is relative to last all connections lost event + long earliestConnection = new Date().getTime(); + for (Connection connection : outBoundConnections) { + earliestConnection = Math.min(earliestConnection, connection.getStatistic().getCreationDate().getTime()); + } + return new Date().getTime() - earliestConnection; + } + + public int getInboundConnectionCount() { + return inBoundConnections.size(); + } + + public int getOutboundConnectionCount() { + return outBoundConnections.size(); + } } diff --git a/p2p/src/test/java/bisq/network/utils/UtilsTest.java b/p2p/src/test/java/bisq/network/utils/UtilsTest.java new file mode 100644 index 0000000000..255eba7751 --- /dev/null +++ b/p2p/src/test/java/bisq/network/utils/UtilsTest.java @@ -0,0 +1,36 @@ +/* + * This file is part of Haveno. + * + * Haveno is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Haveno is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Haveno. If not, see . + */ + +package bisq.network.utils; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class UtilsTest { + + @Test + public void checkV2Address() { + assertFalse(Utils.isV3Address("xmh57jrzrnw6insl.onion")); + } + + @Test + public void checkV3Address() { + assertTrue(Utils.isV3Address("vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd.onion")); + } +}