Compare commits

..

15 Commits

31 changed files with 370 additions and 205 deletions

View File

@ -485,6 +485,31 @@ arbitrator-desktop-mainnet:
--xmrNode=http://127.0.0.1:18081 \ --xmrNode=http://127.0.0.1:18081 \
--useNativeXmrWallet=false \ --useNativeXmrWallet=false \
arbitrator2-daemon-mainnet:
./haveno-daemon$(APP_EXT) \
--baseCurrencyNetwork=XMR_MAINNET \
--useLocalhostForP2P=false \
--useDevPrivilegeKeys=false \
--nodePort=9999 \
--appName=haveno-XMR_MAINNET_arbitrator2 \
--apiPassword=apitest \
--apiPort=1205 \
--passwordRequired=false \
--xmrNode=http://127.0.0.1:18081 \
--useNativeXmrWallet=false \
arbitrator2-desktop-mainnet:
./haveno-desktop$(APP_EXT) \
--baseCurrencyNetwork=XMR_MAINNET \
--useLocalhostForP2P=false \
--useDevPrivilegeKeys=false \
--nodePort=9999 \
--appName=haveno-XMR_MAINNET_arbitrator2 \
--apiPassword=apitest \
--apiPort=1205 \
--xmrNode=http://127.0.0.1:18081 \
--useNativeXmrWallet=false \
haveno-daemon-mainnet: haveno-daemon-mainnet:
./haveno-daemon$(APP_EXT) \ ./haveno-daemon$(APP_EXT) \
--baseCurrencyNetwork=XMR_MAINNET \ --baseCurrencyNetwork=XMR_MAINNET \

View File

