Compare commits

..

21 Commits

Author SHA1 Message Date
woodser
de5250e89a persist trade with payment confirmation msgs before processing 2025-04-22 08:16:13 -04:00
woodser
923b3ad73b do not await updating trade state properties on trade thread 2025-04-22 08:16:13 -04:00
XMRZombie
9a14d5552e
Update tails script to expect installer instead of archive
Update haveno-install.sh
Archive extraction bypass, also renaming the filename to package_filename trough mv for keeping install.sh stable
2025-04-21 11:11:29 -04:00
woodser
a3d3f51f02
change popup from warning to error on take offer error 2025-04-21 10:29:06 -04:00
woodser
77429472f4
fix error popup when arbitrator nacks signing offer 2025-04-21 09:26:49 -04:00
woodser
38615edf86
re-arrange deployment sections and use markup for notes & warnings 2025-04-21 09:02:17 -04:00
woodser
cf9a37f295
improve error handling when clones taken at the same time 2025-04-19 22:28:32 -04:00
woodser
c7a3a9740f
fixes when cloned offers are taken at the same time 2025-04-19 16:54:01 -04:00
woodser
13e13d945d print pub key hex when missing for signature data 2025-04-18 17:30:49 -04:00
woodser
bfef0f9492 fix hanging while posting or canceling offer 2025-04-18 17:30:49 -04:00
woodser
39909e7936
bump version to 1.1.0 2025-04-17 20:55:01 -04:00
woodser
695f2b8dd3
fix startup error with localhost, support fallback from provided nodes 2025-04-17 19:58:12 -04:00
woodser
8f778be4d9
show scrollbar as needed when creating offer 2025-04-17 16:40:25 -04:00
woodser
821ef16d8f
refresh polling when key images added 2025-04-17 16:15:26 -04:00
woodser
58590d60df add arbitrator2 mainnet config to Makefile 2025-04-17 14:53:11 -04:00
woodser
8eccbcce43 skip offer signature validation for cloned offer until signed 2025-04-17 14:53:11 -04:00
woodser
bbfc5d5fed remove max version verification so arbitrators can be behind 2025-04-17 14:53:11 -04:00
woodser
22db354cb2 do not re-filter offers for every offer book change 2025-04-17 14:53:11 -04:00
woodser
60ceff6695 fix redundant key image notifications 2025-04-17 14:53:11 -04:00
woodser
c87b8a5b45
add note to keep arbitrator key in the repo to preserve signed accounts 2025-04-14 21:14:46 -04:00
woodser
bf055556f1
fix offers being deleted after minimum version update 2025-04-14 20:45:15 -04:00
33 changed files with 517 additions and 343 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());
}
fallbackApplied = true;
initializeConnections(); initializeConnections();
if (getConnection() == null) {
log.warn("No provided nodes available, falling back to public nodes");
fallbackToBestConnection();
}
}
} }
// ------------------------------- 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,9 +619,11 @@ 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())) {
MoneroRpcConnection connection = new MoneroRpcConnection(node.getHostNameOrAddress() + ":" + node.getPort()).setPriority(node.getPriority());
if (!connectionList.hasConnection(connection.getUri())) addConnection(connection); 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());
if (!connectionList.hasConnection(connection.getUri())) addConnection(connection); if (!connectionList.hasConnection(connection.getUri())) addConnection(connection);
@ -615,9 +634,11 @@ 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())) {
MoneroRpcConnection connection = new MoneroRpcConnection(node.getHostNameOrAddress() + ":" + node.getPort()).setPriority(node.getPriority());
addConnection(connection); 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());
addConnection(connection); addConnection(connection);
@ -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()) {
try {
log.info("Starting local node"); log.info("Starting local node");
xmrLocalNode.start(); 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,6 +1101,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
} else { } else {
// validate non-pending state // validate non-pending state
boolean skipValidation = openOffer.isDeactivated() && hasConflictingClone(openOffer) && openOffer.getOffer().getOfferPayload().getArbitratorSignature() == null; // clone with conflicting offer is deactivated and unsigned at first
if (!skipValidation) {
try { try {
validateSignedState(openOffer); validateSignedState(openOffer);
resultHandler.handleResult(null); // done processing if non-pending state is valid resultHandler.handleResult(null); // done processing if non-pending state is valid
@ -1114,6 +1116,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
if (openOffer.isAvailable()) openOffer.setState(OpenOffer.State.PENDING); if (openOffer.isAvailable()) openOffer.setState(OpenOffer.State.PENDING);
} }
} }
}
// sign and post offer if already funded // sign and post offer if already funded
if (openOffer.getReserveTxHash() != null) { if (openOffer.getReserveTxHash() != null) {
@ -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;
@ -810,6 +811,10 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
if (processModel.getTradeManager() != null) processModel.getTradeManager().requestPersistence(); if (processModel.getTradeManager() != null) processModel.getTradeManager().requestPersistence();
} }
public void persistNow(@Nullable Runnable completeHandler) {
processModel.getTradeManager().persistNow(completeHandler);
}
public TradeProtocol getProtocol() { public TradeProtocol getProtocol() {
return processModel.getTradeManager().getTradeProtocol(this); return processModel.getTradeManager().getTradeProtocol(this);
} }
@ -1608,11 +1613,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,25 +1771,31 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
} }
private void removeTradeOnError() { private void removeTradeOnError() {
synchronized (removeTradeOnErrorLock) {
// skip if already shut down or removed
if (isShutDown || !processModel.getTradeManager().hasTrade(getId())) return;
log.warn("removeTradeOnError() trade={}, tradeId={}, state={}", getClass().getSimpleName(), getShortId(), getState()); log.warn("removeTradeOnError() trade={}, tradeId={}, state={}", getClass().getSimpleName(), getShortId(), getState());
// force close and re-open wallet in case stuck // force close and re-open wallet in case stuck
forceCloseWallet(); forceCloseWallet();
if (isDepositRequested()) getWallet(); if (isDepositRequested()) getWallet();
// clear and shut down trade
onShutDownStarted();
clearAndShutDown();
// shut down trade thread // shut down trade thread
try { try {
ThreadUtils.shutDown(getId(), 1000l); ThreadUtils.shutDown(getId(), 5000l);
} catch (Exception e) { } catch (Exception e) {
log.warn("Error shutting down trade thread for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage()); log.warn("Error shutting down trade thread for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage());
} }
// clear and shut down trade
clearAndShutDown();
// unregister trade // unregister trade
processModel.getTradeManager().unregisterTrade(this); processModel.getTradeManager().unregisterTrade(this);
} }
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Model implementation // Model implementation
@ -1824,6 +1836,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
@ -1837,7 +1856,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
this.state = state; this.state = state;
requestPersistence(); requestPersistence();
UserThread.await(() -> { UserThread.execute(() -> {
stateProperty.set(state); stateProperty.set(state);
phaseProperty.set(state.getPhase()); phaseProperty.set(state.getPhase());
}); });
@ -1869,7 +1888,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
this.payoutState = payoutState; this.payoutState = payoutState;
requestPersistence(); requestPersistence();
UserThread.await(() -> payoutStateProperty.set(payoutState)); UserThread.execute(() -> payoutStateProperty.set(payoutState));
} }
public void setDisputeState(DisputeState disputeState) { public void setDisputeState(DisputeState disputeState) {
@ -2648,7 +2667,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 +2769,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

@ -546,6 +546,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
persistenceManager.requestPersistence(); persistenceManager.requestPersistence();
} }
public void persistNow(@Nullable Runnable completeHandler) {
persistenceManager.persistNow(completeHandler);
}
private void handleInitTradeRequest(InitTradeRequest request, NodeAddress sender) { private void handleInitTradeRequest(InitTradeRequest request, NodeAddress sender) {
log.info("TradeManager handling InitTradeRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); log.info("TradeManager handling InitTradeRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid());
@ -563,9 +567,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 +989,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 +997,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 +1282,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();
// tasks may complete successfully but process an error
if (trade.getInitError() == null) {
this.errorMessageHandler = null; // TODO: set this when trade state is >= DEPOSIT_PUBLISHED this.errorMessageHandler = null; // TODO: set this when trade state is >= DEPOSIT_PUBLISHED
handleTaskRunnerSuccess(sender, response); handleTaskRunnerSuccess(sender, response);
if (tradeResultHandler != null) tradeResultHandler.handleResult(trade); // trade is initialized 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);
@ -527,7 +537,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
// save message for reprocessing // save message for reprocessing
trade.getBuyer().setPaymentSentMessage(message); trade.getBuyer().setPaymentSentMessage(message);
trade.requestPersistence(); trade.persistNow(() -> {
// process message on trade thread // process message on trade thread
if (!trade.isInitialized() || trade.isShutDownStarted()) return; if (!trade.isInitialized() || trade.isShutDownStarted()) return;
@ -583,6 +593,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
awaitTradeLatch(); awaitTradeLatch();
} }
}, trade.getId()); }, trade.getId());
});
} }
// received by buyer and arbitrator // received by buyer and arbitrator
@ -609,7 +620,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
// save message for reprocessing // save message for reprocessing
trade.getSeller().setPaymentReceivedMessage(message); trade.getSeller().setPaymentReceivedMessage(message);
trade.requestPersistence(); trade.persistNow(() -> {
// process message on trade thread // process message on trade thread
if (!trade.isInitialized() || trade.isShutDownStarted()) return; if (!trade.isInitialized() || trade.isShutDownStarted()) return;
@ -662,6 +673,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
awaitTradeLatch(); awaitTradeLatch();
} }
}, trade.getId()); }, trade.getId());
});
} }
public void onWithdrawCompleted() { public void onWithdrawCompleted() {
@ -832,7 +844,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);
// notify balance update // play sound if funds received
UserThread.execute(() -> {
// check if funds received
boolean fundsReceived = balanceSumBefore != null && getNonTradeBalanceSum().compareTo(balanceSumBefore) > 0; boolean fundsReceived = balanceSumBefore != null && getNonTradeBalanceSum().compareTo(balanceSumBefore) > 0;
if (fundsReceived) { if (fundsReceived) HavenoUtils.playCashRegisterSound();
HavenoUtils.playCashRegisterSound();
}
// increase counter to notify listeners // notify balance update
updateCounter.set(updateCounter.get() + 1); 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,16 +640,18 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
void onPlaceOffer(Offer offer, Runnable resultHandler) { void onPlaceOffer(Offer offer, Runnable resultHandler) {
ThreadUtils.execute(() -> {
errorMessage.set(null); errorMessage.set(null);
createOfferRequested = true; createOfferInProgress = true;
createOfferCanceled = false; createOfferCanceled = false;
dataModel.onPlaceOffer(offer, transaction -> { dataModel.onPlaceOffer(offer, transaction -> {
createOfferInProgress = false;
resultHandler.run(); resultHandler.run();
if (!createOfferCanceled) placeOfferCompleted.set(true); if (!createOfferCanceled) placeOfferCompleted.set(true);
errorMessage.set(null); errorMessage.set(null);
}, errMessage -> { }, errMessage -> {
createOfferRequested = false; createOfferInProgress = false;
if (offer.getState() == Offer.State.OFFER_FEE_RESERVED) errorMessage.set(errMessage + Res.get("createOffer.errorInfo")); if (offer.getState() == Offer.State.OFFER_FEE_RESERVED) errorMessage.set(errMessage + Res.get("createOffer.errorInfo"));
else errorMessage.set(errMessage); else errorMessage.set(errMessage);
@ -658,12 +662,15 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
}); });
}); });
UserThread.execute(() -> {
updateButtonDisableState(); updateButtonDisableState();
updateSpinnerInfo(); updateSpinnerInfo();
});
}, getClass().getSimpleName());
} }
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

