mirror of
https://github.com/haveno-dex/haveno.git
synced 2025-04-22 16:39:23 -04:00
Compare commits
21 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
de5250e89a | ||
![]() |
923b3ad73b | ||
![]() |
9a14d5552e | ||
![]() |
a3d3f51f02 | ||
![]() |
77429472f4 | ||
![]() |
38615edf86 | ||
![]() |
cf9a37f295 | ||
![]() |
c7a3a9740f | ||
![]() |
13e13d945d | ||
![]() |
bfef0f9492 | ||
![]() |
39909e7936 | ||
![]() |
695f2b8dd3 | ||
![]() |
8f778be4d9 | ||
![]() |
821ef16d8f | ||
![]() |
58590d60df | ||
![]() |
8eccbcce43 | ||
![]() |
bbfc5d5fed | ||
![]() |
22db354cb2 | ||
![]() |
60ceff6695 | ||
![]() |
c87b8a5b45 | ||
![]() |
bf055556f1 |
25
Makefile
25
Makefile
@ -485,6 +485,31 @@ arbitrator-desktop-mainnet:
|
||||
--xmrNode=http://127.0.0.1:18081 \
|
||||
--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$(APP_EXT) \
|
||||
--baseCurrencyNetwork=XMR_MAINNET \
|
||||
|
@ -610,7 +610,7 @@ configure(project(':desktop')) {
|
||||
apply plugin: 'com.github.johnrengelman.shadow'
|
||||
apply from: 'package/package.gradle'
|
||||
|
||||
version = '1.0.19-SNAPSHOT'
|
||||
version = '1.1.0-SNAPSHOT'
|
||||
|
||||
jar.manifest.attributes(
|
||||
"Implementation-Title": project.name,
|
||||
|
@ -28,7 +28,7 @@ import static com.google.common.base.Preconditions.checkArgument;
|
||||
public class Version {
|
||||
// The application versions
|
||||
// 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.
|
||||
|
@ -335,12 +335,13 @@ public class SignedWitnessService {
|
||||
String message = Utilities.encodeToHex(signedWitness.getAccountAgeWitnessHash());
|
||||
String signatureBase64 = new String(signedWitness.getSignature(), Charsets.UTF_8);
|
||||
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);
|
||||
verifySignatureWithECKeyResultCache.put(hash, true);
|
||||
return true;
|
||||
} 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);
|
||||
return false;
|
||||
}
|
||||
|
@ -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_REMOTE = 300000; // 5 minutes
|
||||
|
||||
public enum XmrConnectionError {
|
||||
public enum XmrConnectionFallbackType {
|
||||
LOCAL,
|
||||
CUSTOM
|
||||
CUSTOM,
|
||||
PROVIDED
|
||||
}
|
||||
|
||||
private final Object lock = new Object();
|
||||
@ -97,7 +98,7 @@ public final class XmrConnectionService {
|
||||
private final LongProperty chainHeight = new SimpleLongProperty(0);
|
||||
private final DownloadListener downloadListener = new DownloadListener();
|
||||
@Getter
|
||||
private final ObjectProperty<XmrConnectionError> connectionServiceError = new SimpleObjectProperty<>();
|
||||
private final ObjectProperty<XmrConnectionFallbackType> connectionServiceFallbackType = new SimpleObjectProperty<>();
|
||||
@Getter
|
||||
private final StringProperty connectionServiceErrorMsg = new SimpleStringProperty();
|
||||
private final LongProperty numUpdates = new SimpleLongProperty(0);
|
||||
@ -129,6 +130,7 @@ public final class XmrConnectionService {
|
||||
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 boolean fallbackApplied;
|
||||
private boolean usedSyncingLocalNodeBeforeStartup;
|
||||
|
||||
@Inject
|
||||
public XmrConnectionService(P2PService p2PService,
|
||||
@ -156,7 +158,13 @@ public final class XmrConnectionService {
|
||||
p2PService.addP2PServiceListener(new P2PServiceListener() {
|
||||
@Override
|
||||
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
|
||||
public void onHiddenServicePublished() {}
|
||||
@ -270,7 +278,7 @@ public final class XmrConnectionService {
|
||||
accountService.checkAccountOpen();
|
||||
|
||||
// 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");
|
||||
return null;
|
||||
}
|
||||
@ -283,6 +291,10 @@ public final class XmrConnectionService {
|
||||
return bestConnection;
|
||||
}
|
||||
|
||||
private boolean fallbackRequiredBeforeConnectionSwitch() {
|
||||
return lastInfo == null && !fallbackApplied && usedSyncingLocalNodeBeforeStartup && (!xmrLocalNode.isDetected() || xmrLocalNode.shouldBeIgnored());
|
||||
}
|
||||
|
||||
private void addLocalNodeIfIgnored(Collection<MoneroRpcConnection> ignoredConnections) {
|
||||
if (xmrLocalNode.shouldBeIgnored() && connectionManager.hasConnection(xmrLocalNode.getUri())) ignoredConnections.add(connectionManager.getConnectionByUri(xmrLocalNode.getUri()));
|
||||
}
|
||||
@ -458,15 +470,20 @@ public final class XmrConnectionService {
|
||||
|
||||
public void fallbackToBestConnection() {
|
||||
if (isShutDownStarted) return;
|
||||
if (xmrNodes.getProvidedXmrNodes().isEmpty()) {
|
||||
fallbackApplied = true;
|
||||
if (isProvidedConnections() || xmrNodes.getProvidedXmrNodes().isEmpty()) {
|
||||
log.warn("Falling back to public nodes");
|
||||
preferences.setMoneroNodesOptionOrdinal(XmrNodes.MoneroNodesOption.PUBLIC.ordinal());
|
||||
initializeConnections();
|
||||
} else {
|
||||
log.warn("Falling back to provided nodes");
|
||||
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 --------------------------------
|
||||
@ -578,8 +595,8 @@ public final class XmrConnectionService {
|
||||
setConnection(connection.getUri());
|
||||
|
||||
// reset error connecting to local node
|
||||
if (connectionServiceError.get() == XmrConnectionError.LOCAL && isConnectionLocalHost()) {
|
||||
connectionServiceError.set(null);
|
||||
if (connectionServiceFallbackType.get() == XmrConnectionFallbackType.LOCAL && isConnectionLocalHost()) {
|
||||
connectionServiceFallbackType.set(null);
|
||||
}
|
||||
} else if (getConnection() != null && getConnection().getUri().equals(connection.getUri())) {
|
||||
MoneroRpcConnection bestConnection = getBestConnection();
|
||||
@ -602,8 +619,10 @@ public final class XmrConnectionService {
|
||||
// add default connections
|
||||
for (XmrNode node : xmrNodes.getAllXmrNodes()) {
|
||||
if (node.hasClearNetAddress()) {
|
||||
MoneroRpcConnection connection = new MoneroRpcConnection(node.getAddress() + ":" + node.getPort()).setPriority(node.getPriority());
|
||||
if (!connectionList.hasConnection(connection.getUri())) addConnection(connection);
|
||||
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 (node.hasOnionAddress()) {
|
||||
MoneroRpcConnection connection = new MoneroRpcConnection(node.getOnionAddress() + ":" + node.getPort()).setPriority(node.getPriority());
|
||||
@ -615,8 +634,10 @@ public final class XmrConnectionService {
|
||||
// add default connections
|
||||
for (XmrNode node : xmrNodes.selectPreferredNodes(new XmrNodesSetupPreferences(preferences))) {
|
||||
if (node.hasClearNetAddress()) {
|
||||
MoneroRpcConnection connection = new MoneroRpcConnection(node.getAddress() + ":" + node.getPort()).setPriority(node.getPriority());
|
||||
addConnection(connection);
|
||||
if (!xmrLocalNode.shouldBeIgnored() || !xmrLocalNode.equalsUri(node.getClearNetUri())) {
|
||||
MoneroRpcConnection connection = new MoneroRpcConnection(node.getHostNameOrAddress() + ":" + node.getPort()).setPriority(node.getPriority());
|
||||
addConnection(connection);
|
||||
}
|
||||
}
|
||||
if (node.hasOnionAddress()) {
|
||||
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
|
||||
log.info("TOR proxy URI: " + getProxyUri());
|
||||
for (MoneroRpcConnection connection : connectionManager.getConnections()) {
|
||||
@ -666,29 +692,16 @@ public final class XmrConnectionService {
|
||||
onConnectionChanged(connectionManager.getConnection());
|
||||
}
|
||||
|
||||
private boolean lastUsedLocalSyncingNode() {
|
||||
return connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored();
|
||||
}
|
||||
|
||||
public void startLocalNode() {
|
||||
public void startLocalNode() throws Exception {
|
||||
|
||||
// cannot start local node as seed node
|
||||
if (HavenoUtils.isSeedNode()) {
|
||||
throw new RuntimeException("Cannot start local node on seed node");
|
||||
}
|
||||
|
||||
// start local node if offline and used as last connection
|
||||
if (connectionManager.getConnection() != null && xmrLocalNode.equalsUri(connectionManager.getConnection().getUri()) && !xmrLocalNode.isDetected() && !xmrLocalNode.shouldBeIgnored()) {
|
||||
try {
|
||||
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");
|
||||
}
|
||||
// start local node
|
||||
log.info("Starting local node");
|
||||
xmrLocalNode.start();
|
||||
}
|
||||
|
||||
private void onConnectionChanged(MoneroRpcConnection currentConnection) {
|
||||
@ -768,7 +781,7 @@ public final class XmrConnectionService {
|
||||
try {
|
||||
|
||||
// poll daemon
|
||||
if (daemon == null) switchToBestConnection();
|
||||
if (daemon == null && !fallbackRequiredBeforeConnectionSwitch()) switchToBestConnection();
|
||||
try {
|
||||
if (daemon == null) throw new RuntimeException("No connection to Monero daemon");
|
||||
lastInfo = daemon.getInfo();
|
||||
@ -778,16 +791,19 @@ public final class XmrConnectionService {
|
||||
if (isShutDownStarted) return;
|
||||
|
||||
// invoke fallback handling on startup error
|
||||
boolean canFallback = isFixedConnection() || isCustomConnections() || lastUsedLocalSyncingNode();
|
||||
boolean canFallback = isFixedConnection() || isProvidedConnections() || isCustomConnections() || usedSyncingLocalNodeBeforeStartup;
|
||||
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();
|
||||
if (lastUsedLocalSyncingNode()) {
|
||||
if (usedSyncingLocalNodeBeforeStartup) {
|
||||
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 {
|
||||
log.warn("Failed to fetch daemon info from custom connection on startup: " + e.getMessage());
|
||||
connectionServiceError.set(XmrConnectionError.CUSTOM);
|
||||
connectionServiceFallbackType.set(XmrConnectionFallbackType.CUSTOM);
|
||||
}
|
||||
}
|
||||
return;
|
||||
@ -808,7 +824,7 @@ public final class XmrConnectionService {
|
||||
|
||||
// connected to daemon
|
||||
isConnected = true;
|
||||
connectionServiceError.set(null);
|
||||
connectionServiceFallbackType.set(null);
|
||||
|
||||
// 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
|
||||
@ -885,10 +901,14 @@ public final class XmrConnectionService {
|
||||
}
|
||||
|
||||
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() {
|
||||
return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.CUSTOM;
|
||||
}
|
||||
|
||||
private boolean isProvidedConnections() {
|
||||
return preferences.getMoneroNodesOption() == XmrNodes.MoneroNodesOption.PROVIDED;
|
||||
}
|
||||
}
|
||||
|
@ -109,17 +109,18 @@ public class XmrLocalNode {
|
||||
public boolean shouldBeIgnored() {
|
||||
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;
|
||||
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;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasConfiguredLocalNode) return true;
|
||||
|
||||
return false;
|
||||
return !hasConfiguredLocalNode;
|
||||
}
|
||||
|
||||
public void addListener(XmrLocalNodeListener listener) {
|
||||
|
@ -75,7 +75,7 @@ public class HavenoHeadlessApp implements HeadlessApp {
|
||||
log.info("onDisplayTacHandler: We accept the tacs automatically in headless mode");
|
||||
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.setChainFileLockedExceptionHandler(msg -> log.error("onChainFileLockedExceptionHandler: msg={}", msg));
|
||||
tradeManager.setLockedUpFundsHandler(msg -> log.info("onLockedUpFundsHandler: msg={}", msg));
|
||||
|
@ -55,7 +55,7 @@ import haveno.core.alert.PrivateNotificationManager;
|
||||
import haveno.core.alert.PrivateNotificationPayload;
|
||||
import haveno.core.api.CoreContext;
|
||||
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.locale.Res;
|
||||
import haveno.core.offer.OpenOfferManager;
|
||||
@ -159,7 +159,7 @@ public class HavenoSetup {
|
||||
rejectedTxErrorMessageHandler;
|
||||
@Setter
|
||||
@Nullable
|
||||
private Consumer<XmrConnectionError> displayMoneroConnectionErrorHandler;
|
||||
private Consumer<XmrConnectionFallbackType> displayMoneroConnectionFallbackHandler;
|
||||
@Setter
|
||||
@Nullable
|
||||
private Consumer<Boolean> displayTorNetworkSettingsHandler;
|
||||
@ -431,9 +431,9 @@ public class HavenoSetup {
|
||||
getXmrWalletSyncProgress().addListener((observable, oldValue, newValue) -> resetStartupTimeout());
|
||||
|
||||
// listen for fallback handling
|
||||
getConnectionServiceError().addListener((observable, oldValue, newValue) -> {
|
||||
if (displayMoneroConnectionErrorHandler == null) return;
|
||||
displayMoneroConnectionErrorHandler.accept(newValue);
|
||||
getConnectionServiceFallbackType().addListener((observable, oldValue, newValue) -> {
|
||||
if (displayMoneroConnectionFallbackHandler == null) return;
|
||||
displayMoneroConnectionFallbackHandler.accept(newValue);
|
||||
});
|
||||
|
||||
log.info("Init P2P network");
|
||||
@ -735,8 +735,8 @@ public class HavenoSetup {
|
||||
return xmrConnectionService.getConnectionServiceErrorMsg();
|
||||
}
|
||||
|
||||
public ObjectProperty<XmrConnectionError> getConnectionServiceError() {
|
||||
return xmrConnectionService.getConnectionServiceError();
|
||||
public ObjectProperty<XmrConnectionFallbackType> getConnectionServiceFallbackType() {
|
||||
return xmrConnectionService.getConnectionServiceFallbackType();
|
||||
}
|
||||
|
||||
public StringProperty getTopErrorMsg() {
|
||||
|
@ -149,6 +149,20 @@ public class OfferBookService {
|
||||
Offer offer = new Offer(offerPayload);
|
||||
offer.setPriceFeedService(priceFeedService);
|
||||
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());
|
||||
@ -298,20 +312,6 @@ public class OfferBookService {
|
||||
synchronized (offerBookChangedListeners) {
|
||||
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) {
|
||||
|
@ -1101,17 +1101,20 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
} else {
|
||||
|
||||
// validate non-pending state
|
||||
try {
|
||||
validateSignedState(openOffer);
|
||||
resultHandler.handleResult(null); // done processing if non-pending state is valid
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
log.warn(e.getMessage());
|
||||
boolean skipValidation = openOffer.isDeactivated() && hasConflictingClone(openOffer) && openOffer.getOffer().getOfferPayload().getArbitratorSignature() == null; // clone with conflicting offer is deactivated and unsigned at first
|
||||
if (!skipValidation) {
|
||||
try {
|
||||
validateSignedState(openOffer);
|
||||
resultHandler.handleResult(null); // done processing if non-pending state is valid
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
log.warn(e.getMessage());
|
||||
|
||||
// reset arbitrator signature
|
||||
openOffer.getOffer().getOfferPayload().setArbitratorSignature(null);
|
||||
openOffer.getOffer().getOfferPayload().setArbitratorSigner(null);
|
||||
if (openOffer.isAvailable()) openOffer.setState(OpenOffer.State.PENDING);
|
||||
// reset arbitrator signature
|
||||
openOffer.getOffer().getOfferPayload().setArbitratorSignature(null);
|
||||
openOffer.getOffer().getOfferPayload().setArbitratorSigner(null);
|
||||
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
|
||||
boolean hasBuyerAsTakerWithoutDeposit = offer.getDirection() == OfferDirection.SELL && offer.isPrivateOffer() && offer.getChallengeHash() != null && offer.getChallengeHash().length() > 0 && offer.getTakerFeePct() == 0;
|
||||
if (hasBuyerAsTakerWithoutDeposit) {
|
||||
@ -2023,7 +2018,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
originalOfferPayload.getAcceptedCountryCodes(),
|
||||
originalOfferPayload.getBankId(),
|
||||
originalOfferPayload.getAcceptedBankIds(),
|
||||
originalOfferPayload.getVersionNr(),
|
||||
Version.VERSION,
|
||||
originalOfferPayload.getBlockHeightAtOfferCreation(),
|
||||
originalOfferPayload.getMaxTradeLimit(),
|
||||
originalOfferPayload.getMaxTradePeriod(),
|
||||
|
@ -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 int NUM_CONFIRMATIONS_FOR_SCHEDULED_IMPORT = 5;
|
||||
protected final Object pollLock = new Object();
|
||||
private final Object removeTradeOnErrorLock = new Object();
|
||||
protected static final Object importMultisigLock = new Object();
|
||||
private boolean pollInProgress;
|
||||
private boolean restartInProgress;
|
||||
@ -810,6 +811,10 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
if (processModel.getTradeManager() != null) processModel.getTradeManager().requestPersistence();
|
||||
}
|
||||
|
||||
public void persistNow(@Nullable Runnable completeHandler) {
|
||||
processModel.getTradeManager().persistNow(completeHandler);
|
||||
}
|
||||
|
||||
public TradeProtocol getProtocol() {
|
||||
return processModel.getTradeManager().getTradeProtocol(this);
|
||||
}
|
||||
@ -1608,11 +1613,12 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
}
|
||||
|
||||
// shut down trade threads
|
||||
isInitialized = false;
|
||||
isShutDown = true;
|
||||
List<Runnable> shutDownThreads = new ArrayList<>();
|
||||
shutDownThreads.add(() -> ThreadUtils.shutDown(getId()));
|
||||
ThreadUtils.awaitTasks(shutDownThreads);
|
||||
stopProtocolTimeout();
|
||||
isInitialized = false;
|
||||
|
||||
// save and close
|
||||
if (wallet != null) {
|
||||
@ -1765,24 +1771,30 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
}
|
||||
|
||||
private void removeTradeOnError() {
|
||||
log.warn("removeTradeOnError() trade={}, tradeId={}, state={}", getClass().getSimpleName(), getShortId(), getState());
|
||||
synchronized (removeTradeOnErrorLock) {
|
||||
|
||||
// force close and re-open wallet in case stuck
|
||||
forceCloseWallet();
|
||||
if (isDepositRequested()) getWallet();
|
||||
// skip if already shut down or removed
|
||||
if (isShutDown || !processModel.getTradeManager().hasTrade(getId())) return;
|
||||
log.warn("removeTradeOnError() trade={}, tradeId={}, state={}", getClass().getSimpleName(), getShortId(), getState());
|
||||
|
||||
// shut down trade thread
|
||||
try {
|
||||
ThreadUtils.shutDown(getId(), 1000l);
|
||||
} catch (Exception e) {
|
||||
log.warn("Error shutting down trade thread for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage());
|
||||
// force close and re-open wallet in case stuck
|
||||
forceCloseWallet();
|
||||
if (isDepositRequested()) getWallet();
|
||||
|
||||
// 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 +1836,13 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
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) {
|
||||
if (isInitialized) {
|
||||
// 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;
|
||||
requestPersistence();
|
||||
UserThread.await(() -> {
|
||||
UserThread.execute(() -> {
|
||||
stateProperty.set(state);
|
||||
phaseProperty.set(state.getPhase());
|
||||
});
|
||||
@ -1869,7 +1888,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
|
||||
this.payoutState = payoutState;
|
||||
requestPersistence();
|
||||
UserThread.await(() -> payoutStateProperty.set(payoutState));
|
||||
UserThread.execute(() -> payoutStateProperty.set(payoutState));
|
||||
}
|
||||
|
||||
public void setDisputeState(DisputeState disputeState) {
|
||||
@ -2648,7 +2667,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||
}
|
||||
}
|
||||
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();
|
||||
|
||||
// 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() {
|
||||
synchronized (walletLock) {
|
||||
if (isWalletBehind()) {
|
||||
|
@ -546,6 +546,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
persistenceManager.requestPersistence();
|
||||
}
|
||||
|
||||
public void persistNow(@Nullable Runnable completeHandler) {
|
||||
persistenceManager.persistNow(completeHandler);
|
||||
}
|
||||
|
||||
private void handleInitTradeRequest(InitTradeRequest request, NodeAddress sender) {
|
||||
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());
|
||||
if (!openOfferOptional.isPresent()) return;
|
||||
OpenOffer openOffer = openOfferOptional.get();
|
||||
if (openOffer.getState() != OpenOffer.State.AVAILABLE) return;
|
||||
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
|
||||
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);
|
||||
@ -980,9 +989,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
closedTradableManager.add(trade);
|
||||
trade.setCompleted(true);
|
||||
removeTrade(trade, true);
|
||||
|
||||
// TODO The address entry should have been removed already. Check and if its the case remove that.
|
||||
xmrWalletService.swapPayoutAddressEntryToAvailable(trade.getId());
|
||||
xmrWalletService.swapPayoutAddressEntryToAvailable(trade.getId()); // TODO The address entry should have been removed already. Check and if its the case remove that.
|
||||
requestPersistence();
|
||||
}
|
||||
|
||||
@ -990,6 +997,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
log.warn("Unregistering {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
removeTrade(trade, true);
|
||||
removeFailedTrade(trade);
|
||||
xmrWalletService.swapPayoutAddressEntryToAvailable(trade.getId()); // TODO The address entry should have been removed already. Check and if its the case remove that.
|
||||
requestPersistence();
|
||||
}
|
||||
|
||||
@ -1274,11 +1282,15 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
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) {
|
||||
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) {
|
||||
synchronized (tradableList.getList()) {
|
||||
return tradableList.stream().filter(e -> e.getId().equals(tradeId)).findFirst();
|
||||
|
@ -460,9 +460,19 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||
.using(new TradeTaskRunner(trade,
|
||||
() -> {
|
||||
stopTimeout();
|
||||
this.errorMessageHandler = null; // TODO: set this when trade state is >= DEPOSIT_PUBLISHED
|
||||
handleTaskRunnerSuccess(sender, response);
|
||||
if (tradeResultHandler != null) tradeResultHandler.handleResult(trade); // trade is initialized
|
||||
|
||||
// tasks may complete successfully but process an error
|
||||
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 -> {
|
||||
handleTaskRunnerFault(sender, response, errorMessage);
|
||||
@ -527,62 +537,63 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||
|
||||
// save message for reprocessing
|
||||
trade.getBuyer().setPaymentSentMessage(message);
|
||||
trade.requestPersistence();
|
||||
trade.persistNow(() -> {
|
||||
|
||||
// process message on trade thread
|
||||
if (!trade.isInitialized() || trade.isShutDownStarted()) return;
|
||||
ThreadUtils.execute(() -> {
|
||||
// We are more tolerant with expected phase and allow also DEPOSITS_PUBLISHED as it can be the case
|
||||
// that the wallet is still syncing and so the DEPOSITS_CONFIRMED state to yet triggered when we received
|
||||
// a mailbox message with PaymentSentMessage.
|
||||
// TODO A better fix would be to add a listener for the wallet sync state and process
|
||||
// the mailbox msg once wallet is ready and trade state set.
|
||||
synchronized (trade.getLock()) {
|
||||
if (!trade.isInitialized() || trade.isShutDownStarted()) return;
|
||||
if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_SENT.ordinal()) {
|
||||
log.warn("Received another PaymentSentMessage which was already processed for {} {}, ACKing", trade.getClass().getSimpleName(), trade.getId());
|
||||
handleTaskRunnerSuccess(peer, message);
|
||||
return;
|
||||
// process message on trade thread
|
||||
if (!trade.isInitialized() || trade.isShutDownStarted()) return;
|
||||
ThreadUtils.execute(() -> {
|
||||
// We are more tolerant with expected phase and allow also DEPOSITS_PUBLISHED as it can be the case
|
||||
// that the wallet is still syncing and so the DEPOSITS_CONFIRMED state to yet triggered when we received
|
||||
// a mailbox message with PaymentSentMessage.
|
||||
// TODO A better fix would be to add a listener for the wallet sync state and process
|
||||
// the mailbox msg once wallet is ready and trade state set.
|
||||
synchronized (trade.getLock()) {
|
||||
if (!trade.isInitialized() || trade.isShutDownStarted()) return;
|
||||
if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_SENT.ordinal()) {
|
||||
log.warn("Received another PaymentSentMessage which was already processed for {} {}, ACKing", trade.getClass().getSimpleName(), trade.getId());
|
||||
handleTaskRunnerSuccess(peer, message);
|
||||
return;
|
||||
}
|
||||
if (trade.getPayoutTx() != null) {
|
||||
log.warn("We received a PaymentSentMessage but we have already created the payout tx " +
|
||||
"so we ignore the message. This can happen if the ACK message to the peer did not " +
|
||||
"arrive and the peer repeats sending us the message. We send another ACK msg.");
|
||||
sendAckMessage(peer, message, true, null);
|
||||
removeMailboxMessageAfterProcessing(message);
|
||||
return;
|
||||
}
|
||||
latchTrade();
|
||||
expect(anyPhase()
|
||||
.with(message)
|
||||
.from(peer))
|
||||
.setup(tasks(
|
||||
ApplyFilter.class,
|
||||
ProcessPaymentSentMessage.class,
|
||||
VerifyPeersAccountAgeWitness.class)
|
||||
.using(new TradeTaskRunner(trade,
|
||||
() -> {
|
||||
handleTaskRunnerSuccess(peer, message);
|
||||
},
|
||||
(errorMessage) -> {
|
||||
log.warn("Error processing payment sent message: " + errorMessage);
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
|
||||
// schedule to reprocess message unless deleted
|
||||
if (trade.getBuyer().getPaymentSentMessage() != null) {
|
||||
UserThread.runAfter(() -> {
|
||||
reprocessPaymentSentMessageCount++;
|
||||
maybeReprocessPaymentSentMessage(reprocessOnError);
|
||||
}, trade.getReprocessDelayInSeconds(reprocessPaymentSentMessageCount));
|
||||
} else {
|
||||
handleTaskRunnerFault(peer, message, errorMessage); // otherwise send nack
|
||||
}
|
||||
unlatchTrade();
|
||||
})))
|
||||
.executeTasks(true);
|
||||
awaitTradeLatch();
|
||||
}
|
||||
if (trade.getPayoutTx() != null) {
|
||||
log.warn("We received a PaymentSentMessage but we have already created the payout tx " +
|
||||
"so we ignore the message. This can happen if the ACK message to the peer did not " +
|
||||
"arrive and the peer repeats sending us the message. We send another ACK msg.");
|
||||
sendAckMessage(peer, message, true, null);
|
||||
removeMailboxMessageAfterProcessing(message);
|
||||
return;
|
||||
}
|
||||
latchTrade();
|
||||
expect(anyPhase()
|
||||
.with(message)
|
||||
.from(peer))
|
||||
.setup(tasks(
|
||||
ApplyFilter.class,
|
||||
ProcessPaymentSentMessage.class,
|
||||
VerifyPeersAccountAgeWitness.class)
|
||||
.using(new TradeTaskRunner(trade,
|
||||
() -> {
|
||||
handleTaskRunnerSuccess(peer, message);
|
||||
},
|
||||
(errorMessage) -> {
|
||||
log.warn("Error processing payment sent message: " + errorMessage);
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
|
||||
// schedule to reprocess message unless deleted
|
||||
if (trade.getBuyer().getPaymentSentMessage() != null) {
|
||||
UserThread.runAfter(() -> {
|
||||
reprocessPaymentSentMessageCount++;
|
||||
maybeReprocessPaymentSentMessage(reprocessOnError);
|
||||
}, trade.getReprocessDelayInSeconds(reprocessPaymentSentMessageCount));
|
||||
} else {
|
||||
handleTaskRunnerFault(peer, message, errorMessage); // otherwise send nack
|
||||
}
|
||||
unlatchTrade();
|
||||
})))
|
||||
.executeTasks(true);
|
||||
awaitTradeLatch();
|
||||
}
|
||||
}, trade.getId());
|
||||
}, trade.getId());
|
||||
});
|
||||
}
|
||||
|
||||
// received by buyer and arbitrator
|
||||
@ -609,59 +620,60 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||
|
||||
// save message for reprocessing
|
||||
trade.getSeller().setPaymentReceivedMessage(message);
|
||||
trade.requestPersistence();
|
||||
trade.persistNow(() -> {
|
||||
|
||||
// process message on trade thread
|
||||
if (!trade.isInitialized() || trade.isShutDownStarted()) return;
|
||||
ThreadUtils.execute(() -> {
|
||||
synchronized (trade.getLock()) {
|
||||
if (!trade.isInitialized() || trade.isShutDownStarted()) return;
|
||||
latchTrade();
|
||||
Validator.checkTradeId(processModel.getOfferId(), message);
|
||||
processModel.setTradeMessage(message);
|
||||
// process message on trade thread
|
||||
if (!trade.isInitialized() || trade.isShutDownStarted()) return;
|
||||
ThreadUtils.execute(() -> {
|
||||
synchronized (trade.getLock()) {
|
||||
if (!trade.isInitialized() || trade.isShutDownStarted()) return;
|
||||
latchTrade();
|
||||
Validator.checkTradeId(processModel.getOfferId(), message);
|
||||
processModel.setTradeMessage(message);
|
||||
|
||||
// check minimum trade phase
|
||||
if (trade.isBuyer() && trade.getPhase().ordinal() < Trade.Phase.PAYMENT_SENT.ordinal()) {
|
||||
log.warn("Received PaymentReceivedMessage before payment sent for {} {}, ignoring", trade.getClass().getSimpleName(), trade.getId());
|
||||
return;
|
||||
// check minimum trade phase
|
||||
if (trade.isBuyer() && trade.getPhase().ordinal() < Trade.Phase.PAYMENT_SENT.ordinal()) {
|
||||
log.warn("Received PaymentReceivedMessage before payment sent for {} {}, ignoring", trade.getClass().getSimpleName(), trade.getId());
|
||||
return;
|
||||
}
|
||||
if (trade.isArbitrator() && trade.getPhase().ordinal() < Trade.Phase.DEPOSITS_CONFIRMED.ordinal()) {
|
||||
log.warn("Received PaymentReceivedMessage before deposits confirmed for {} {}, ignoring", trade.getClass().getSimpleName(), trade.getId());
|
||||
return;
|
||||
}
|
||||
if (trade.isSeller() && trade.getPhase().ordinal() < Trade.Phase.DEPOSITS_UNLOCKED.ordinal()) {
|
||||
log.warn("Received PaymentReceivedMessage before deposits unlocked for {} {}, ignoring", trade.getClass().getSimpleName(), trade.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
expect(anyPhase()
|
||||
.with(message)
|
||||
.from(peer))
|
||||
.setup(tasks(
|
||||
ProcessPaymentReceivedMessage.class)
|
||||
.using(new TradeTaskRunner(trade,
|
||||
() -> {
|
||||
handleTaskRunnerSuccess(peer, message);
|
||||
},
|
||||
errorMessage -> {
|
||||
log.warn("Error processing payment received message: " + errorMessage);
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
|
||||
// schedule to reprocess message unless deleted
|
||||
if (trade.getSeller().getPaymentReceivedMessage() != null) {
|
||||
UserThread.runAfter(() -> {
|
||||
reprocessPaymentReceivedMessageCount++;
|
||||
maybeReprocessPaymentReceivedMessage(reprocessOnError);
|
||||
}, trade.getReprocessDelayInSeconds(reprocessPaymentReceivedMessageCount));
|
||||
} else {
|
||||
handleTaskRunnerFault(peer, message, errorMessage); // otherwise send nack
|
||||
}
|
||||
unlatchTrade();
|
||||
})))
|
||||
.executeTasks(true);
|
||||
awaitTradeLatch();
|
||||
}
|
||||
if (trade.isArbitrator() && trade.getPhase().ordinal() < Trade.Phase.DEPOSITS_CONFIRMED.ordinal()) {
|
||||
log.warn("Received PaymentReceivedMessage before deposits confirmed for {} {}, ignoring", trade.getClass().getSimpleName(), trade.getId());
|
||||
return;
|
||||
}
|
||||
if (trade.isSeller() && trade.getPhase().ordinal() < Trade.Phase.DEPOSITS_UNLOCKED.ordinal()) {
|
||||
log.warn("Received PaymentReceivedMessage before deposits unlocked for {} {}, ignoring", trade.getClass().getSimpleName(), trade.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
expect(anyPhase()
|
||||
.with(message)
|
||||
.from(peer))
|
||||
.setup(tasks(
|
||||
ProcessPaymentReceivedMessage.class)
|
||||
.using(new TradeTaskRunner(trade,
|
||||
() -> {
|
||||
handleTaskRunnerSuccess(peer, message);
|
||||
},
|
||||
errorMessage -> {
|
||||
log.warn("Error processing payment received message: " + errorMessage);
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
|
||||
// schedule to reprocess message unless deleted
|
||||
if (trade.getSeller().getPaymentReceivedMessage() != null) {
|
||||
UserThread.runAfter(() -> {
|
||||
reprocessPaymentReceivedMessageCount++;
|
||||
maybeReprocessPaymentReceivedMessage(reprocessOnError);
|
||||
}, trade.getReprocessDelayInSeconds(reprocessPaymentReceivedMessageCount));
|
||||
} else {
|
||||
handleTaskRunnerFault(peer, message, errorMessage); // otherwise send nack
|
||||
}
|
||||
unlatchTrade();
|
||||
})))
|
||||
.executeTasks(true);
|
||||
awaitTradeLatch();
|
||||
}
|
||||
}, trade.getId());
|
||||
}, trade.getId());
|
||||
});
|
||||
}
|
||||
|
||||
public void onWithdrawCompleted() {
|
||||
@ -832,7 +844,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||
}
|
||||
}
|
||||
|
||||
protected synchronized void stopTimeout() {
|
||||
public synchronized void stopTimeout() {
|
||||
synchronized (timeoutTimerLock) {
|
||||
if (timeoutTimer != null) {
|
||||
timeoutTimer.stop();
|
||||
|
@ -95,6 +95,18 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
|
||||
// set peer's 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
|
||||
Offer offer = trade.getOffer();
|
||||
boolean isFromTaker = sender == trade.getTaker();
|
||||
@ -138,7 +150,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
|
||||
|
||||
// relay deposit txs when both requests received
|
||||
MoneroDaemon daemon = trade.getXmrWalletService().getDaemon();
|
||||
if (processModel.getMaker().getContractSignature() != null && processModel.getTaker().getContractSignature() != null) {
|
||||
if (hasBothContractSignatures()) {
|
||||
|
||||
// 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());
|
||||
@ -182,22 +194,15 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
|
||||
throw e;
|
||||
}
|
||||
} 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.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() {
|
||||
return !processModel.getTradeManager().hasOpenTrade(trade);
|
||||
}
|
||||
@ -210,7 +215,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
|
||||
|
||||
// log error
|
||||
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
|
||||
@ -229,7 +234,7 @@ public class ArbitratorProcessDepositRequest extends TradeTask {
|
||||
}
|
||||
|
||||
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() {
|
||||
@Override
|
||||
public void onArrived() {
|
||||
|
@ -38,13 +38,14 @@ public class ProcessDepositResponse extends TradeTask {
|
||||
try {
|
||||
runInterceptHook();
|
||||
|
||||
// throw if error
|
||||
// handle error
|
||||
DepositResponse message = (DepositResponse) processModel.getTradeMessage();
|
||||
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);
|
||||
processModel.getTradeManager().unregisterTrade(trade);
|
||||
throw new RuntimeException(message.getErrorMessage());
|
||||
trade.setInitError(new RuntimeException(message.getErrorMessage()));
|
||||
complete();
|
||||
return;
|
||||
}
|
||||
|
||||
// record security deposits
|
||||
|
@ -37,7 +37,6 @@ package haveno.core.xmr;
|
||||
import com.google.inject.Inject;
|
||||
|
||||
import haveno.common.ThreadUtils;
|
||||
import haveno.common.UserThread;
|
||||
import haveno.core.api.model.XmrBalanceInfo;
|
||||
import haveno.core.offer.OpenOffer;
|
||||
import haveno.core.offer.OpenOfferManager;
|
||||
@ -163,18 +162,12 @@ public class Balances {
|
||||
// calculate reserved balance
|
||||
reservedBalance = reservedOfferBalance.add(reservedTradeBalance);
|
||||
|
||||
// play sound if funds received
|
||||
boolean fundsReceived = balanceSumBefore != null && getNonTradeBalanceSum().compareTo(balanceSumBefore) > 0;
|
||||
if (fundsReceived) HavenoUtils.playCashRegisterSound();
|
||||
|
||||
// notify balance update
|
||||
UserThread.execute(() -> {
|
||||
|
||||
// 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);
|
||||
});
|
||||
updateCounter.set(updateCounter.get() + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -184,10 +184,6 @@ public class XmrNodes {
|
||||
this.operator = operator;
|
||||
}
|
||||
|
||||
public boolean hasOnionAddress() {
|
||||
return onionAddress != null;
|
||||
}
|
||||
|
||||
public String getHostNameOrAddress() {
|
||||
if (hostName != null)
|
||||
return hostName;
|
||||
@ -195,10 +191,19 @@ public class XmrNodes {
|
||||
return address;
|
||||
}
|
||||
|
||||
public boolean hasOnionAddress() {
|
||||
return onionAddress != null;
|
||||
}
|
||||
|
||||
public boolean hasClearNetAddress() {
|
||||
return hostName != null || address != null;
|
||||
}
|
||||
|
||||
public String getClearNetUri() {
|
||||
if (!hasClearNetAddress()) throw new IllegalStateException("XmrNode does not have clearnet address");
|
||||
return "http://" + getHostNameOrAddress() + ":" + port;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "onionAddress='" + onionAddress + '\'' +
|
||||
|
@ -159,8 +159,13 @@ public class XmrKeyImagePoller {
|
||||
if (keyImagesGroup == null) return;
|
||||
keyImagesGroup.removeAll(keyImages);
|
||||
if (keyImagesGroup.isEmpty()) keyImageGroups.remove(groupId);
|
||||
Set<String> allKeyImages = getKeyImages();
|
||||
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();
|
||||
}
|
||||
@ -171,10 +176,10 @@ public class XmrKeyImagePoller {
|
||||
Set<String> keyImagesGroup = keyImageGroups.get(groupId);
|
||||
if (keyImagesGroup == null) return;
|
||||
keyImageGroups.remove(groupId);
|
||||
Set<String> keyImages = getKeyImages();
|
||||
Set<String> allKeyImages = getKeyImages();
|
||||
synchronized (lastStatuses) {
|
||||
for (String keyImage : keyImagesGroup) {
|
||||
if (lastStatuses.containsKey(keyImage) && !keyImages.contains(keyImage)) {
|
||||
if (lastStatuses.containsKey(keyImage) && !allKeyImages.contains(keyImage)) {
|
||||
lastStatuses.remove(keyImage);
|
||||
}
|
||||
}
|
||||
|
@ -105,7 +105,6 @@ public abstract class XmrWalletBase {
|
||||
// start polling wallet for progress
|
||||
syncProgressLatch = new CountDownLatch(1);
|
||||
syncProgressLooper = new TaskLooper(() -> {
|
||||
if (wallet == null) return;
|
||||
long height;
|
||||
try {
|
||||
height = wallet.getHeight(); // can get read timeout while syncing
|
||||
|
@ -237,6 +237,7 @@ shared.pending=Pending
|
||||
shared.me=Me
|
||||
shared.maker=Maker
|
||||
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
|
||||
|
||||
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.start=Start local node
|
||||
xmrConnectionError.localNode.start.error=Error starting local node
|
||||
|
@ -60,6 +60,6 @@
|
||||
</content_rating>
|
||||
|
||||
<releases>
|
||||
<release version="1.0.19" date="2025-03-10"/>
|
||||
<release version="1.1.0" date="2025-04-17"/>
|
||||
</releases>
|
||||
</component>
|
||||
|
@ -5,10 +5,10 @@
|
||||
<!-- See: https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -->
|
||||
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0.19</string>
|
||||
<string>1.1.0</string>
|
||||
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.19</string>
|
||||
<string>1.1.0</string>
|
||||
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>Haveno</string>
|
||||
|
@ -353,7 +353,7 @@ public class MainView extends InitializableView<StackPane, MainViewModel> {
|
||||
settingsButtonWithBadge.getStyleClass().add("new");
|
||||
|
||||
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;
|
||||
|
||||
Class<? extends View> viewClass = viewPath.tip();
|
||||
|
@ -337,7 +337,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener
|
||||
tacWindow.onAction(acceptedHandler::run).show();
|
||||
}, 1));
|
||||
|
||||
havenoSetup.setDisplayMoneroConnectionErrorHandler(connectionError -> {
|
||||
havenoSetup.setDisplayMoneroConnectionFallbackHandler(connectionError -> {
|
||||
if (connectionError == null) {
|
||||
if (moneroConnectionErrorPopup != null) moneroConnectionErrorPopup.hide();
|
||||
} else {
|
||||
@ -349,7 +349,6 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener
|
||||
.actionButtonText(Res.get("xmrConnectionError.localNode.start"))
|
||||
.onAction(() -> {
|
||||
log.warn("User has chosen to start local node.");
|
||||
havenoSetup.getConnectionServiceError().set(null);
|
||||
new Thread(() -> {
|
||||
try {
|
||||
HavenoUtils.xmrConnectionService.startLocalNode();
|
||||
@ -359,16 +358,20 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener
|
||||
.headLine(Res.get("xmrConnectionError.localNode.start.error"))
|
||||
.warning(e.getMessage())
|
||||
.closeButtonText(Res.get("shared.close"))
|
||||
.onClose(() -> havenoSetup.getConnectionServiceError().set(null))
|
||||
.onClose(() -> havenoSetup.getConnectionServiceFallbackType().set(null))
|
||||
.show();
|
||||
} finally {
|
||||
havenoSetup.getConnectionServiceFallbackType().set(null);
|
||||
}
|
||||
}).start();
|
||||
})
|
||||
.secondaryActionButtonText(Res.get("xmrConnectionError.localNode.fallback"))
|
||||
.onSecondaryAction(() -> {
|
||||
log.warn("User has chosen to fallback to the next best available Monero node.");
|
||||
havenoSetup.getConnectionServiceError().set(null);
|
||||
new Thread(() -> HavenoUtils.xmrConnectionService.fallbackToBestConnection()).start();
|
||||
new Thread(() -> {
|
||||
HavenoUtils.xmrConnectionService.fallbackToBestConnection();
|
||||
havenoSetup.getConnectionServiceFallbackType().set(null);
|
||||
}).start();
|
||||
})
|
||||
.closeButtonText(Res.get("shared.shutDown"))
|
||||
.onClose(HavenoApp.getShutDownHandler());
|
||||
@ -376,16 +379,35 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener
|
||||
case CUSTOM:
|
||||
moneroConnectionErrorPopup = new Popup()
|
||||
.headLine(Res.get("xmrConnectionError.headline"))
|
||||
.warning(Res.get("xmrConnectionError.customNode"))
|
||||
.warning(Res.get("xmrConnectionError.customNodes"))
|
||||
.actionButtonText(Res.get("shared.yes"))
|
||||
.onAction(() -> {
|
||||
havenoSetup.getConnectionServiceError().set(null);
|
||||
new Thread(() -> HavenoUtils.xmrConnectionService.fallbackToBestConnection()).start();
|
||||
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.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;
|
||||
}
|
||||
|
@ -380,8 +380,6 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
|
||||
}
|
||||
|
||||
private void onShowPayFundsScreen() {
|
||||
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
|
||||
|
||||
nextButton.setVisible(false);
|
||||
nextButton.setManaged(false);
|
||||
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)
|
||||
// waitingForFundsSpinner.play();
|
||||
|
||||
payFundsTitledGroupBg.setVisible(true);
|
||||
totalToPayTextField.setVisible(true);
|
||||
addressTextField.setVisible(true);
|
||||
qrCodeImageView.setVisible(true);
|
||||
balanceTextField.setVisible(true);
|
||||
cancelButton2.setVisible(true);
|
||||
reserveExactAmountSlider.setVisible(true);
|
||||
showFundingGroup();
|
||||
}
|
||||
|
||||
private void updateOfferElementsStyle() {
|
||||
@ -986,6 +978,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
|
||||
gridPane.setVgap(5);
|
||||
GUIUtil.setDefaultTwoColumnConstraintsForGridPane(gridPane);
|
||||
scrollPane.setContent(gridPane);
|
||||
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
|
||||
}
|
||||
|
||||
private void addPaymentGroup() {
|
||||
@ -1179,6 +1172,40 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
|
||||
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() {
|
||||
Tuple3<HBox, InfoInputTextField, Label> tuple = getEditableValueBoxWithInfo(
|
||||
Res.get("createOffer.securityDeposit.prompt"));
|
||||
@ -1326,6 +1353,8 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
|
||||
});
|
||||
cancelButton2.setDefaultButton(false);
|
||||
cancelButton2.setVisible(false);
|
||||
|
||||
hideFundingGroup();
|
||||
}
|
||||
|
||||
private void openWallet() {
|
||||
|
@ -19,6 +19,8 @@ package haveno.desktop.main.offer;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.name.Named;
|
||||
|
||||
import haveno.common.ThreadUtils;
|
||||
import haveno.common.UserThread;
|
||||
import haveno.common.app.DevEnv;
|
||||
import haveno.common.handlers.ErrorMessageHandler;
|
||||
@ -108,7 +110,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
|
||||
private String amountDescription;
|
||||
private String addressAsString;
|
||||
private final String paymentLabel;
|
||||
private boolean createOfferRequested;
|
||||
private boolean createOfferInProgress;
|
||||
public boolean createOfferCanceled;
|
||||
|
||||
public final StringProperty amount = new SimpleStringProperty();
|
||||
@ -638,32 +640,37 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void onPlaceOffer(Offer offer, Runnable resultHandler) {
|
||||
errorMessage.set(null);
|
||||
createOfferRequested = true;
|
||||
createOfferCanceled = false;
|
||||
|
||||
dataModel.onPlaceOffer(offer, transaction -> {
|
||||
resultHandler.run();
|
||||
if (!createOfferCanceled) placeOfferCompleted.set(true);
|
||||
ThreadUtils.execute(() -> {
|
||||
errorMessage.set(null);
|
||||
}, errMessage -> {
|
||||
createOfferRequested = false;
|
||||
if (offer.getState() == Offer.State.OFFER_FEE_RESERVED) errorMessage.set(errMessage + Res.get("createOffer.errorInfo"));
|
||||
else errorMessage.set(errMessage);
|
||||
createOfferInProgress = true;
|
||||
createOfferCanceled = false;
|
||||
|
||||
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(errMessage);
|
||||
|
||||
UserThread.execute(() -> {
|
||||
updateButtonDisableState();
|
||||
updateSpinnerInfo();
|
||||
resultHandler.run();
|
||||
});
|
||||
});
|
||||
|
||||
UserThread.execute(() -> {
|
||||
updateButtonDisableState();
|
||||
updateSpinnerInfo();
|
||||
resultHandler.run();
|
||||
});
|
||||
});
|
||||
|
||||
updateButtonDisableState();
|
||||
updateSpinnerInfo();
|
||||
}, getClass().getSimpleName());
|
||||
}
|
||||
|
||||
public void onCancelOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
createOfferRequested = false;
|
||||
log.info("Canceling posting offer {}", offer.getId());
|
||||
createOfferCanceled = true;
|
||||
OpenOfferManager openOfferManager = HavenoUtils.openOfferManager;
|
||||
Optional<OpenOffer> openOffer = openOfferManager.getOpenOffer(offer.getId());
|
||||
@ -1355,7 +1362,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
|
||||
inputDataValid = inputDataValid && getExtraInfoValidationResult().isValid;
|
||||
|
||||
isNextButtonDisabled.set(!inputDataValid);
|
||||
isPlaceOfferButtonDisabled.set(createOfferRequested || !inputDataValid || !dataModel.getIsXmrWalletFunded().get());
|
||||
isPlaceOfferButtonDisabled.set(createOfferInProgress || !inputDataValid || !dataModel.getIsXmrWalletFunded().get());
|
||||
}
|
||||
|
||||
private ValidationResult getExtraInfoValidationResult() {
|
||||
|
@ -174,9 +174,11 @@ abstract class OfferBookViewModel extends ActivatableViewModel {
|
||||
tradeCurrencyListChangeListener = c -> fillCurrencies();
|
||||
|
||||
// refresh filter on changes
|
||||
offerBook.getOfferBookListItems().addListener((ListChangeListener<OfferBookListItem>) c -> {
|
||||
filterOffers();
|
||||
});
|
||||
// TODO: This is removed because it's expensive to re-filter offers for every change (high cpu for many offers).
|
||||
// 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 -> {
|
||||
final Optional<OfferBookListItem> highestAmountOffer = filteredItems.stream()
|
||||
|
@ -649,7 +649,7 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
|
||||
|
||||
errorMessageSubscription = EasyBind.subscribe(model.errorMessage, newValue -> {
|
||||
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(() -> {
|
||||
errorPopupDisplayed.set(true);
|
||||
model.resetErrorMessage();
|
||||
|
@ -91,7 +91,7 @@
|
||||
<AutoTooltipButton fx:id="rescanOutputsButton"/>
|
||||
</VBox>
|
||||
|
||||
<TitledGroupBg fx:id="p2pHeader" GridPane.rowIndex="5" GridPane.rowSpan="5">
|
||||
<TitledGroupBg fx:id="p2pHeader" GridPane.rowIndex="5" GridPane.rowSpan="6">
|
||||
<padding>
|
||||
<Insets top="50.0"/>
|
||||
</padding>
|
||||
@ -159,7 +159,10 @@
|
||||
<HavenoTextField fx:id="chainHeightTextField" GridPane.rowIndex="9" editable="false"
|
||||
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 hgrow="ALWAYS" minWidth="500"/>
|
||||
|
@ -75,7 +75,7 @@ public class NetworkSettingsView extends ActivatableView<GridPane, Void> {
|
||||
@FXML
|
||||
InputTextField xmrNodesInputTextField;
|
||||
@FXML
|
||||
TextField onionAddress, sentDataTextField, receivedDataTextField, chainHeightTextField;
|
||||
TextField onionAddress, sentDataTextField, receivedDataTextField, chainHeightTextField, minVersionForTrading;
|
||||
@FXML
|
||||
Label p2PPeersLabel, moneroConnectionsLabel;
|
||||
@FXML
|
||||
@ -176,6 +176,7 @@ public class NetworkSettingsView extends ActivatableView<GridPane, Void> {
|
||||
sentDataTextField.setPromptText(Res.get("settings.net.sentDataLabel"));
|
||||
receivedDataTextField.setPromptText(Res.get("settings.net.receivedDataLabel"));
|
||||
chainHeightTextField.setPromptText(Res.get("settings.net.chainHeightLabel"));
|
||||
minVersionForTrading.setPromptText(Res.get("filterWindow.disableTradeBelowVersion"));
|
||||
roundTripTimeColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.roundTripTimeColumn")));
|
||||
sentBytesColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.sentBytesColumn")));
|
||||
receivedBytesColumn.setGraphic(new AutoTooltipLabel(Res.get("settings.net.receivedBytesColumn")));
|
||||
@ -275,7 +276,7 @@ public class NetworkSettingsView extends ActivatableView<GridPane, Void> {
|
||||
showShutDownPopup();
|
||||
}
|
||||
};
|
||||
filterPropertyListener = (observable, oldValue, newValue) -> applyPreventPublicXmrNetwork();
|
||||
filterPropertyListener = (observable, oldValue, newValue) -> applyFilter();
|
||||
|
||||
// disable radio buttons if no nodes available
|
||||
if (xmrNodes.getProvidedXmrNodes().isEmpty()) {
|
||||
@ -298,7 +299,7 @@ public class NetworkSettingsView extends ActivatableView<GridPane, Void> {
|
||||
moneroPeersToggleGroup.selectedToggleProperty().addListener(moneroPeersToggleGroupListener);
|
||||
|
||||
if (filterManager.getFilter() != null)
|
||||
applyPreventPublicXmrNetwork();
|
||||
applyFilter();
|
||||
|
||||
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();
|
||||
usePublicNodesRadio.setDisable(isPublicNodesDisabled());
|
||||
if (preventPublicXmrNetwork && selectedMoneroNodesOption == XmrNodes.MoneroNodesOption.PUBLIC) {
|
||||
@ -501,6 +504,10 @@ public class NetworkSettingsView extends ActivatableView<GridPane, Void> {
|
||||
selectMoneroPeersToggle();
|
||||
onMoneroPeersToggleSelected(false);
|
||||
}
|
||||
|
||||
// set min version for trading
|
||||
String minVersion = filterManager.getDisableTradeBelowVersion();
|
||||
minVersionForTrading.textProperty().setValue(minVersion == null ? Res.get("shared.none") : minVersion);
|
||||
}
|
||||
|
||||
private boolean isPublicNodesDisabled() {
|
||||
|
@ -5,10 +5,11 @@ This guide describes how to deploy a Haveno network:
|
||||
- Manage services on a VPS
|
||||
- Fork and build Haveno
|
||||
- Start a Monero node
|
||||
- Build and start price nodes
|
||||
- Add seed nodes
|
||||
- Add arbitrators
|
||||
- Configure trade fees and other configuration
|
||||
- Build and start price nodes
|
||||
- Set a network filter
|
||||
- Build Haveno installers for distribution
|
||||
- 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.
|
||||
|
||||
## 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
|
||||
|
||||
### 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.
|
||||
|
||||
> **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.
|
||||
> * 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.
|
||||
|
||||
**Notes**
|
||||
- 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.
|
||||
- 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.
|
||||
> [!note]
|
||||
> * 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.
|
||||
> * 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.
|
||||
|
||||
## Remove an arbitrator
|
||||
|
||||
> **Note**
|
||||
> Ensure the arbitrator's trades are completed before retiring the instance.
|
||||
> [!warning]
|
||||
> * 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.
|
||||
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
|
||||
|
||||
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).
|
||||
|
||||
## 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
|
||||
|
||||
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 user1 on Monero's mainnet using `make user1-desktop-mainnet` or Monero's stagenet using `make user1-desktop-stagenet`.
|
||||
|
@ -124,7 +124,7 @@ OUTPUT=$(gpg --digest-algo SHA256 --verify "${signature_filename}" "${binary_fil
|
||||
if ! echo "$OUTPUT" | grep -q "Good signature from"; then
|
||||
echo_red "Verification failed: $OUTPUT"
|
||||
exit 1;
|
||||
else 7z x "${binary_filename}" && mv haveno*.deb "${package_filename}"
|
||||
else mv -f "${binary_filename}" "${package_filename}"
|
||||
fi
|
||||
|
||||
echo_blue "Haveno binaries have been successfully verified."
|
||||
@ -136,7 +136,7 @@ mkdir -p "${install_dir}"
|
||||
|
||||
# Delete old Haveno binaries
|
||||
#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}"
|
||||
|
||||
|
||||
|
@ -41,7 +41,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@Slf4j
|
||||
public class SeedNodeMain extends ExecutableForAppWithP2p {
|
||||
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 Timer checkConnectionLossTime;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user