@ -610,7 +610,7 @@ configure(project(':desktop')) {
apply plugin: 'com.github.johnrengelman.shadow' apply plugin: 'com.github.johnrengelman.shadow'
apply from: 'package/package.gradle' apply from: 'package/package.gradle'
version = '1.0.19-SNAPSHOT' version = '1.1.0-SNAPSHOT'
jar.manifest.attributes( jar.manifest.attributes(
"Implementation-Title": project.name, "Implementation-Title": project.name,

View File

@ -28,7 +28,7 @@ import static com.google.common.base.Preconditions.checkArgument;
public class Version { public class Version {
// The application versions // The application versions
// We use semantic versioning with major, minor and patch // We use semantic versioning with major, minor and patch
public static final String VERSION = "1.0.19"; public static final String VERSION = "1.1.0";
/** /**
* Holds a list of the tagged resource files for optimizing the getData requests. * Holds a list of the tagged resource files for optimizing the getData requests.

View File

@ -335,12 +335,13 @@ public class SignedWitnessService {
String message = Utilities.encodeToHex(signedWitness.getAccountAgeWitnessHash()); String message = Utilities.encodeToHex(signedWitness.getAccountAgeWitnessHash());
String signatureBase64 = new String(signedWitness.getSignature(), Charsets.UTF_8); String signatureBase64 = new String(signedWitness.getSignature(), Charsets.UTF_8);
ECKey key = ECKey.fromPublicOnly(signedWitness.getSignerPubKey()); ECKey key = ECKey.fromPublicOnly(signedWitness.getSignerPubKey());
if (arbitratorManager.isPublicKeyInList(Utilities.encodeToHex(key.getPubKey()))) { String pubKeyHex = Utilities.encodeToHex(key.getPubKey());
if (arbitratorManager.isPublicKeyInList(pubKeyHex)) {
key.verifyMessage(message, signatureBase64); key.verifyMessage(message, signatureBase64);
verifySignatureWithECKeyResultCache.put(hash, true); verifySignatureWithECKeyResultCache.put(hash, true);
return true; return true;
} else { } else {
log.warn("Provided EC key is not in list of valid arbitrators."); log.warn("Provided EC key is not in list of valid arbitrators: " + pubKeyHex);
verifySignatureWithECKeyResultCache.put(hash, false); verifySignatureWithECKeyResultCache.put(hash, false);
return false; return false;
} }

View File

@ -75,9 +75,10 @@ public final class XmrConnectionService {
private static final long KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL = 20000; // 20 seconds private static final long KEY_IMAGE_REFRESH_PERIOD_MS_LOCAL = 20000; // 20 seconds
private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes private static final long KEY_IMAGE_REFRESH_PERIOD_MS_REMOTE = 300000; // 5 minutes
public enum XmrConnectionError { public enum XmrConnectionFallbackType {
LOCAL, LOCAL,
CUSTOM CUSTOM,
PROVIDED
} }
private final Object lock = new Object(); private final Object lock = new Object();
@ -97,7 +98,7 @@ public final class XmrConnectionService {
private final LongProperty chainHeight = new SimpleLongProperty(0); private final LongProperty chainHeight = new SimpleLongProperty(0);
private final DownloadListener downloadListener = new DownloadListener(); private final DownloadListener downloadListener = new DownloadListener();
@Getter @Getter
private final ObjectProperty<XmrConnectionError> connectionServiceError = new SimpleObjectProperty<>(); private final ObjectProperty<XmrConnectionFallbackType> connectionServiceFallbackType = new SimpleObjectProperty<>();
@Getter @Getter
private final StringProperty connectionServiceErrorMsg = new SimpleStringProperty(); private final StringProperty connectionServiceErrorMsg = new SimpleStringProperty();
private final LongProperty numUpdates = new SimpleLongProperty(0); private final LongProperty numUpdates = new SimpleLongProperty(0);
@ -129,6 +130,7 @@ public final class XmrConnectionService {
private Set<MoneroRpcConnection> excludedConnections = new HashSet<>(); private Set<MoneroRpcConnection> excludedConnections = new HashSet<>();
private static final long FALLBACK_INVOCATION_PERIOD_MS = 1000 * 30 * 1; // offer to fallback up to once every 30s private static final long FALLBACK_INVOCATION_PERIOD_MS = 1000 * 30 * 1; // offer to fallback up to once every 30s
private boolean fallbackApplied; private boolean fallbackApplied;
private boolean usedSyncingLocalNodeBeforeStartup;
@Inject @Inject
public XmrConnectionService(P2PService p2PService, public XmrConnectionService(P2PService p2PService,
@ -156,7 +158,13 @@ public final class XmrConnectionService {
p2PService.addP2PServiceListener(new P2PServiceListener() { p2PService.addP2PServiceListener(new P2PServiceListener() {
@Override @Override
public void onTorNodeReady() { public void onTorNodeReady() {
ThreadUtils.submitToPool(() -> initialize()); ThreadUtils.submitToPool(() -> {
try {
initialize();
} catch (Exception e) {
log.warn("Error initializing connection service, error={}\n", e.getMessage(), e);
}
});
} }
@Override @Override
public void onHiddenServicePublished() {} public void onHiddenServicePublished() {}
@ -270,7 +278,7 @@ public final class XmrConnectionService {
accountService.checkAccountOpen(); accountService.checkAccountOpen();
// user needs to authorize fallback on startup after using locally synced node // user needs to authorize fallback on startup after using locally synced node
if (lastInfo == null && !fallbackApplied && lastUsedLocalSyncingNode() && !xmrLocalNode.isDetected()) { if (fallbackRequiredBeforeConnectionSwitch()) {
log.warn("Cannot get best connection on startup because we last synced local node and user has not opted to fallback"); log.warn("Cannot get best connection on startup because we last synced local node and user has not opted to fallback");
return null; return null;
} }
@ -283,6 +291,10 @@ public final class XmrConnectionService {
return bestConnection; return bestConnection;
} }
private boolean fallbackRequiredBeforeConnectionSwitch() {
return lastInfo == null && !fallbackApplied && usedSyncingLocalNodeBeforeStartup && (!xmrLocalNode.isDetected() || xmrLocalNode.shouldBeIgnored());
}
private void addLocalNodeIfIgnored(Collection<MoneroRpcConnection> ignoredConnections) { private void addLocalNodeIfIgnored(Collection<MoneroRpcConnection> ignoredConnections) {
if (xmrLocalNode.shouldBeIgnored() && connectionManager.hasConnection(xmrLocalNode.getUri())) ignoredConnections.add(connectionManager.getConnectionByUri(xmrLocalNode.getUri())); if (xmrLocalNode.shouldBeIgnored() && connectionManager.hasConnection(xmrLocalNode.getUri())) ignoredConnections.add(connectionManager.getConnectionByUri(xmrLocalNode.getUri()));
} }
@ -458,15 +470,20 @@ public final class XmrConnectionService {
public void fallbackToBestConnection() { public void fallbackToBestConnection() {
if (isShutDownStarted) return; if (isShutDownStarted) return;
if (xmrNodes.getProvidedXmrNodes().isEmpty()) { fallbackApplied = true;
if (isProvidedConnections() || xmrNodes.getProvidedXmrNodes().isEmpty()) {
log.warn("Falling back to public nodes"); log.warn("Falling back to public nodes");
preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PUBLIC.ordinal()); preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PUBLIC.ordinal());
initializeConnections();
} else { } else {
log.warn("Falling back to provided nodes"); log.warn("Falling back to provided nodes");
preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PROVIDED.ordinal()); preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PROVIDED.ordinal());
initializeConnections();
if (getConnection() == null) {
log.warn("No provided nodes available, falling back to public nodes");
fallbackToBestConnection();
}
} }
fallbackApplied = true;
initializeConnections();
} }
// ------------------------------- HELPERS -------------------------------- // ------------------------------- HELPERS --------------------------------
@ -578,8 +595,8 @@ public final class XmrConnectionService {
setConnection(connection.getUri()); setConnection(connection.getUri());
// reset error connecting to local node // reset error connecting to local node
if (connectionServiceError.get() == XmrConnectionError.LOCAL && isConnectionLocalHost()) { if (connectionServiceFallbackType.get() == XmrConnectionFallbackType.LOCAL && isConnectionLocalHost()) {
connectionServiceError.set(null); connectionServiceFallbackType.set(null);
} }
} else if (getConnection() != null && getConnection().getUri().equals(connection.getUri())) { } else if (getConnection() != null && getConnection().getUri().equals(connection.getUri())) {
MoneroRpcConnection bestConnection = getBestConnection(); MoneroRpcConnection bestConnection = getBestConnection();
@ -602,8 +619,10 @@ public final class XmrConnectionService {
// add default connections // add default connections
for (XmrNode node : xmrNodes.getAllXmrNodes()) { for (XmrNode node : xmrNodes.getAllXmrNodes()) {
if (node.hasClearNetAddress()) { if (node.hasClearNetAddress()) {
MoneroRpcConnection connection = new MoneroRpcConnection(node.getAddress() + ":" + node.getPort()).setPriority(node.getPriority()); if (!xmrLocalNode.shouldBeIgnored() || !xmrLocalNode.equalsUri(node.getClearNetUri())) {
if (!connectionList.hasConnection(connection.getUri())) addConnection(connection); MoneroRpcConnection connection = new MoneroRpcConnection(node.getHostNameOrAddress() + ":" + node.getPort()).setPriority(node.getPriority());
if (!connectionList.hasConnection(connection.getUri())) addConnection(connection);
}
} }
if (node.hasOnionAddress()) { if (node.hasOnionAddress()) {
MoneroRpcConnection connection = new MoneroRpcConnection(node.getOnionAddress() + ":" + node.getPort()).setPriority(node.getPriority()); MoneroRpcConnection connection = new MoneroRpcConnection(node.getOnionAddress() + ":" + node.getPort()).setPriority(node.getPriority());
@ -615,8 +634,10 @@ public final class XmrConnectionService {
// add default connections // add default connections
for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) { for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) {
if (node.hasClearNetAddress()) { if (node.hasClearNetAddress()) {
MoneroRpcConnection connection = new MoneroRpcConnection(node.getAddress() + ":" + node.getPort()).setPriority(node.getPriority()); if (!xmrLocalNode.shouldBeIgnored() || !xmrLocalNode.equalsUri(node.getClearNetUri())) {
addConnection(connection); MoneroRpcConnection connection = new MoneroRpcConnection(node.getHostNameOrAddress() + ":" + node.getPort()).setPriority(node.getPriority());
addConnection(connection);
}
} }
if (node.hasOnionAddress()) { if (node.hasOnionAddress()) {
MoneroRpcConnection connection = new MoneroRpcConnection(node.getOnionAddress() + ":" + node.getPort()).setPriority(node.getPriority()); MoneroRpcConnection connection = new MoneroRpcConnection(node.getOnionAddress() + ":" + node.getPort()).setPriority(node.getPriority());
@ -632,6 +653,11 @@ public final class XmrConnectionService {
} }
} }
// set if last node was locally syncing
if (!isInitialized) {
usedSyncingLocalNodeBeforeStartup = connectionList.getCurrentConnectionUri().isPresent() && xmrLocalNode.equalsUri(connectionList.getCurrentConnectionUri().get()) && preferences.getXmrNodeSettings().getSyncBlockchain();
}
// set connection proxies // set connection proxies
log.info("TOR proxy URI: " + getProxyUri()); log.info("TOR proxy URI: " + getProxyUri());
for (MoneroRpcConnection connection : connectionManager.getConnections()) { for (MoneroRpcConnection connection : connectionManager.getConnections()) {
@ -666,29 +692,16 @@ public final class XmrConnectionService {
onConnectionChanged(connectionManager.getConnection()); onConnectionChanged(connectionManager.getConnection());
} }
private boolean lastUsedLocalSyncingNode() { public void startLocalNode() throws Exception {
return connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored();
}
public void startLocalNode() {
// cannot start local node as seed node // cannot start local node as seed node
if (HavenoUtils.isSeedNode()) { if (HavenoUtils.isSeedNode()) {
throw new RuntimeException("Cannot start local node on seed node"); throw new RuntimeException("Cannot start local node on seed node");
} }
// start local node if offline and used as last connection // start local node
if (connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored()) { log.info("Starting local node");
try { xmrLocalNode.start();
log.info("Starting local node");
xmrLocalNode.start();
} catch (Exception e) {
log.error("Unable to start local monero node, error={}\n", e.getMessage(), e);
throw new RuntimeException(e);
}
} else {
throw new RuntimeException("Local node is not offline and used as last connection");
}
} }
private void onConnectionChanged(MoneroRpcConnection currentConnection) { private void onConnectionChanged(MoneroRpcConnection currentConnection) {
@ -768,7 +781,7 @@ public final class XmrConnectionService {
try { try {
// poll daemon // poll daemon
if (daemon == null) switchToBestConnection(); if (daemon == null && !fallbackRequiredBeforeConnectionSwitch()) switchToBestConnection();
try { try {
if (daemon == null) throw new RuntimeException("No connection to Monero daemon"); if (daemon == null) throw new RuntimeException("No connection to Monero daemon");
lastInfo = daemon.getInfo(); lastInfo = daemon.getInfo();
@ -778,16 +791,19 @@ public final class XmrConnectionService {
if (isShutDownStarted) return; if (isShutDownStarted) return;
// invoke fallback handling on startup error // invoke fallback handling on startup error
boolean canFallback = isFixedConnection() || isCustomConnections() || lastUsedLocalSyncingNode(); boolean canFallback = isFixedConnection() || isProvidedConnections() || isCustomConnections() || usedSyncingLocalNodeBeforeStartup;
if (lastInfo == null && canFallback) { if (lastInfo == null && canFallback) {
if (connectionServiceError.get() == null && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) { if (connectionServiceFallbackType.get() == null && (lastFallbackInvocation == null || System.currentTimeMillis() - lastFallbackInvocation > FALLBACK_INVOCATION_PERIOD_MS)) {
lastFallbackInvocation = System.currentTimeMillis(); lastFallbackInvocation = System.currentTimeMillis();
if (lastUsedLocalSyncingNode()) { if (usedSyncingLocalNodeBeforeStartup) {
log.warn("Failed to fetch daemon info from local connection on startup: " + e.getMessage()); log.warn("Failed to fetch daemon info from local connection on startup: " + e.getMessage());
connectionServiceError.set(XmrConnectionError.LOCAL); connectionServiceFallbackType.set(XmrConnectionFallbackType.LOCAL);
} else if (isProvidedConnections()) {
log.warn("Failed to fetch daemon info from provided connections on startup: " + e.getMessage());
connectionServiceFallbackType.set(XmrConnectionFallbackType.PROVIDED);
} else { } else {
log.warn("Failed to fetch daemon info from custom connection on startup: " + e.getMessage()); log.warn("Failed to fetch daemon info from custom connection on startup: " + e.getMessage());
connectionServiceError.set(XmrConnectionError.CUSTOM); connectionServiceFallbackType.set(XmrConnectionFallbackType.CUSTOM);
} }
} }
return; return;
@ -808,7 +824,7 @@ public final class XmrConnectionService {
// connected to daemon // connected to daemon
isConnected = true; isConnected = true;
connectionServiceError.set(null); connectionServiceFallbackType.set(null);
// determine if blockchain is syncing locally // determine if blockchain is syncing locally
boolean blockchainSyncing = lastInfo.getHeight().equals(lastInfo.getHeightWithoutBootstrap()) || (lastInfo.getTargetHeight().equals(0l) && lastInfo.getHeightWithoutBootstrap().equals(0l)); // blockchain is syncing if height equals height without bootstrap, or target height and height without bootstrap both equal 0 boolean blockchainSyncing = lastInfo.getHeight().equals(lastInfo.getHeightWithoutBootstrap()) || (lastInfo.getTargetHeight().equals(0l) && lastInfo.getHeightWithoutBootstrap().equals(0l)); // blockchain is syncing if height equals height without bootstrap, or target height and height without bootstrap both equal 0
@ -885,10 +901,14 @@ public final class XmrConnectionService {
} }
private boolean isFixedConnection() { private boolean isFixedConnection() {
return !"".equals(config.xmrNode) && (!HavenoUtils.isLocalHost(config.xmrNode) || !xmrLocalNode.shouldBeIgnored()) && !fallbackApplied; return !"".equals(config.xmrNode) && !(HavenoUtils.isLocalHost(config.xmrNode) && xmrLocalNode.shouldBeIgnored()) && !fallbackApplied;
} }
private boolean isCustomConnections() { private boolean isCustomConnections() {
return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM; return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM;
} }
private boolean isProvidedConnections() {
return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.PROVIDED;
}
} }

View File

@ -109,17 +109,18 @@ public class XmrLocalNode {
public boolean shouldBeIgnored() { public boolean shouldBeIgnored() {
if (config.ignoreLocalXmrNode) return true; if (config.ignoreLocalXmrNode) return true;
// determine if local node is configured // ignore if fixed connection is not local
if (!"".equals(config.xmrNode)) return !HavenoUtils.isLocalHost(config.xmrNode);
// check if local node is within configuration
boolean hasConfiguredLocalNode = false; boolean hasConfiguredLocalNode = false;
for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) { for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) {
if (node.getAddress() != null && equalsUri("http://" + node.getAddress() + ":" + node.getPort())) { if (node.hasClearNetAddress() && equalsUri(node.getClearNetUri())) {
hasConfiguredLocalNode = true; hasConfiguredLocalNode = true;
break; break;
} }
} }
if (!hasConfiguredLocalNode) return true; return !hasConfiguredLocalNode;
return false;
} }
public void addListener(XmrLocalNodeListener listener) { public void addListener(XmrLocalNodeListener listener) {

View File

@ -75,7 +75,7 @@ public class HavenoHeadlessApp implements HeadlessApp {
log.info("onDisplayTacHandler: We accept the tacs automatically in headless mode"); log.info("onDisplayTacHandler: We accept the tacs automatically in headless mode");
acceptedHandler.run(); acceptedHandler.run();
}); });
havenoSetup.setDisplayMoneroConnectionErrorHandler(show -> log.warn("onDisplayMoneroConnectionErrorHandler: show={}", show)); havenoSetup.setDisplayMoneroConnectionFallbackHandler(show -> log.warn("onDisplayMoneroConnectionFallbackHandler: show={}", show));
havenoSetup.setDisplayTorNetworkSettingsHandler(show -> log.info("onDisplayTorNetworkSettingsHandler: show={}", show)); havenoSetup.setDisplayTorNetworkSettingsHandler(show -> log.info("onDisplayTorNetworkSettingsHandler: show={}", show));
havenoSetup.setChainFileLockedExceptionHandler(msg -> log.error("onChainFileLockedExceptionHandler: msg={}", msg)); havenoSetup.setChainFileLockedExceptionHandler(msg -> log.error("onChainFileLockedExceptionHandler: msg={}", msg));
tradeManager.setLockedUpFundsHandler(msg -> log.info("onLockedUpFundsHandler: msg={}", msg)); tradeManager.setLockedUpFundsHandler(msg -> log.info("onLockedUpFundsHandler: msg={}", msg));

View File

@ -55,7 +55,7 @@ import haveno.core.alert.PrivateNotificationManager;
import haveno.core.alert.PrivateNotificationPayload; import haveno.core.alert.PrivateNotificationPayload;
import haveno.core.api.CoreContext; import haveno.core.api.CoreContext;
import haveno.core.api.XmrConnectionService; import haveno.core.api.XmrConnectionService;
import haveno.core.api.XmrConnectionService.XmrConnectionError; import haveno.core.api.XmrConnectionService.XmrConnectionFallbackType;
import haveno.core.api.XmrLocalNode; import haveno.core.api.XmrLocalNode;
import haveno.core.locale.Res; import haveno.core.locale.Res;
import haveno.core.offer.OpenOfferManager; import haveno.core.offer.OpenOfferManager;
@ -159,7 +159,7 @@ public class HavenoSetup {
rejectedTxErrorMessageHandler; rejectedTxErrorMessageHandler;
@Setter @Setter
@Nullable @Nullable
private Consumer<XmrConnectionError> displayMoneroConnectionErrorHandler; private Consumer<XmrConnectionFallbackType> displayMoneroConnectionFallbackHandler;
@Setter @Setter
@Nullable @Nullable
private Consumer<Boolean> displayTorNetworkSettingsHandler; private Consumer<Boolean> displayTorNetworkSettingsHandler;
@ -431,9 +431,9 @@ public class HavenoSetup {
getXmrWalletSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout()); getXmrWalletSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout());
// listen for fallback handling // listen for fallback handling
getConnectionServiceError().addListener((observable, oldValue, newValue) -> { getConnectionServiceFallbackType().addListener((observable, oldValue, newValue) -> {
if (displayMoneroConnectionErrorHandler == null) return; if (displayMoneroConnectionFallbackHandler == null) return;
displayMoneroConnectionErrorHandler.accept(newValue); displayMoneroConnectionFallbackHandler.accept(newValue);
}); });
log.info("Init P2P network"); log.info("Init P2P network");
@ -735,8 +735,8 @@ public class HavenoSetup {
return xmrConnectionService.getConnectionServiceErrorMsg(); return xmrConnectionService.getConnectionServiceErrorMsg();
} }
public ObjectProperty<XmrConnectionError> getConnectionServiceError() { public ObjectProperty<XmrConnectionFallbackType> getConnectionServiceFallbackType() {
return xmrConnectionService.getConnectionServiceError(); return xmrConnectionService.getConnectionServiceFallbackType();
} }
public StringProperty getTopErrorMsg() { public StringProperty getTopErrorMsg() {

View File

@ -149,6 +149,20 @@ public class OfferBookService {
Offer offer = new Offer(offerPayload); Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService); offer.setPriceFeedService(priceFeedService);
announceOfferRemoved(offer); announceOfferRemoved(offer);
// check if invalid offers are now valid
synchronized (invalidOffers) {
for (Offer invalidOffer : new ArrayList<Offer>(invalidOffers)) {
try {
validateOfferPayload(invalidOffer.getOfferPayload());
removeInvalidOffer(invalidOffer.getId());
replaceValidOffer(invalidOffer);
announceOfferAdded(invalidOffer);
} catch (Exception e) {
// ignore
}
}
}
} }
}); });
}, OfferBookService.class.getSimpleName()); }, OfferBookService.class.getSimpleName());
@ -298,20 +312,6 @@ public class OfferBookService {
synchronized (offerBookChangedListeners) { synchronized (offerBookChangedListeners) {
offerBookChangedListeners.forEach(listener -> listener.onRemoved(offer)); offerBookChangedListeners.forEach(listener -> listener.onRemoved(offer));
} }
// check if invalid offers are now valid
synchronized (invalidOffers) {
for (Offer invalidOffer : new ArrayList<Offer>(invalidOffers)) {
try {
validateOfferPayload(invalidOffer.getOfferPayload());
removeInvalidOffer(invalidOffer.getId());
replaceValidOffer(invalidOffer);
announceOfferAdded(invalidOffer);
} catch (Exception e) {
// ignore
}
}
}
} }
private boolean hasValidOffer(String offerId) { private boolean hasValidOffer(String offerId) {

View File

@ -1101,17 +1101,20 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
} else { } else {
// validate non-pending state // validate non-pending state
try { boolean skipValidation = openOffer.isDeactivated() && hasConflictingClone(openOffer) && openOffer.getOffer().getOfferPayload().getArbitratorSignature() == null; // clone with conflicting offer is deactivated and unsigned at first
validateSignedState(openOffer); if (!skipValidation) {
resultHandler.handleResult(null); // done processing if non-pending state is valid try {
return; validateSignedState(openOffer);
} catch (Exception e) { resultHandler.handleResult(null); // done processing if non-pending state is valid
log.warn(e.getMessage()); return;
} catch (Exception e) {
log.warn(e.getMessage());
// reset arbitrator signature // reset arbitrator signature
openOffer.getOffer().getOfferPayload().setArbitratorSignature(null); openOffer.getOffer().getOfferPayload().setArbitratorSignature(null);
openOffer.getOffer().getOfferPayload().setArbitratorSigner(null); openOffer.getOffer().getOfferPayload().setArbitratorSigner(null);
if (openOffer.isAvailable()) openOffer.setState(OpenOffer.State.PENDING); if (openOffer.isAvailable()) openOffer.setState(OpenOffer.State.PENDING);
}
} }
} }
@ -1574,14 +1577,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
} }
} }
// verify the max version number
if (Version.compare(request.getOfferPayload().getVersionNr(), Version.VERSION) > 0) {
errorMessage = "Offer version number is too high: " + request.getOfferPayload().getVersionNr() + " > " + Version.VERSION;
log.warn(errorMessage);
sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage);
return;
}
// verify maker and taker fees // verify maker and taker fees
boolean hasBuyerAsTakerWithoutDeposit = offer.getDirection() == OfferDirection.SELL && offer.isPrivateOffer() && offer.getChallengeHash() != null && offer.getChallengeHash().length() > 0 && offer.getTakerFeePct() == 0; boolean hasBuyerAsTakerWithoutDeposit = offer.getDirection() == OfferDirection.SELL && offer.isPrivateOffer() && offer.getChallengeHash() != null && offer.getChallengeHash().length() > 0 && offer.getTakerFeePct() == 0;
if (hasBuyerAsTakerWithoutDeposit) { if (hasBuyerAsTakerWithoutDeposit) {
@ -2023,7 +2018,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
originalOfferPayload.getAcceptedCountryCodes(), originalOfferPayload.getAcceptedCountryCodes(),
originalOfferPayload.getBankId(), originalOfferPayload.getBankId(),
originalOfferPayload.getAcceptedBankIds(), originalOfferPayload.getAcceptedBankIds(),
originalOfferPayload.getVersionNr(), Version.VERSION,
originalOfferPayload.getBlockHeightAtOfferCreation(), originalOfferPayload.getBlockHeightAtOfferCreation(),
originalOfferPayload.getMaxTradeLimit(), originalOfferPayload.getMaxTradeLimit(),
originalOfferPayload.getMaxTradePeriod(), originalOfferPayload.getMaxTradePeriod(),

View File

@ -145,6 +145,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
private static final long DELETE_AFTER_MS = TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS; private static final long DELETE_AFTER_MS = TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS;
private static final int NUM_CONFIRMATIONS_FOR_SCHEDULED_IMPORT = 5; private static final int NUM_CONFIRMATIONS_FOR_SCHEDULED_IMPORT = 5;
protected final Object pollLock = new Object(); protected final Object pollLock = new Object();
private final Object removeTradeOnErrorLock = new Object();
protected static final Object importMultisigLock = new Object(); protected static final Object importMultisigLock = new Object();
private boolean pollInProgress; private boolean pollInProgress;
private boolean restartInProgress; private boolean restartInProgress;
@ -1608,11 +1609,12 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
} }
// shut down trade threads // shut down trade threads
isInitialized = false;
isShutDown = true; isShutDown = true;
List<Runnable> shutDownThreads = new ArrayList<>(); List<Runnable> shutDownThreads = new ArrayList<>();
shutDownThreads.add(() -> ThreadUtils.shutDown(getId())); shutDownThreads.add(() -> ThreadUtils.shutDown(getId()));
ThreadUtils.awaitTasks(shutDownThreads); ThreadUtils.awaitTasks(shutDownThreads);
stopProtocolTimeout();
isInitialized = false;
// save and close // save and close
if (wallet != null) { if (wallet != null) {
@ -1765,24 +1767,30 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
} }
private void removeTradeOnError() { private void removeTradeOnError() {
log.warn("removeTradeOnError() trade={}, tradeId={}, state={}", getClass().getSimpleName(), getShortId(), getState()); synchronized (removeTradeOnErrorLock) {
// force close and re-open wallet in case stuck // skip if already shut down or removed
forceCloseWallet(); if (isShutDown || !processModel.getTradeManager().hasTrade(getId())) return;
if (isDepositRequested()) getWallet(); log.warn("removeTradeOnError() trade={}, tradeId={}, state={}", getClass().getSimpleName(), getShortId(), getState());
// shut down trade thread // force close and re-open wallet in case stuck
try { forceCloseWallet();
ThreadUtils.shutDown(getId(), 1000l); if (isDepositRequested()) getWallet();
} catch (Exception e) {
log.warn("Error shutting down trade thread for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage()); // clear and shut down trade
onShutDownStarted();
clearAndShutDown();
// shut down trade thread
try {
ThreadUtils.shutDown(getId(), 5000l);
} catch (Exception e) {
log.warn("Error shutting down trade thread for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage());
}
// unregister trade
processModel.getTradeManager().unregisterTrade(this);
} }
// clear and shut down trade
clearAndShutDown();
// unregister trade
processModel.getTradeManager().unregisterTrade(this);
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -1824,6 +1832,13 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
getProtocol().startTimeout(TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS); getProtocol().startTimeout(TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS);
} }
public void stopProtocolTimeout() {
if (!isInitialized) return;
TradeProtocol protocol = getProtocol();
if (protocol == null) return;
protocol.stopTimeout();
}
public void setState(State state) { public void setState(State state) {
if (isInitialized) { if (isInitialized) {
// We don't want to log at startup the setState calls from all persisted trades // We don't want to log at startup the setState calls from all persisted trades
@ -2648,7 +2663,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
} }
} }
setDepositTxs(txs); setDepositTxs(txs);
if (getMaker().getDepositTx() == null || (getTaker().getDepositTx() == null && !hasBuyerAsTakerWithoutDeposit())) return; // skip if either deposit tx not seen if (!isPublished(getMaker().getDepositTx()) || (!hasBuyerAsTakerWithoutDeposit() && !isPublished(getTaker().getDepositTx()))) return; // skip if deposit txs not published successfully
setStateDepositsSeen(); setStateDepositsSeen();
// set actual security deposits // set actual security deposits
@ -2750,6 +2765,13 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
} }
} }
private static boolean isPublished(MoneroTx tx) {
if (tx == null) return false;
if (Boolean.TRUE.equals(tx.isFailed())) return false;
if (!Boolean.TRUE.equals(tx.inTxPool()) && !Boolean.TRUE.equals(tx.isConfirmed())) return false;
return true;
}
private void syncWalletIfBehind() { private void syncWalletIfBehind() {
synchronized (walletLock) { synchronized (walletLock) {
if (isWalletBehind()) { if (isWalletBehind()) {

View File

@ -563,9 +563,14 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOffer(request.getOfferId()); Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOffer(request.getOfferId());
if (!openOfferOptional.isPresent()) return; if (!openOfferOptional.isPresent()) return;
OpenOffer openOffer = openOfferOptional.get(); OpenOffer openOffer = openOfferOptional.get();
if (openOffer.getState() != OpenOffer.State.AVAILABLE) return;
Offer offer = openOffer.getOffer(); Offer offer = openOffer.getOffer();
// check availability
if (openOffer.getState() != OpenOffer.State.AVAILABLE) {
log.warn("Ignoring InitTradeRequest to maker because offer is not available, offerId={}, sender={}", request.getOfferId(), sender);
return;
}
// validate challenge // validate challenge
if (openOffer.getChallenge() != null && !HavenoUtils.getChallengeHash(openOffer.getChallenge()).equals(HavenoUtils.getChallengeHash(request.getChallenge()))) { if (openOffer.getChallenge() != null && !HavenoUtils.getChallengeHash(openOffer.getChallenge()).equals(HavenoUtils.getChallengeHash(request.getChallenge()))) {
log.warn("Ignoring InitTradeRequest to maker because challenge is incorrect, tradeId={}, sender={}", request.getOfferId(), sender); log.warn("Ignoring InitTradeRequest to maker because challenge is incorrect, tradeId={}, sender={}", request.getOfferId(), sender);
@ -980,9 +985,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
closedTradableManager.add(trade); closedTradableManager.add(trade);
trade.setCompleted(true); trade.setCompleted(true);
removeTrade(trade, true); removeTrade(trade, true);
xmrWalletService.swapPayoutAddressEntryToAvailable(trade.getId()); // TODO The address entry should have been removed already. Check and if its the case remove that.
// TODO The address entry should have been removed already. Check and if its the case remove that.
xmrWalletService.swapPayoutAddressEntryToAvailable(trade.getId());
requestPersistence(); requestPersistence();
} }
@ -990,6 +993,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
log.warn("Unregistering {} {}", trade.getClass().getSimpleName(), trade.getId()); log.warn("Unregistering {} {}", trade.getClass().getSimpleName(), trade.getId());
removeTrade(trade, true); removeTrade(trade, true);
removeFailedTrade(trade); removeFailedTrade(trade);
xmrWalletService.swapPayoutAddressEntryToAvailable(trade.getId()); // TODO The address entry should have been removed already. Check and if its the case remove that.
requestPersistence(); requestPersistence();
} }
@ -1274,11 +1278,15 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
return offer.getDirection() == OfferDirection.SELL; return offer.getDirection() == OfferDirection.SELL;
} }
// TODO (woodser): make Optional<Trade> versus Trade return types consistent // TODO: make Optional<Trade> versus Trade return types consistent
public Trade getTrade(String tradeId) { public Trade getTrade(String tradeId) {
return getOpenTrade(tradeId).orElseGet(() -> getClosedTrade(tradeId).orElseGet(() -> getFailedTrade(tradeId).orElseGet(() -> null))); return getOpenTrade(tradeId).orElseGet(() -> getClosedTrade(tradeId).orElseGet(() -> getFailedTrade(tradeId).orElseGet(() -> null)));
} }
public boolean hasTrade(String tradeId) {
return getTrade(tradeId) != null;
}
public Optional<Trade> getOpenTrade(String tradeId) { public Optional<Trade> getOpenTrade(String tradeId) {
synchronized (tradableList.getList()) { synchronized (tradableList.getList()) {
return tradableList.stream().filter(e -> e.getId().equals(tradeId)).findFirst(); return tradableList.stream().filter(e -> e.getId().equals(tradeId)).findFirst();

View File

@ -460,9 +460,19 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
.using(new TradeTaskRunner(trade, .using(new TradeTaskRunner(trade,
() -> { () -> {
stopTimeout(); stopTimeout();
this.errorMessageHandler = null; // TODO: set this when trade state is >= DEPOSIT_PUBLISHED
handleTaskRunnerSuccess(sender, response); // tasks may complete successfully but process an error
if (tradeResultHandler != null) tradeResultHandler.handleResult(trade); // trade is initialized if (trade.getInitError() == null) {
this.errorMessageHandler = null; // TODO: set this when trade state is >= DEPOSIT_PUBLISHED
handleTaskRunnerSuccess(sender, response);
if (tradeResultHandler != null) tradeResultHandler.handleResult(trade); // trade is initialized
} else {
handleTaskRunnerSuccess(sender, response);
if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage(trade.getInitError().getMessage());
}
this.tradeResultHandler = null;
this.errorMessageHandler = null;
}, },
errorMessage -> { errorMessage -> {
handleTaskRunnerFault(sender, response, errorMessage); handleTaskRunnerFault(sender, response, errorMessage);
@ -832,7 +842,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
} }
} }
protected synchronized void stopTimeout() { public synchronized void stopTimeout() {
synchronized (timeoutTimerLock) { synchronized (timeoutTimerLock) {
if (timeoutTimer != null) { if (timeoutTimer != null) {
timeoutTimer.stop(); timeoutTimer.stop();

View File

@ -95,6 +95,18 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
// set peer's signature // set peer's signature
sender.setContractSignature(signature); sender.setContractSignature(signature);
// subscribe to trade state once to send responses with ack or nack
if (!hasBothContractSignatures()) {
trade.stateProperty().addListener((obs, oldState, newState) -> {
if (oldState == newState) return;
if (newState == Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED) {
sendDepositResponsesOnce(trade.getProcessModel().error == null ? "Arbitrator failed to publish deposit txs within timeout for trade " + trade.getId() : trade.getProcessModel().error.getMessage());
} else if (newState.ordinal() >= Trade.State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS.ordinal()) {
sendDepositResponsesOnce(null);
}
});
}
// collect expected values // collect expected values
Offer offer = trade.getOffer(); Offer offer = trade.getOffer();
boolean isFromTaker = sender == trade.getTaker(); boolean isFromTaker = sender == trade.getTaker();
@ -138,7 +150,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
// relay deposit txs when both requests received // relay deposit txs when both requests received
MoneroDaemon daemon = trade.getXmrWalletService().getDaemon(); MoneroDaemon daemon = trade.getXmrWalletService().getDaemon();
if (processModel.getMaker().getContractSignature() != null && processModel.getTaker().getContractSignature() != null) { if (hasBothContractSignatures()) {
// check timeout and extend just before relaying // check timeout and extend just before relaying
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out before relaying deposit txs for {} {}" + trade.getClass().getSimpleName() + " " + trade.getShortId()); if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out before relaying deposit txs for {} {}" + trade.getClass().getSimpleName() + " " + trade.getShortId());
@ -182,22 +194,15 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
throw e; throw e;
} }
} else { } else {
// subscribe to trade state once to send responses with ack or nack
trade.stateProperty().addListener((obs, oldState, newState) -> {
if (oldState == newState) return;
if (newState == Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED) {
sendDepositResponsesOnce(trade.getProcessModel().error == null ? "Arbitrator failed to publish deposit txs within timeout for trade " + trade.getId() : trade.getProcessModel().error.getMessage());
} else if (newState.ordinal() >= Trade.State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS.ordinal()) {
sendDepositResponsesOnce(null);
}
});
if (processModel.getMaker().getDepositTxHex() == null) log.info("Arbitrator waiting for deposit request from maker for trade " + trade.getId()); if (processModel.getMaker().getDepositTxHex() == null) log.info("Arbitrator waiting for deposit request from maker for trade " + trade.getId());
if (processModel.getTaker().getDepositTxHex() == null && !trade.hasBuyerAsTakerWithoutDeposit()) log.info("Arbitrator waiting for deposit request from taker for trade " + trade.getId()); if (processModel.getTaker().getDepositTxHex() == null && !trade.hasBuyerAsTakerWithoutDeposit()) log.info("Arbitrator waiting for deposit request from taker for trade " + trade.getId());
} }
} }
private boolean hasBothContractSignatures() {
return processModel.getMaker().getContractSignature() != null && processModel.getTaker().getContractSignature() != null;
}
private boolean isTimedOut() { private boolean isTimedOut() {
return !processModel.getTradeManager().hasOpenTrade(trade); return !processModel.getTradeManager().hasOpenTrade(trade);
} }
@ -210,7 +215,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
// log error // log error
if (errorMessage != null) { if (errorMessage != null) {
log.warn("Sending deposit responses with error={}", errorMessage, new Throwable("Stack trace")); log.warn("Sending deposit responses for tradeId={}, error={}", trade.getId(), errorMessage);
} }
// create deposit response // create deposit response
@ -229,7 +234,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
} }
private void sendDepositResponse(NodeAddress nodeAddress, PubKeyRing pubKeyRing, DepositResponse response) { private void sendDepositResponse(NodeAddress nodeAddress, PubKeyRing pubKeyRing, DepositResponse response) {
log.info("Sending deposit response to trader={}; offerId={}, error={}", nodeAddress, trade.getId(), trade.getProcessModel().error); log.info("Sending deposit response to trader={}; offerId={}, error={}", nodeAddress, trade.getId(), response.getErrorMessage());
processModel.getP2PService().sendEncryptedDirectMessage(nodeAddress, pubKeyRing, response, new SendDirectMessageListener() { processModel.getP2PService().sendEncryptedDirectMessage(nodeAddress, pubKeyRing, response, new SendDirectMessageListener() {
@Override @Override
public void onArrived() { public void onArrived() {

View File

@ -38,13 +38,14 @@ public class ProcessDepositResponse extends TradeTask {
try { try {
runInterceptHook(); runInterceptHook();
// throw if error // handle error
DepositResponse message = (DepositResponse) processModel.getTradeMessage(); DepositResponse message = (DepositResponse) processModel.getTradeMessage();
if (message.getErrorMessage() != null) { if (message.getErrorMessage() != null) {
log.warn("Unregistering trade {} {} because deposit response has error message={}", trade.getClass().getSimpleName(), trade.getShortId(), message.getErrorMessage()); log.warn("Deposit response for {} {} has error message={}", trade.getClass().getSimpleName(), trade.getShortId(), message.getErrorMessage());
trade.setStateIfValidTransitionTo(Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED); trade.setStateIfValidTransitionTo(Trade.State.PUBLISH_DEPOSIT_TX_REQUEST_FAILED);
processModel.getTradeManager().unregisterTrade(trade); trade.setInitError(new RuntimeException(message.getErrorMessage()));
throw new RuntimeException(message.getErrorMessage()); complete();
return;
} }
// record security deposits // record security deposits

View File

@ -37,7 +37,6 @@ package haveno.core.xmr;
import com.google.inject.Inject; import com.google.inject.Inject;
import haveno.common.ThreadUtils; import haveno.common.ThreadUtils;
import haveno.common.UserThread;
import haveno.core.api.model.XmrBalanceInfo; import haveno.core.api.model.XmrBalanceInfo;
import haveno.core.offer.OpenOffer; import haveno.core.offer.OpenOffer;
import haveno.core.offer.OpenOfferManager; import haveno.core.offer.OpenOfferManager;
@ -163,18 +162,12 @@ public class Balances {
// calculate reserved balance // calculate reserved balance
reservedBalance = reservedOfferBalance.add(reservedTradeBalance); reservedBalance = reservedOfferBalance.add(reservedTradeBalance);
// play sound if funds received
boolean fundsReceived = balanceSumBefore != null && getNonTradeBalanceSum().compareTo(balanceSumBefore) > 0;
if (fundsReceived) HavenoUtils.playCashRegisterSound();
// notify balance update // notify balance update
UserThread.execute(() -> { updateCounter.set(updateCounter.get() + 1);
// check if funds received
boolean fundsReceived = balanceSumBefore != null && getNonTradeBalanceSum().compareTo(balanceSumBefore) > 0;
if (fundsReceived) {
HavenoUtils.playCashRegisterSound();
}
// increase counter to notify listeners
updateCounter.set(updateCounter.get() + 1);
});
} }
} }
} }

View File

@ -184,10 +184,6 @@ public class XmrNodes {
this.operator = operator; this.operator = operator;
} }
public boolean hasOnionAddress() {
return onionAddress != null;
}
public String getHostNameOrAddress() { public String getHostNameOrAddress() {
if (hostName != null) if (hostName != null)
return hostName; return hostName;
@ -195,10 +191,19 @@ public class XmrNodes {
return address; return address;
} }
public boolean hasOnionAddress() {
return onionAddress != null;
}
public boolean hasClearNetAddress() { public boolean hasClearNetAddress() {
return hostName != null || address != null; return hostName != null || address != null;
} }
public String getClearNetUri() {
if (!hasClearNetAddress()) throw new IllegalStateException("XmrNode does not have clearnet address");
return "http://" + getHostNameOrAddress() + ":" + port;
}
@Override @Override
public String toString() { public String toString() {
return "onionAddress='" + onionAddress + '\'' + return "onionAddress='" + onionAddress + '\'' +

View File

@ -159,8 +159,13 @@ public class XmrKeyImagePoller {
if (keyImagesGroup == null) return; if (keyImagesGroup == null) return;
keyImagesGroup.removeAll(keyImages); keyImagesGroup.removeAll(keyImages);
if (keyImagesGroup.isEmpty()) keyImageGroups.remove(groupId); if (keyImagesGroup.isEmpty()) keyImageGroups.remove(groupId);
Set<String> allKeyImages = getKeyImages();
synchronized (lastStatuses) { synchronized (lastStatuses) {
for (String lastKeyImage : new HashSet<>(lastStatuses.keySet())) lastStatuses.remove(lastKeyImage); for (String keyImage : keyImages) {
if (lastStatuses.containsKey(keyImage) && !allKeyImages.contains(keyImage)) {
lastStatuses.remove(keyImage);
}
}
} }
refreshPolling(); refreshPolling();
} }
@ -171,10 +176,10 @@ public class XmrKeyImagePoller {
Set<String> keyImagesGroup = keyImageGroups.get(groupId); Set<String> keyImagesGroup = keyImageGroups.get(groupId);
if (keyImagesGroup == null) return; if (keyImagesGroup == null) return;
keyImageGroups.remove(groupId); keyImageGroups.remove(groupId);
Set<String> keyImages = getKeyImages(); Set<String> allKeyImages = getKeyImages();
synchronized (lastStatuses) { synchronized (lastStatuses) {
for (String keyImage : keyImagesGroup) { for (String keyImage : keyImagesGroup) {
if (lastStatuses.containsKey(keyImage) && !keyImages.contains(keyImage)) { if (lastStatuses.containsKey(keyImage) && !allKeyImages.contains(keyImage)) {
lastStatuses.remove(keyImage); lastStatuses.remove(keyImage);
} }
} }

View File

@ -105,7 +105,6 @@ public abstract class XmrWalletBase {
// start polling wallet for progress // start polling wallet for progress
syncProgressLatch = new CountDownLatch(1); syncProgressLatch = new CountDownLatch(1);
syncProgressLooper = new TaskLooper(() -> { syncProgressLooper = new TaskLooper(() -> {
if (wallet == null) return;
long height; long height;
try { try {
height = wallet.getHeight(); // can get read timeout while syncing height = wallet.getHeight(); // can get read timeout while syncing

View File

@ -237,6 +237,7 @@ shared.pending=Pending
shared.me=Me shared.me=Me
shared.maker=Maker shared.maker=Maker
shared.taker=Taker shared.taker=Taker
shared.none=None
#################################################################### ####################################################################
@ -2090,7 +2091,8 @@ closedTradesSummaryWindow.totalTradeFeeInXmr.value={0} ({1} of total trade amoun
walletPasswordWindow.headline=Enter password to unlock walletPasswordWindow.headline=Enter password to unlock
xmrConnectionError.headline=Monero connection error xmrConnectionError.headline=Monero connection error
xmrConnectionError.customNode=Error connecting to your custom Monero node(s).\n\nDo you want to use the next best available Monero node? xmrConnectionError.providedNodes=Error connecting to provided Monero node(s).\n\nDo you want to use the next best available Monero node?
xmrConnectionError.customNodes=Error connecting to your custom Monero node(s).\n\nDo you want to use the next best available Monero node?
xmrConnectionError.localNode=We previously synced using a local Monero node, but it appears to be unreachable.\n\nPlease check that it's running and synced. xmrConnectionError.localNode=We previously synced using a local Monero node, but it appears to be unreachable.\n\nPlease check that it's running and synced.
xmrConnectionError.localNode.start=Start local node xmrConnectionError.localNode.start=Start local node
xmrConnectionError.localNode.start.error=Error starting local node xmrConnectionError.localNode.start.error=Error starting local node

View File

@ -60,6 +60,6 @@
</content_rating> </content_rating>
<releases> <releases>
<release version="1.0.19" date="2025-03-10"/> <release version="1.1.0" date="2025-04-17"/>
</releases> </releases>
</component> </component>

View File

@ -5,10 +5,10 @@
<!-- See: https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html --> <!-- See: https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -->
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0.19</string> <string>1.1.0</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.0.19</string> <string>1.1.0</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>Haveno</string> <string>Haveno</string>

View File

@ -353,7 +353,7 @@ public class MainView extends InitializableView<StackPane, MainViewModel> {
settingsButtonWithBadge.getStyleClass().add("new"); settingsButtonWithBadge.getStyleClass().add("new");
navigation.addListener((viewPath, data) -> { navigation.addListener((viewPath, data) -> {
UserThread.await(() -> { UserThread.await(() -> { // TODO: this uses `await` to fix nagivation link from market view to offer book, but await can cause hanging, so execute should be used
if (viewPath.size() != 2 || viewPath.indexOf(MainView.class) != 0) return; if (viewPath.size() != 2 || viewPath.indexOf(MainView.class) != 0) return;
Class<? extends View> viewClass = viewPath.tip(); Class<? extends View> viewClass = viewPath.tip();

View File

@ -337,7 +337,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener
tacWindow.onAction(acceptedHandler::run).show(); tacWindow.onAction(acceptedHandler::run).show();
}, 1)); }, 1));
havenoSetup.setDisplayMoneroConnectionErrorHandler(connectionError -> { havenoSetup.setDisplayMoneroConnectionFallbackHandler(connectionError -> {
if (connectionError == null) { if (connectionError == null) {
if (moneroConnectionErrorPopup != null) moneroConnectionErrorPopup.hide(); if (moneroConnectionErrorPopup != null) moneroConnectionErrorPopup.hide();
} else { } else {
@ -349,7 +349,6 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener
.actionButtonText(Res.get("xmrConnectionError.localNode.start")) .actionButtonText(Res.get("xmrConnectionError.localNode.start"))
.onAction(() -> { .onAction(() -> {
log.warn("User has chosen to start local node."); log.warn("User has chosen to start local node.");
havenoSetup.getConnectionServiceError().set(null);
new Thread(() -> { new Thread(() -> {
try { try {
HavenoUtils.xmrConnectionService.startLocalNode(); HavenoUtils.xmrConnectionService.startLocalNode();
@ -359,16 +358,20 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener
.headLine(Res.get("xmrConnectionError.localNode.start.error")) .headLine(Res.get("xmrConnectionError.localNode.start.error"))
.warning(e.getMessage()) .warning(e.getMessage())
.closeButtonText(Res.get("shared.close")) .closeButtonText(Res.get("shared.close"))
.onClose(() -> havenoSetup.getConnectionServiceError().set(null)) .onClose(() -> havenoSetup.getConnectionServiceFallbackType().set(null))
.show(); .show();
} finally {
havenoSetup.getConnectionServiceFallbackType().set(null);
} }
}).start(); }).start();
}) })
.secondaryActionButtonText(Res.get("xmrConnectionError.localNode.fallback")) .secondaryActionButtonText(Res.get("xmrConnectionError.localNode.fallback"))
.onSecondaryAction(() -> { .onSecondaryAction(() -> {
log.warn("User has chosen to fallback to the next best available Monero node."); log.warn("User has chosen to fallback to the next best available Monero node.");
havenoSetup.getConnectionServiceError().set(null); new Thread(() -> {
new Thread(() -> HavenoUtils.xmrConnectionService.fallbackToBestConnection()).start(); HavenoUtils.xmrConnectionService.fallbackToBestConnection();
havenoSetup.getConnectionServiceFallbackType().set(null);
}).start();
}) })
.closeButtonText(Res.get("shared.shutDown")) .closeButtonText(Res.get("shared.shutDown"))
.onClose(HavenoApp.getShutDownHandler()); .onClose(HavenoApp.getShutDownHandler());
@ -376,16 +379,35 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener
case CUSTOM: case CUSTOM:
moneroConnectionErrorPopup = new Popup() moneroConnectionErrorPopup = new Popup()
.headLine(Res.get("xmrConnectionError.headline")) .headLine(Res.get("xmrConnectionError.headline"))
.warning(Res.get("xmrConnectionError.customNode")) .warning(Res.get("xmrConnectionError.customNodes"))
.actionButtonText(Res.get("shared.yes")) .actionButtonText(Res.get("shared.yes"))
.onAction(() -> { .onAction(() -> {
havenoSetup.getConnectionServiceError().set(null); new Thread(() -> {
new Thread(() -> HavenoUtils.xmrConnectionService.fallbackToBestConnection()).start(); HavenoUtils.xmrConnectionService.fallbackToBestConnection();
havenoSetup.getConnectionServiceFallbackType().set(null);
}).start();
}) })
.closeButtonText(Res.get("shared.no")) .closeButtonText(Res.get("shared.no"))
.onClose(() -> { .onClose(() -> {
log.warn("User has declined to fallback to the next best available Monero node."); log.warn("User has declined to fallback to the next best available Monero node.");
havenoSetup.getConnectionServiceError().set(null); havenoSetup.getConnectionServiceFallbackType().set(null);
});
break;
case PROVIDED:
moneroConnectionErrorPopup = new Popup()
.headLine(Res.get("xmrConnectionError.headline"))
.warning(Res.get("xmrConnectionError.providedNodes"))
.actionButtonText(Res.get("shared.yes"))
.onAction(() -> {
new Thread(() -> {
HavenoUtils.xmrConnectionService.fallbackToBestConnection();
havenoSetup.getConnectionServiceFallbackType().set(null);
}).start();
})
.closeButtonText(Res.get("shared.no"))
.onClose(() -> {
log.warn("User has declined to fallback to the next best available Monero node.");
havenoSetup.getConnectionServiceFallbackType().set(null);
}); });
break; break;
} }

View File

@ -380,8 +380,6 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
} }
private void onShowPayFundsScreen() { private void onShowPayFundsScreen() {
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
nextButton.setVisible(false); nextButton.setVisible(false);
nextButton.setManaged(false); nextButton.setManaged(false);
nextButton.setOnAction(null); nextButton.setOnAction(null);
@ -445,13 +443,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
// temporarily disabled due to high CPU usage (per issue #4649) // temporarily disabled due to high CPU usage (per issue #4649)
// waitingForFundsSpinner.play(); // waitingForFundsSpinner.play();
payFundsTitledGroupBg.setVisible(true); showFundingGroup();
totalToPayTextField.setVisible(true);
addressTextField.setVisible(true);
qrCodeImageView.setVisible(true);
balanceTextField.setVisible(true);
cancelButton2.setVisible(true);
reserveExactAmountSlider.setVisible(true);
} }
private void updateOfferElementsStyle() { private void updateOfferElementsStyle() {
@ -986,6 +978,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
gridPane.setVgap(5); gridPane.setVgap(5);
GUIUtil.setDefaultTwoColumnConstraintsForGridPane(gridPane); GUIUtil.setDefaultTwoColumnConstraintsForGridPane(gridPane);
scrollPane.setContent(gridPane); scrollPane.setContent(gridPane);
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
} }
private void addPaymentGroup() { private void addPaymentGroup() {
@ -1179,6 +1172,40 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
cancelButton1.setManaged(false); cancelButton1.setManaged(false);
} }
protected void hideFundingGroup() {
payFundsTitledGroupBg.setVisible(false);
payFundsTitledGroupBg.setManaged(false);
totalToPayTextField.setVisible(false);
totalToPayTextField.setManaged(false);
addressTextField.setVisible(false);
addressTextField.setManaged(false);
qrCodeImageView.setVisible(false);
qrCodeImageView.setManaged(false);
balanceTextField.setVisible(false);
balanceTextField.setManaged(false);
cancelButton2.setVisible(false);
cancelButton2.setManaged(false);
reserveExactAmountSlider.setVisible(false);
reserveExactAmountSlider.setManaged(false);
}
protected void showFundingGroup() {
payFundsTitledGroupBg.setVisible(true);
payFundsTitledGroupBg.setManaged(true);
totalToPayTextField.setVisible(true);
totalToPayTextField.setManaged(true);
addressTextField.setVisible(true);
addressTextField.setManaged(true);
qrCodeImageView.setVisible(true);
qrCodeImageView.setManaged(true);
balanceTextField.setVisible(true);
balanceTextField.setManaged(true);
cancelButton2.setVisible(true);
cancelButton2.setManaged(true);
reserveExactAmountSlider.setVisible(true);
reserveExactAmountSlider.setManaged(true);
}
private VBox getSecurityDepositBox() { private VBox getSecurityDepositBox() {
Tuple3<HBox, InfoInputTextField, Label> tuple = getEditableValueBoxWithInfo( Tuple3<HBox, InfoInputTextField, Label> tuple = getEditableValueBoxWithInfo(
Res.get("createOffer.securityDeposit.prompt")); Res.get("createOffer.securityDeposit.prompt"));
@ -1326,6 +1353,8 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
}); });
cancelButton2.setDefaultButton(false); cancelButton2.setDefaultButton(false);
cancelButton2.setVisible(false); cancelButton2.setVisible(false);
hideFundingGroup();
} }
private void openWallet() { private void openWallet() {

View File

@ -19,6 +19,8 @@ package haveno.desktop.main.offer;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.name.Named; import com.google.inject.name.Named;
import haveno.common.ThreadUtils;
import haveno.common.UserThread; import haveno.common.UserThread;
import haveno.common.app.DevEnv; import haveno.common.app.DevEnv;
import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ErrorMessageHandler;
@ -108,7 +110,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
private String amountDescription; private String amountDescription;
private String addressAsString; private String addressAsString;
private final String paymentLabel; private final String paymentLabel;
private boolean createOfferRequested; private boolean createOfferInProgress;
public boolean createOfferCanceled; public boolean createOfferCanceled;
public final StringProperty amount = new SimpleStringProperty(); public final StringProperty amount = new SimpleStringProperty();
@ -638,32 +640,37 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
void onPlaceOffer(Offer offer, Runnable resultHandler) { void onPlaceOffer(Offer offer, Runnable resultHandler) {
errorMessage.set(null); ThreadUtils.execute(() -> {
createOfferRequested = true;
createOfferCanceled = false;
dataModel.onPlaceOffer(offer, transaction -> {
resultHandler.run();
if (!createOfferCanceled) placeOfferCompleted.set(true);
errorMessage.set(null); errorMessage.set(null);
}, errMessage -> { createOfferInProgress = true;
createOfferRequested = false; createOfferCanceled = false;
if (offer.getState() == Offer.State.OFFER_FEE_RESERVED) errorMessage.set(errMessage + Res.get("createOffer.errorInfo"));
else errorMessage.set(errMessage); dataModel.onPlaceOffer(offer, transaction -> {
createOfferInProgress = false;
resultHandler.run();
if (!createOfferCanceled) placeOfferCompleted.set(true);
errorMessage.set(null);
}, errMessage -> {
createOfferInProgress = false;
if (offer.getState() == Offer.State.OFFER_FEE_RESERVED) errorMessage.set(errMessage + Res.get("createOffer.errorInfo"));
else errorMessage.set(errorMessage.get());
UserThread.execute(() -> {
updateButtonDisableState();
updateSpinnerInfo();
resultHandler.run();
});
});
UserThread.execute(() -> { UserThread.execute(() -> {
updateButtonDisableState(); updateButtonDisableState();
updateSpinnerInfo(); updateSpinnerInfo();
resultHandler.run();
}); });
}); }, getClass().getSimpleName());
updateButtonDisableState();
updateSpinnerInfo();
} }
public void onCancelOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { public void onCancelOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
createOfferRequested = false; log.info("Canceling posting offer {}", offer.getId());
createOfferCanceled = true; createOfferCanceled = true;
OpenOfferManager openOfferManager = HavenoUtils.openOfferManager; OpenOfferManager openOfferManager = HavenoUtils.openOfferManager;
Optional<OpenOffer> openOffer = openOfferManager.getOpenOffer(offer.getId()); Optional<OpenOffer> openOffer = openOfferManager.getOpenOffer(offer.getId());
@ -1355,7 +1362,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
inputDataValid = inputDataValid && getExtraInfoValidationResult().isValid; inputDataValid = inputDataValid && getExtraInfoValidationResult().isValid;
isNextButtonDisabled.set(!inputDataValid); isNextButtonDisabled.set(!inputDataValid);
isPlaceOfferButtonDisabled.set(createOfferRequested || !inputDataValid || !dataModel.getIsXmrWalletFunded().get()); isPlaceOfferButtonDisabled.set(createOfferInProgress || !inputDataValid || !dataModel.getIsXmrWalletFunded().get());
} }
private ValidationResult getExtraInfoValidationResult() { private ValidationResult getExtraInfoValidationResult() {

View File

@ -174,9 +174,11 @@ abstract class OfferBookViewModel extends ActivatableViewModel {
tradeCurrencyListChangeListener = c -> fillCurrencies(); tradeCurrencyListChangeListener = c -> fillCurrencies();
// refresh filter on changes // refresh filter on changes
offerBook.getOfferBookListItems().addListener((ListChangeListener<OfferBookListItem>) c -> { // TODO: This is removed because it's expensive to re-filter offers for every change (high cpu for many offers).
filterOffers(); // This was used to ensure offer list is fully refreshed, but is unnecessary after refactoring OfferBookService to clone offers?
}); // offerBook.getOfferBookListItems().addListener((ListChangeListener<OfferBookListItem>) c -> {
// filterOffers();
// });
filterItemsListener = c -> { filterItemsListener = c -> {
final Optional<OfferBookListItem> highestAmountOffer = filteredItems.stream() final Optional<OfferBookListItem> highestAmountOffer = filteredItems.stream()

View File

@ -91,7 +91,7 @@
<AutoTooltipButton fx:id="rescanOutputsButton"/> <AutoTooltipButton fx:id="rescanOutputsButton"/>
</VBox> </VBox>
<TitledGroupBg fx:id="p2pHeader" GridPane.rowIndex="5" GridPane.rowSpan="5"> <TitledGroupBg fx:id="p2pHeader" GridPane.rowIndex="5" GridPane.rowSpan="6">
<padding> <padding>
<Insets top="50.0"/> <Insets top="50.0"/>
</padding> </padding>
@ -159,7 +159,10 @@
<HavenoTextField fx:id="chainHeightTextField" GridPane.rowIndex="9" editable="false" <HavenoTextField fx:id="chainHeightTextField" GridPane.rowIndex="9" editable="false"
focusTraversable="false" labelFloat="true"/> focusTraversable="false" labelFloat="true"/>
<AutoTooltipButton fx:id="openTorSettingsButton" GridPane.rowIndex="10" GridPane.columnIndex="0"/> <HavenoTextField fx:id="minVersionForTrading" GridPane.rowIndex="10" editable="false"
focusTraversable="false" labelFloat="true"/>
<AutoTooltipButton fx:id="openTorSettingsButton" GridPane.rowIndex="11" GridPane.columnIndex="0"/>
<columnConstraints> <columnConstraints>
<ColumnConstraints hgrow="ALWAYS" minWidth="500"/> <ColumnConstraints hgrow="ALWAYS" minWidth="500"/>

View File

@ -75,7 +75,7 @@ public class NetworkSettingsView extends ActivatableView<GridPane, Void> {
@FXML @FXML
InputTextField xmrNodesInputTextField; InputTextField xmrNodesInputTextField;
@FXML @FXML
TextField onionAddress, sentDataTextField, receivedDataTextField, chainHeightTextField; TextField onionAddress, sentDataTextField, receivedDataTextField, chainHeightTextField, minVersionForTrading;
@FXML @FXML
Label p2PPeersLabel, moneroConnectionsLabel; Label p2PPeersLabel, moneroConnectionsLabel;
@FXML @FXML
@ -176,6 +176,7 @@ public class NetworkSettingsView extends ActivatableView<GridPane, Void> {
sentDataTextField.setPromptText(Res.get("settings.net.sentDataLabel")); sentDataTextField.setPromptText(Res.get("settings.net.sentDataLabel"));
receivedDataTextField.setPromptText(Res.get("settings.net.receivedDataLabel")); receivedDataTextField.setPromptText(Res.get("settings.net.receivedDataLabel"));
chainHeightTextField.setPromptText(Res.get("settings.net.chainHeightLabel")); chainHeightTextField.setPromptText(Res.get("settings.net.chainHeightLabel"));
minVersionForTrading.setPromptText(Res.get("filterWindow.disableTradeBelowVersion"));
roundTripTimeColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.roundTripTimeColumn"))); roundTripTimeColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.roundTripTimeColumn")));
sentBytesColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.sentBytesColumn"))); sentBytesColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.sentBytesColumn")));
receivedBytesColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.receivedBytesColumn"))); receivedBytesColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.receivedBytesColumn")));
@ -275,7 +276,7 @@ public class NetworkSettingsView extends ActivatableView<GridPane, Void> {
showShutDownPopup(); showShutDownPopup();
} }
}; };
filterPropertyListener = (observable, oldValue, newValue) -> applyPreventPublicXmrNetwork(); filterPropertyListener = (observable, oldValue, newValue) -> applyFilter();
// disable radio buttons if no nodes available // disable radio buttons if no nodes available
if (xmrNodes.getProvidedXmrNodes().isEmpty()) { if (xmrNodes.getProvidedXmrNodes().isEmpty()) {
@ -298,7 +299,7 @@ public class NetworkSettingsView extends ActivatableView<GridPane, Void> {
moneroPeersToggleGroup.selectedToggleProperty().addListener(moneroPeersToggleGroupListener); moneroPeersToggleGroup.selectedToggleProperty().addListener(moneroPeersToggleGroupListener);
if (filterManager.getFilter() != null) if (filterManager.getFilter() != null)
applyPreventPublicXmrNetwork(); applyFilter();
filterManager.filterProperty().addListener(filterPropertyListener); filterManager.filterProperty().addListener(filterPropertyListener);
@ -492,7 +493,9 @@ public class NetworkSettingsView extends ActivatableView<GridPane, Void> {
} }
private void applyPreventPublicXmrNetwork() { private void applyFilter() {
// prevent public xmr network
final boolean preventPublicXmrNetwork = isPreventPublicXmrNetwork(); final boolean preventPublicXmrNetwork = isPreventPublicXmrNetwork();
usePublicNodesRadio.setDisable(isPublicNodesDisabled()); usePublicNodesRadio.setDisable(isPublicNodesDisabled());
if (preventPublicXmrNetwork && selectedMoneroNodesOption == XmrNodes.MoneroNodesOption.PUBLIC) { if (preventPublicXmrNetwork && selectedMoneroNodesOption == XmrNodes.MoneroNodesOption.PUBLIC) {
@ -501,6 +504,10 @@ public class NetworkSettingsView extends ActivatableView<GridPane, Void> {
selectMoneroPeersToggle(); selectMoneroPeersToggle();
onMoneroPeersToggleSelected(false); onMoneroPeersToggleSelected(false);
} }
// set min version for trading
String minVersion = filterManager.getDisableTradeBelowVersion();
minVersionForTrading.textProperty().setValue(minVersion == null ? Res.get("shared.none") : minVersion);
} }
private boolean isPublicNodesDisabled() { private boolean isPublicNodesDisabled() {

View File

@ -194,6 +194,9 @@ The arbitrator is now registered and ready to accept requests for dispute resolu
1. Start the arbitrator's desktop application using the application launcher or e.g. `make arbitrator-desktop-mainnet` from the root of the repository. 1. Start the arbitrator's desktop application using the application launcher or e.g. `make arbitrator-desktop-mainnet` from the root of the repository.
2. Go to the `Account` tab and click the button to unregister the arbitrator. 2. Go to the `Account` tab and click the button to unregister the arbitrator.
> **Note**
> To preserve signed accounts, the arbitrator public key must remain in the repository, even after revoking.
## Set a network filter on mainnet ## Set a network filter on mainnet
On mainnet, the p2p network is expected to have a filter object for offers, onions, currencies, payment methods, etc. On mainnet, the p2p network is expected to have a filter object for offers, onions, currencies, payment methods, etc.

View File

@ -41,7 +41,7 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
public class SeedNodeMain extends ExecutableForAppWithP2p { public class SeedNodeMain extends ExecutableForAppWithP2p {
private static final long CHECK_CONNECTION_LOSS_SEC = 30; private static final long CHECK_CONNECTION_LOSS_SEC = 30;
private static final String VERSION = "1.0.19"; private static final String VERSION = "1.1.0";
private SeedNode seedNode; private SeedNode seedNode;
private Timer checkConnectionLossTime; private Timer checkConnectionLossTime;