@ -649,7 +649,7 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
errorMessageSubscription = EasyBind.subscribe(model.errorMessage, newValue -> { errorMessageSubscription = EasyBind.subscribe(model.errorMessage, newValue -> {
if (newValue != null) { if (newValue != null) {
new Popup().warning(Res.get("takeOffer.error.message", model.errorMessage.get())) new Popup().error(Res.get("takeOffer.error.message", model.errorMessage.get()))
.onClose(() -> { .onClose(() -> {
errorPopupDisplayed.set(true); errorPopupDisplayed.set(true);
model.resetErrorMessage(); model.resetErrorMessage();

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

@ -5,10 +5,11 @@ This guide describes how to deploy a Haveno network:
- Manage services on a VPS - Manage services on a VPS
- Fork and build Haveno - Fork and build Haveno
- Start a Monero node - Start a Monero node
- Build and start price nodes
- Add seed nodes - Add seed nodes
- Add arbitrators - Add arbitrators
- Configure trade fees and other configuration - Configure trade fees and other configuration
- Build and start price nodes
- Set a network filter
- Build Haveno installers for distribution - Build Haveno installers for distribution
- Send alerts to update the application and other maintenance - Send alerts to update the application and other maintenance
@ -69,14 +70,6 @@ Optionally customize and deploy monero-stagenet.service and monero-stagenet.conf
You can also start the Monero node in your current terminal session by running `make monerod` for mainnet or `make monerod-stagenet` for stagenet. You can also start the Monero node in your current terminal session by running `make monerod` for mainnet or `make monerod-stagenet` for stagenet.
## Build and start price nodes
The price node is separated from Haveno and is run as a standalone service. To deploy a pricenode on both TOR and clearnet, see the instructions on the repository: https://github.com/haveno-dex/haveno-pricenode.
After the price node is built and deployed, add the price node to `DEFAULT_NODES` in [ProvidersRepository.java](https://github.com/haveno-dex/haveno/blob/3cdd88b56915c7f8afd4f1a39e6c1197c2665d63/core/src/main/java/haveno/core/provider/ProvidersRepository.java#L50).
Customize and deploy haveno-pricenode.env and haveno-pricenode.service to run as a system service.
## Add seed nodes ## Add seed nodes
### Seed nodes without Proof of Work (PoW) ### Seed nodes without Proof of Work (PoW)
@ -139,7 +132,7 @@ Each seed node requires a locally running Monero node. You can use the default p
Rebuild all seed nodes any time the list of registered seed nodes changes. Rebuild all seed nodes any time the list of registered seed nodes changes.
> **Notes** > [!note]
> * Avoid all seed nodes going offline at the same time. If all seed nodes go offline at the same time, the network will be reset, including registered arbitrators, the network filter object, and trade history. In that case, arbitrators need to restart or re-register, and the network filter object needs to be re-applied. This should be done immediately or clients will cancel their offers due to the signing arbitrators being unregistered and no replacements being available to re-sign. > * Avoid all seed nodes going offline at the same time. If all seed nodes go offline at the same time, the network will be reset, including registered arbitrators, the network filter object, and trade history. In that case, arbitrators need to restart or re-register, and the network filter object needs to be re-applied. This should be done immediately or clients will cancel their offers due to the signing arbitrators being unregistered and no replacements being available to re-sign.
> * At least 2 seed nodes should be run because the seed nodes restart once per day. > * At least 2 seed nodes should be run because the seed nodes restart once per day.
@ -180,32 +173,21 @@ For each arbitrator:
The arbitrator is now registered and ready to accept requests for dispute resolution. The arbitrator is now registered and ready to accept requests for dispute resolution.
**Notes** > [!note]
- Arbitrators must use a local Monero node with unrestricted RPC in order to submit and flush transactions from the pool. > * Arbitrators must use a local Monero node with unrestricted RPC in order to submit and flush transactions from the pool.
- Arbitrators should remain online as much as possible in order to balance trades and avoid clients spending time trying to contact offline arbitrators. A VPS or dedicated machine running 24/7 is highly recommended. > * Arbitrators should remain online as much as possible in order to balance trades and avoid clients spending time trying to contact offline arbitrators. A VPS or dedicated machine running 24/7 is highly recommended.
- Remember that for the network to run correctly and people to be able to open and accept trades, at least one arbitrator must be registered on the network. > * Remember that for the network to run correctly and people to be able to open and accept trades, at least one arbitrator must be registered on the network.
- IMPORTANT: Do not reuse keypairs on multiple arbitrator instances. > * IMPORTANT: Do not reuse keypairs on multiple arbitrator instances.
## Remove an arbitrator ## Remove an arbitrator
> **Note** > [!warning]
> Ensure the arbitrator's trades are completed before retiring the instance. > * Ensure the arbitrator's trades are completed before retiring the instance.
> * To preserve signed accounts, the arbitrator public key must remain in the repository, even after revoking.
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.
## 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.
To set the network's filter object:
1. Enter `ctrl + f` in the arbitrator or other Haveno instance to open the Filter window.
2. Enter a developer private key from the previous steps and click "Add Filter" to register.
> **Note**
> If all seed nodes are restarted at the same time, arbitrators and the filter object will become unregistered and will need to be re-registered.
## Change the default folder name for Haveno application data ## Change the default folder name for Haveno application data
To avoid user data corruption when using multiple Haveno networks, change the default folder name for Haveno's application data on your network: To avoid user data corruption when using multiple Haveno networks, change the default folder name for Haveno's application data on your network:
@ -243,10 +225,30 @@ Set `ARBITRATOR_ASSIGNS_TRADE_FEE_ADDRESS` to `true` for the arbitrator to assig
Otherwise set `ARBITRATOR_ASSIGNS_TRADE_FEE_ADDRESS` to `false` and set the XMR address in `getGlobalTradeFeeAddress()` to collect all trade fees to a single address (e.g. a multisig wallet shared among network administrators). Otherwise set `ARBITRATOR_ASSIGNS_TRADE_FEE_ADDRESS` to `false` and set the XMR address in `getGlobalTradeFeeAddress()` to collect all trade fees to a single address (e.g. a multisig wallet shared among network administrators).
## Build and start price nodes
The price node is separated from Haveno and is run as a standalone service. To deploy a pricenode on both TOR and clearnet, see the instructions on the repository: https://github.com/haveno-dex/haveno-pricenode.
After the price node is built and deployed, add the price node to `DEFAULT_NODES` in [ProvidersRepository.java](https://github.com/haveno-dex/haveno/blob/3cdd88b56915c7f8afd4f1a39e6c1197c2665d63/core/src/main/java/haveno/core/provider/ProvidersRepository.java#L50).
Customize and deploy haveno-pricenode.env and haveno-pricenode.service to run as a system service.
## Update the download URL ## Update the download URL
Change every instance of `https://haveno.exchange/downloads` to your download URL. For example, `https://havenoexample.com/downloads`. Change every instance of `https://haveno.exchange/downloads` to your download URL. For example, `https://havenoexample.com/downloads`.
## 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.
To set the network's filter object:
1. Enter `ctrl + f` in the arbitrator or other Haveno instance to open the Filter window.
2. Enter a developer private key from the previous steps and click "Add Filter" to register.
> [!note]
> If all seed nodes are restarted at the same time, arbitrators and the filter object will become unregistered and will need to be re-registered.
## Start users for testing ## Start users for testing
Start user1 on Monero's mainnet using `make user1-desktop-mainnet` or Monero's stagenet using `make user1-desktop-stagenet`. Start user1 on Monero's mainnet using `make user1-desktop-mainnet` or Monero's stagenet using `make user1-desktop-stagenet`.

View File

@ -124,7 +124,7 @@ OUTPUT=$(gpg --digest-algo SHA256 --verify "${signature_filename}" "${binary_fil
if ! echo "$OUTPUT" | grep -q "Good signature from"; then if ! echo "$OUTPUT" | grep -q "Good signature from"; then
echo_red "Verification failed: $OUTPUT" echo_red "Verification failed: $OUTPUT"
exit 1; exit 1;
else 7z x "${binary_filename}" && mv haveno*.deb "${package_filename}" else mv -f "${binary_filename}" "${package_filename}"
fi fi
echo_blue "Haveno binaries have been successfully verified." echo_blue "Haveno binaries have been successfully verified."
@ -136,7 +136,7 @@ mkdir -p "${install_dir}"
# Delete old Haveno binaries # Delete old Haveno binaries
#rm -f "${install_dir}/"*.deb* #rm -f "${install_dir}/"*.deb*
mv "${binary_filename}" "${package_filename}" "${key_filename}" "${signature_filename}" "${install_dir}" mv "${package_filename}" "${key_filename}" "${signature_filename}" "${install_dir}"
echo_blue "Files moved to persistent directory ${install_dir}" echo_blue "Files moved to persistent directory ${install_dir}"

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;