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