P2P status indicator with update prompt

Co-authored-by: jmacxx <47253594+jmacxx@users.noreply.github.com>
Co-authored-by: Christoph Atteneder <christoph.atteneder@gmail.com>
This commit is contained in:
napoly 2022-11-20 21:01:42 +01:00 committed by woodser
parent a2929035bc
commit bd70b935e4
13 changed files with 354 additions and 64 deletions

View file

@ -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<String> 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<String> resultHandler) {
Ping ping = new Ping(new Random().nextInt(), 0);
log.info("Send Ping to peer {}, nonce={}", peersNodeAddress, ping.getNonce());
SettableFuture<Connection> 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");
}
}
}
}

View file

@ -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));
}

View file

@ -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<Runnable> displayTacHandler;
@ -188,8 +187,10 @@ public class HavenoSetup {
private Runnable qubesOSInfoHandler;
@Setter
@Nullable
private Runnable torAddressUpgradeHandler;
@Setter
@Nullable
private Consumer<String> downGradePreventionHandler;
@Getter
final BooleanProperty newVersionAvailableProperty = new SimpleBooleanProperty(false);
private BooleanProperty p2pNetworkReady;
@ -199,6 +200,19 @@ public class HavenoSetup {
private MonadicBinding<Boolean> p2pNetworkAndWalletInitialized;
private final List<HavenoSetupListener> 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();
}

View file

@ -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");
}
}
}

View file

@ -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

View file

@ -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");
}

View file

@ -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<StackPane, MainViewModel> {
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<String> walletServiceErrorMsgListener;
private ChangeListener<String> btcSyncIconIdListener;
private ChangeListener<String> splashP2PNetworkErrorMsgListener;
private ChangeListener<String> splashP2PNetworkIconIdListener;
private ChangeListener<Boolean> 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<StackPane, MainViewModel> {
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<String> walletServiceErrorMsgListener;
private ChangeListener<String> btcSyncIconIdListener;
private ChangeListener<String> splashP2PNetworkErrorMsgListener;
private ChangeListener<String> splashP2PNetworkIconIdListener;
private ChangeListener<Boolean> 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<StackPane, MainViewModel> {
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<StackPane, MainViewModel> {
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<StackPane, MainViewModel> {
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);

View file

@ -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());
}
}

View file

@ -123,7 +123,7 @@ public class TorNetworkSettingsWindow extends Overlay<TorNetworkSettingsWindow>
headLine = Res.get("torNetworkSettingWindow.header");
width = 1068;
rowIndex = 0;
createGridPane();
gridPane.getColumnConstraints().get(0).setHalignment(HPos.LEFT);

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

View file

@ -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)

View file

@ -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();
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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"));
}
}