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

View File

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

View File

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

View File

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

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_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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

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 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()) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 + '\'' +

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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
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}"

View File

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