diff --git a/Makefile b/Makefile index 236815ec..3a73d2a6 100644 --- a/Makefile +++ b/Makefile @@ -40,24 +40,9 @@ deploy: screen -S localnet -X screen -t $$target; \ screen -S localnet -p $$target -X stuff "make $$target\n"; \ done; - # give bitcoind rpc server time to start + # give time to start sleep 5 -bitcoind: - ./.localnet/bitcoind \ - -regtest \ - -peerbloomfilters=1 \ - -datadir=.localnet/ \ - -rpcuser=haveno \ - -rpcpassword=1234 \ - -btc-blocks: - ./.localnet/bitcoin-cli \ - -regtest \ - -rpcuser=haveno \ - -rpcpassword=1234 \ - generatetoaddress 101 bcrt1q6j90vywv8x7eyevcnn2tn2wrlg3vsjlsvt46qz - .PHONY: build seednode localnet # Local network diff --git a/build.gradle b/build.gradle index 703ada79..054695ca 100644 --- a/build.gradle +++ b/build.gradle @@ -163,28 +163,33 @@ configure([project(':cli'), // edit generated shell scripts such that they expect to be executed in the // project root dir as opposed to a 'bin' subdirectory - def windowsScriptFile = file("${rootProject.projectDir}/haveno-${applicationName}.bat") - windowsScriptFile.text = windowsScriptFile.text.replace( - 'set APP_HOME=%DIRNAME%..', 'set APP_HOME=%DIRNAME%') - - def unixScriptFile = file("${rootProject.projectDir}/haveno-$applicationName") - unixScriptFile.text = unixScriptFile.text.replace( - 'APP_HOME=$( cd "${APP_HOME:-./}.." > /dev/null && pwd -P ) || exit', 'APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit') - - if (applicationName == 'desktop') { + if (osdetector.os == 'windows') { + def windowsScriptFile = file("${rootProject.projectDir}/haveno-${applicationName}.bat") windowsScriptFile.text = windowsScriptFile.text.replace( - 'DEFAULT_JVM_OPTS=', 'DEFAULT_JVM_OPTS=-XX:MaxRAM=4g ' + - '--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=ALL-UNNAMED ' + - '--add-opens=javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED ' + - '--add-opens=java.base/java.lang.reflect=ALL-UNNAMED ' + - '--add-opens=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED') + 'set APP_HOME=%DIRNAME%..', 'set APP_HOME=%DIRNAME%') + if (applicationName == 'desktop') { + windowsScriptFile.text = windowsScriptFile.text.replace( + 'DEFAULT_JVM_OPTS=', 'DEFAULT_JVM_OPTS=-XX:MaxRAM=4g ' + + '--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=ALL-UNNAMED ' + + '--add-opens=javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED ' + + '--add-opens=java.base/java.lang.reflect=ALL-UNNAMED ' + + '--add-opens=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED') + } + } + else { + def unixScriptFile = file("${rootProject.projectDir}/haveno-$applicationName") unixScriptFile.text = unixScriptFile.text.replace( - 'DEFAULT_JVM_OPTS=""', 'DEFAULT_JVM_OPTS="-XX:MaxRAM=4g ' + - '--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=ALL-UNNAMED ' + - '--add-opens=javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED ' + - '--add-opens=java.base/java.lang.reflect=ALL-UNNAMED ' + - '--add-opens=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED"') + 'APP_HOME=$( cd "${APP_HOME:-./}.." > /dev/null && pwd -P ) || exit', 'APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit') + + if (applicationName == 'desktop') { + unixScriptFile.text = unixScriptFile.text.replace( + 'DEFAULT_JVM_OPTS=""', 'DEFAULT_JVM_OPTS="-XX:MaxRAM=4g ' + + '--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=ALL-UNNAMED ' + + '--add-opens=javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED ' + + '--add-opens=java.base/java.lang.reflect=ALL-UNNAMED ' + + '--add-opens=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED"') + } } if (applicationName == 'apitest') { @@ -605,7 +610,7 @@ configure(project(':desktop')) { apply plugin: 'com.github.johnrengelman.shadow' apply from: 'package/package.gradle' - version = '1.0.8-SNAPSHOT' + version = '1.0.9-SNAPSHOT' jar.manifest.attributes( "Implementation-Title": project.name, diff --git a/common/src/main/java/haveno/common/ThreadUtils.java b/common/src/main/java/haveno/common/ThreadUtils.java index 832ea91a..e463e831 100644 --- a/common/src/main/java/haveno/common/ThreadUtils.java +++ b/common/src/main/java/haveno/common/ThreadUtils.java @@ -47,6 +47,7 @@ public class ThreadUtils { synchronized (THREADS) { THREADS.put(threadId, Thread.currentThread()); } + Thread.currentThread().setName(threadId); command.run(); }); } diff --git a/common/src/main/java/haveno/common/app/Version.java b/common/src/main/java/haveno/common/app/Version.java index 3554ea19..92ca9cf1 100644 --- a/common/src/main/java/haveno/common/app/Version.java +++ b/common/src/main/java/haveno/common/app/Version.java @@ -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.8"; + public static final String VERSION = "1.0.9"; /** * Holds a list of the tagged resource files for optimizing the getData requests. diff --git a/core/src/main/java/haveno/core/api/CoreApi.java b/core/src/main/java/haveno/core/api/CoreApi.java index b4a293a3..25f98c34 100644 --- a/core/src/main/java/haveno/core/api/CoreApi.java +++ b/core/src/main/java/haveno/core/api/CoreApi.java @@ -496,6 +496,10 @@ public class CoreApi { tradeInstant); } + public void deletePaymentAccount(String paymentAccountId) { + paymentAccountsService.deletePaymentAccount(paymentAccountId); + } + public List getCryptoCurrencyPaymentMethods() { return paymentAccountsService.getCryptoCurrencyPaymentMethods(); } @@ -557,10 +561,6 @@ public class CoreApi { return coreTradesService.getTrades(); } - public String getTradeRole(String tradeId) { - return coreTradesService.getTradeRole(tradeId); - } - public List getChatMessages(String tradeId) { return coreTradesService.getChatMessages(tradeId); } diff --git a/core/src/main/java/haveno/core/api/CorePaymentAccountsService.java b/core/src/main/java/haveno/core/api/CorePaymentAccountsService.java index df88d094..dcf1ef0e 100644 --- a/core/src/main/java/haveno/core/api/CorePaymentAccountsService.java +++ b/core/src/main/java/haveno/core/api/CorePaymentAccountsService.java @@ -145,6 +145,16 @@ class CorePaymentAccountsService { return cryptoCurrencyAccount; } + synchronized void deletePaymentAccount(String paymentAccountId) { + accountService.checkAccountOpen(); + PaymentAccount paymentAccount = getPaymentAccount(paymentAccountId); + if (paymentAccount == null) throw new IllegalArgumentException(format("Payment account with id %s not found", paymentAccountId)); + user.removePaymentAccount(paymentAccount); + log.info("Deleted payment account with id {} and payment method {}.", + paymentAccount.getId(), + paymentAccount.getPaymentAccountPayload().getPaymentMethodId()); + } + // TODO Support all alt coin payment methods supported by UI. // The getCryptoCurrencyPaymentMethods method below will be // callable from the CLI when more are supported. diff --git a/core/src/main/java/haveno/core/api/XmrConnectionService.java b/core/src/main/java/haveno/core/api/XmrConnectionService.java index 50307f9c..6f9b4bb3 100644 --- a/core/src/main/java/haveno/core/api/XmrConnectionService.java +++ b/core/src/main/java/haveno/core/api/XmrConnectionService.java @@ -36,7 +36,10 @@ import haveno.network.Socks5ProxyProvider; import haveno.network.p2p.P2PService; import haveno.network.p2p.P2PServiceListener; import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import javafx.beans.property.IntegerProperty; import javafx.beans.property.LongProperty; @@ -67,8 +70,6 @@ public final class XmrConnectionService { private static final int MIN_BROADCAST_CONNECTIONS = 0; // TODO: 0 for stagenet, 5+ for mainnet private static final long REFRESH_PERIOD_HTTP_MS = 20000; // refresh period when connected to remote node over http private static final long REFRESH_PERIOD_ONION_MS = 30000; // refresh period when connected to remote node over tor - private static final long LOG_POLL_ERROR_AFTER_MS = 300000; // minimum period between logging errors fetching daemon info - private static Long lastErrorTimestamp; private final Object lock = new Object(); private final Object pollLock = new Object(); @@ -97,11 +98,21 @@ public final class XmrConnectionService { private Boolean isConnected = false; @Getter private MoneroDaemonInfo lastInfo; + private Long lastLogPollErrorTimestamp; private Long syncStartHeight = null; private TaskLooper daemonPollLooper; + @Getter private boolean isShutDownStarted; private List listeners = new ArrayList<>(); + // connection switching + private static final int EXCLUDE_CONNECTION_SECONDS = 180; + private static final int MAX_SWITCH_REQUESTS_PER_MINUTE = 2; + private static final int SKIP_SWITCH_WITHIN_MS = 10000; + private int numRequestsLastMinute; + private long lastSwitchTimestamp; + private Set excludedConnections = new HashSet<>(); + @Inject public XmrConnectionService(P2PService p2PService, Config config, @@ -200,12 +211,6 @@ public final class XmrConnectionService { return connectionManager.getConnections(); } - public void switchToBestConnection() { - if (isFixedConnection() || !connectionManager.getAutoSwitch()) return; - MoneroRpcConnection bestConnection = getBestAvailableConnection(); - if (bestConnection != null) setConnection(bestConnection); - } - public void setConnection(String connectionUri) { accountService.checkAccountOpen(); connectionManager.setConnection(connectionUri); // listener will update connection list @@ -243,10 +248,86 @@ public final class XmrConnectionService { public MoneroRpcConnection getBestAvailableConnection() { accountService.checkAccountOpen(); List ignoredConnections = new ArrayList(); - if (xmrLocalNode.shouldBeIgnored() && connectionManager.hasConnection(xmrLocalNode.getUri())) ignoredConnections.add(connectionManager.getConnectionByUri(xmrLocalNode.getUri())); + addLocalNodeIfIgnored(ignoredConnections); return connectionManager.getBestAvailableConnection(ignoredConnections.toArray(new MoneroRpcConnection[0])); } + private MoneroRpcConnection getBestAvailableConnection(Collection ignoredConnections) { + accountService.checkAccountOpen(); + Set ignoredConnectionsSet = new HashSet<>(ignoredConnections); + addLocalNodeIfIgnored(ignoredConnectionsSet); + return connectionManager.getBestAvailableConnection(ignoredConnectionsSet.toArray(new MoneroRpcConnection[0])); + } + + private void addLocalNodeIfIgnored(Collection ignoredConnections) { + if (xmrLocalNode.shouldBeIgnored() && connectionManager.hasConnection(xmrLocalNode.getUri())) ignoredConnections.add(connectionManager.getConnectionByUri(xmrLocalNode.getUri())); + } + + private void switchToBestConnection() { + if (isFixedConnection() || !connectionManager.getAutoSwitch()) { + log.info("Skipping switch to best Monero connection because connection is fixed or auto switch is disabled"); + return; + } + MoneroRpcConnection bestConnection = getBestAvailableConnection(); + if (bestConnection != null) setConnection(bestConnection); + } + + public synchronized boolean requestSwitchToNextBestConnection() { + log.warn("Requesting switch to next best monerod, current monerod={}", getConnection() == null ? null : getConnection().getUri()); + + // skip if shut down started + if (isShutDownStarted) { + log.warn("Skipping switch to next best Monero connection because shut down has started"); + return false; + } + + // skip if connection is fixed + if (isFixedConnection() || !connectionManager.getAutoSwitch()) { + log.info("Skipping switch to next best Monero connection because connection is fixed or auto switch is disabled"); + return false; + } + + // skip if last switch was too recent + boolean skipSwitch = System.currentTimeMillis() - lastSwitchTimestamp < SKIP_SWITCH_WITHIN_MS; + if (skipSwitch) { + log.warn("Skipping switch to next best Monero connection because last switch was less than {} seconds ago", SKIP_SWITCH_WITHIN_MS / 1000); + return false; + } + + // skip if too many requests in the last minute + if (numRequestsLastMinute > MAX_SWITCH_REQUESTS_PER_MINUTE) { + log.warn("Skipping switch to next best Monero connection because more than {} requests were made in the last minute", MAX_SWITCH_REQUESTS_PER_MINUTE); + return false; + } + + // increment request count + numRequestsLastMinute++; + UserThread.runAfter(() -> numRequestsLastMinute--, 60); // decrement after one minute + + // exclude current connection + MoneroRpcConnection currentConnection = getConnection(); + if (currentConnection != null) excludedConnections.add(currentConnection); + + // get connection to switch to + MoneroRpcConnection bestConnection = getBestAvailableConnection(excludedConnections); + + // remove from excluded connections after period + UserThread.runAfter(() -> { + if (currentConnection != null) excludedConnections.remove(currentConnection); + }, EXCLUDE_CONNECTION_SECONDS); + + // return if no connection to switch to + if (bestConnection == null) { + log.warn("No connection to switch to"); + return false; + } + + // switch to best connection + lastSwitchTimestamp = System.currentTimeMillis(); + setConnection(bestConnection); + return true; + } + public void setAutoSwitch(boolean autoSwitch) { accountService.checkAccountOpen(); connectionManager.setAutoSwitch(autoSwitch); @@ -504,7 +585,6 @@ public final class XmrConnectionService { // register connection listener connectionManager.addListener(this::onConnectionChanged); - isInitialized = true; } @@ -620,8 +700,14 @@ public final class XmrConnectionService { return; } + // log error message periodically + if ((lastLogPollErrorTimestamp == null || System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS)) { + log.warn("Failed to fetch daemon info, trying to switch to best connection: " + e.getMessage()); + if (DevEnv.isDevMode()) e.printStackTrace(); + lastLogPollErrorTimestamp = System.currentTimeMillis(); + } + // switch to best connection - log.warn("Failed to fetch daemon info, trying to switch to best connection: " + e.getMessage()); switchToBestConnection(); lastInfo = daemon.getInfo(); // caught internally if still fails } @@ -662,9 +748,9 @@ public final class XmrConnectionService { }); // handle error recovery - if (lastErrorTimestamp != null) { + if (lastLogPollErrorTimestamp != null) { log.info("Successfully fetched daemon info after previous error"); - lastErrorTimestamp = null; + lastLogPollErrorTimestamp = null; } // clear error message @@ -677,13 +763,6 @@ public final class XmrConnectionService { // skip if shut down if (isShutDownStarted) return; - // log error message periodically - if ((lastErrorTimestamp == null || System.currentTimeMillis() - lastErrorTimestamp > LOG_POLL_ERROR_AFTER_MS)) { - lastErrorTimestamp = System.currentTimeMillis(); - log.warn("Could not update daemon info: " + e.getMessage()); - if (DevEnv.isDevMode()) e.printStackTrace(); - } - // set error message getConnectionServiceErrorMsg().set(e.getMessage()); } finally { diff --git a/core/src/main/java/haveno/core/api/model/TradeInfo.java b/core/src/main/java/haveno/core/api/model/TradeInfo.java index f53d3ee9..fa94fd27 100644 --- a/core/src/main/java/haveno/core/api/model/TradeInfo.java +++ b/core/src/main/java/haveno/core/api/model/TradeInfo.java @@ -21,6 +21,7 @@ import haveno.common.Payload; import haveno.core.api.model.builder.TradeInfoV1Builder; import haveno.core.trade.Contract; import haveno.core.trade.Trade; +import haveno.core.trade.TradeUtil; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -142,10 +143,7 @@ public class TradeInfo implements Payload { } public static TradeInfo toTradeInfo(Trade trade) { - return toTradeInfo(trade, null); - } - - public static TradeInfo toTradeInfo(Trade trade, String role) { + String role = TradeUtil.getRole(trade); ContractInfo contractInfo; if (trade.getContract() != null) { Contract contract = trade.getContract(); diff --git a/core/src/main/java/haveno/core/app/HavenoSetup.java b/core/src/main/java/haveno/core/app/HavenoSetup.java index 67214fa2..0d1ee2b9 100644 --- a/core/src/main/java/haveno/core/app/HavenoSetup.java +++ b/core/src/main/java/haveno/core/app/HavenoSetup.java @@ -113,7 +113,7 @@ import org.fxmisc.easybind.monadic.MonadicBinding; public class HavenoSetup { private static final String VERSION_FILE_NAME = "version"; - private static final long STARTUP_TIMEOUT_MINUTES = 5; + private static final long STARTUP_TIMEOUT_MINUTES = 4; private final DomainInitialisation domainInitialisation; private final P2PNetworkSetup p2PNetworkSetup; diff --git a/core/src/main/java/haveno/core/offer/Offer.java b/core/src/main/java/haveno/core/offer/Offer.java index 5c2f6eb5..675fbeb5 100644 --- a/core/src/main/java/haveno/core/offer/Offer.java +++ b/core/src/main/java/haveno/core/offer/Offer.java @@ -410,6 +410,10 @@ public class Offer implements NetworkPayload, PersistablePayload { return getExtraDataMap().get(OfferPayload.PAY_BY_MAIL_EXTRA_INFO); else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.AUSTRALIA_PAYID_EXTRA_INFO)) return getExtraDataMap().get(OfferPayload.AUSTRALIA_PAYID_EXTRA_INFO); + else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.PAYPAL_EXTRA_INFO)) + return getExtraDataMap().get(OfferPayload.PAYPAL_EXTRA_INFO); + else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.CASHAPP_EXTRA_INFO)) + return getExtraDataMap().get(OfferPayload.CASHAPP_EXTRA_INFO); else return ""; } diff --git a/core/src/main/java/haveno/core/offer/OfferPayload.java b/core/src/main/java/haveno/core/offer/OfferPayload.java index 1a194a1a..2f377bab 100644 --- a/core/src/main/java/haveno/core/offer/OfferPayload.java +++ b/core/src/main/java/haveno/core/offer/OfferPayload.java @@ -94,12 +94,14 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay // Keys for extra map // Only set for traditional offers public static final String ACCOUNT_AGE_WITNESS_HASH = "accountAgeWitnessHash"; + public static final String CASHAPP_EXTRA_INFO = "cashAppExtraInfo"; public static final String REFERRAL_ID = "referralId"; // Only used in payment method F2F public static final String F2F_CITY = "f2fCity"; public static final String F2F_EXTRA_INFO = "f2fExtraInfo"; public static final String PAY_BY_MAIL_EXTRA_INFO = "payByMailExtraInfo"; public static final String AUSTRALIA_PAYID_EXTRA_INFO = "australiaPayidExtraInfo"; + public static final String PAYPAL_EXTRA_INFO = "payPalExtraInfo"; // Comma separated list of ordinal of a haveno.common.app.Capability. E.g. ordinal of // Capability.SIGNED_ACCOUNT_AGE_WITNESS is 11 and Capability.MEDIATION is 12 so if we want to signal that maker diff --git a/core/src/main/java/haveno/core/offer/OfferUtil.java b/core/src/main/java/haveno/core/offer/OfferUtil.java index 6d314fe2..72593ab5 100644 --- a/core/src/main/java/haveno/core/offer/OfferUtil.java +++ b/core/src/main/java/haveno/core/offer/OfferUtil.java @@ -37,16 +37,20 @@ import haveno.core.monetary.Volume; import static haveno.core.offer.OfferPayload.ACCOUNT_AGE_WITNESS_HASH; import static haveno.core.offer.OfferPayload.AUSTRALIA_PAYID_EXTRA_INFO; import static haveno.core.offer.OfferPayload.CAPABILITIES; +import static haveno.core.offer.OfferPayload.CASHAPP_EXTRA_INFO; import static haveno.core.offer.OfferPayload.F2F_CITY; import static haveno.core.offer.OfferPayload.F2F_EXTRA_INFO; import static haveno.core.offer.OfferPayload.PAY_BY_MAIL_EXTRA_INFO; +import static haveno.core.offer.OfferPayload.PAYPAL_EXTRA_INFO; import static haveno.core.offer.OfferPayload.REFERRAL_ID; import static haveno.core.offer.OfferPayload.XMR_AUTO_CONF; import static haveno.core.offer.OfferPayload.XMR_AUTO_CONF_ENABLED_VALUE; import haveno.core.payment.AustraliaPayidAccount; +import haveno.core.payment.CashAppAccount; import haveno.core.payment.F2FAccount; import haveno.core.payment.PayByMailAccount; +import haveno.core.payment.PayPalAccount; import haveno.core.payment.PaymentAccount; import haveno.core.provider.price.MarketPrice; import haveno.core.provider.price.PriceFeedService; @@ -200,6 +204,14 @@ public class OfferUtil { extraDataMap.put(PAY_BY_MAIL_EXTRA_INFO, ((PayByMailAccount) paymentAccount).getExtraInfo()); } + if (paymentAccount instanceof PayPalAccount) { + extraDataMap.put(PAYPAL_EXTRA_INFO, ((PayPalAccount) paymentAccount).getExtraInfo()); + } + + if (paymentAccount instanceof CashAppAccount) { + extraDataMap.put(CASHAPP_EXTRA_INFO, ((CashAppAccount) paymentAccount).getExtraInfo()); + } + if (paymentAccount instanceof AustraliaPayidAccount) { extraDataMap.put(AUSTRALIA_PAYID_EXTRA_INFO, ((AustraliaPayidAccount) paymentAccount).getExtraInfo()); } diff --git a/core/src/main/java/haveno/core/offer/OpenOffer.java b/core/src/main/java/haveno/core/offer/OpenOffer.java index 27f9c829..b0df3e0e 100644 --- a/core/src/main/java/haveno/core/offer/OpenOffer.java +++ b/core/src/main/java/haveno/core/offer/OpenOffer.java @@ -42,7 +42,6 @@ import javafx.beans.property.SimpleObjectProperty; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; -import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; import java.util.ArrayList; @@ -51,11 +50,10 @@ import java.util.List; import java.util.Optional; @EqualsAndHashCode -@Slf4j public final class OpenOffer implements Tradable { public enum State { - SCHEDULED, + PENDING, AVAILABLE, RESERVED, CLOSED, @@ -122,7 +120,7 @@ public final class OpenOffer implements Tradable { this.offer = offer; this.triggerPrice = triggerPrice; this.reserveExactAmount = reserveExactAmount; - state = State.SCHEDULED; + state = State.PENDING; } public OpenOffer(Offer offer, long triggerPrice, OpenOffer openOffer) { @@ -167,8 +165,8 @@ public final class OpenOffer implements Tradable { this.reserveTxHex = reserveTxHex; this.reserveTxKey = reserveTxKey; - if (this.state == State.RESERVED) - setState(State.AVAILABLE); + // reset reserved state to available + if (this.state == State.RESERVED) setState(State.AVAILABLE); } @Override @@ -234,8 +232,8 @@ public final class OpenOffer implements Tradable { return stateProperty; } - public boolean isScheduled() { - return state == State.SCHEDULED; + public boolean isPending() { + return state == State.PENDING; } public boolean isAvailable() { diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index b9670c2d..ec8baae5 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -96,6 +96,7 @@ import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -113,6 +114,7 @@ import monero.daemon.model.MoneroKeyImageSpentStatus; import monero.daemon.model.MoneroTx; import monero.wallet.model.MoneroIncomingTransfer; import monero.wallet.model.MoneroOutputQuery; +import monero.wallet.model.MoneroOutputWallet; import monero.wallet.model.MoneroTransferQuery; import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroTxQuery; @@ -130,7 +132,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe private static final long REPUBLISH_AGAIN_AT_STARTUP_DELAY_SEC = 30; private static final long REPUBLISH_INTERVAL_MS = TimeUnit.MINUTES.toMillis(30); private static final long REFRESH_INTERVAL_MS = OfferPayload.TTL / 2; - private static final int MAX_PROCESS_ATTEMPTS = 5; + private static final int NUM_ATTEMPTS_THRESHOLD = 5; // process pending offer only on republish cycle after this many attempts private final CoreContext coreContext; private final KeyRing keyRing; @@ -250,17 +252,17 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // read open offers persistenceManager.readPersisted(persisted -> { - openOffers.setAll(persisted.getList()); - openOffers.forEach(openOffer -> openOffer.getOffer().setPriceFeedService(priceFeedService)); + openOffers.setAll(persisted.getList()); + openOffers.forEach(openOffer -> openOffer.getOffer().setPriceFeedService(priceFeedService)); - // read signed offers - signedOfferPersistenceManager.readPersisted(signedOfferPersisted -> { - signedOffers.setAll(signedOfferPersisted.getList()); - completeHandler.run(); - }, - completeHandler); - }, - completeHandler); + // read signed offers + signedOfferPersistenceManager.readPersisted(signedOfferPersisted -> { + signedOffers.setAll(signedOfferPersisted.getList()); + completeHandler.run(); + }, + completeHandler); + }, + completeHandler); } private synchronized void maybeInitializeKeyImagePoller() { @@ -470,17 +472,19 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // .forEach(openOffer -> OfferUtil.getInvalidMakerFeeTxErrorMessage(openOffer.getOffer(), btcWalletService) // .ifPresent(errorMsg -> invalidOffers.add(new Tuple2<>(openOffer, errorMsg)))); - // process scheduled offers - processScheduledOffers((transaction) -> {}, (errorMessage) -> { - log.warn("Error processing unposted offers: " + errorMessage); + // process pending offers + processPendingOffers(false, (transaction) -> {}, (errorMessage) -> { + log.warn("Error processing pending offers on bootstrap: " + errorMessage); }); - // register to process unposted offers on new block + // register to process pending offers on new block xmrWalletService.addWalletListener(new MoneroWalletListener() { @Override public void onNewBlock(long height) { - processScheduledOffers((transaction) -> {}, (errorMessage) -> { - log.warn("Error processing unposted offers on new block {}: {}", height, errorMessage); + + // process each pending offer on new block a few times, then rely on period republish + processPendingOffers(true, (transaction) -> {}, (errorMessage) -> { + log.warn("Error processing pending offers on new block {}: {}", height, errorMessage); }); } }); @@ -547,19 +551,17 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe synchronized (processOffersLock) { CountDownLatch latch = new CountDownLatch(1); addOpenOffer(openOffer); - processUnpostedOffer(getOpenOffers(), openOffer, (transaction) -> { + processPendingOffer(getOpenOffers(), openOffer, (transaction) -> { requestPersistence(); latch.countDown(); resultHandler.handleResult(transaction); }, (errorMessage) -> { - if (openOffer.isCanceled()) latch.countDown(); - else { - log.warn("Error processing unposted offer {}: {}", openOffer.getId(), errorMessage); + if (!openOffer.isCanceled()) { + log.warn("Error processing pending offer {}: {}", openOffer.getId(), errorMessage); doCancelOffer(openOffer); - offer.setErrorMessage(errorMessage); - latch.countDown(); - errorMessageHandler.handleErrorMessage(errorMessage); } + latch.countDown(); + errorMessageHandler.handleErrorMessage(errorMessage); }); HavenoUtils.awaitLatch(latch); } @@ -581,9 +583,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe public void activateOpenOffer(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - if (openOffer.isScheduled()) { - resultHandler.handleResult(); // ignore if scheduled - } else if (!offersToBeEdited.containsKey(openOffer.getId())) { + if (openOffer.isPending()) { + resultHandler.handleResult(); // ignore if pending + } else if (offersToBeEdited.containsKey(openOffer.getId())) { + errorMessageHandler.handleErrorMessage("You can't activate an offer that is currently edited."); + } else { Offer offer = openOffer.getOffer(); offerBookService.activateOffer(offer, () -> { @@ -593,8 +597,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe resultHandler.handleResult(); }, errorMessageHandler); - } else { - errorMessageHandler.handleErrorMessage("You can't activate an offer that is currently edited."); } } @@ -622,6 +624,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe log.info("Canceling open offer: {}", openOffer.getId()); if (!offersToBeEdited.containsKey(openOffer.getId())) { if (openOffer.isAvailable()) { + openOffer.setState(OpenOffer.State.CANCELED); offerBookService.removeOffer(openOffer.getOffer().getOfferPayload(), () -> { ThreadUtils.submitToPool(() -> { // TODO: this runs off thread and then shows popup when done. should show overlay spinner until done @@ -631,6 +634,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe }, errorMessageHandler); } else { + openOffer.setState(OpenOffer.State.CANCELED); ThreadUtils.submitToPool(() -> { doCancelOffer(openOffer); if (resultHandler != null) resultHandler.handleResult(); @@ -852,47 +856,48 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } } - public boolean hasAvailableOutput(BigInteger amount) { - return findSplitOutputFundingTx(getOpenOffers(), null, amount, null) != null; - } - /////////////////////////////////////////////////////////////////////////////////////////// // Place offer helpers /////////////////////////////////////////////////////////////////////////////////////////// - - private void processScheduledOffers(TransactionResultHandler resultHandler, // TODO (woodser): transaction not needed with result handler + private void processPendingOffers(boolean skipOffersWithTooManyAttempts, + TransactionResultHandler resultHandler, // TODO (woodser): transaction not needed with result handler ErrorMessageHandler errorMessageHandler) { ThreadUtils.execute(() -> { synchronized (processOffersLock) { List errorMessages = new ArrayList(); List openOffers = getOpenOffers(); - for (OpenOffer scheduledOffer : openOffers) { - if (scheduledOffer.getState() != OpenOffer.State.SCHEDULED) continue; + for (OpenOffer pendingOffer : openOffers) { + if (pendingOffer.getState() != OpenOffer.State.PENDING) continue; + if (skipOffersWithTooManyAttempts && pendingOffer.getNumProcessingAttempts() > NUM_ATTEMPTS_THRESHOLD) continue; // skip offers with too many attempts CountDownLatch latch = new CountDownLatch(1); - processUnpostedOffer(openOffers, scheduledOffer, (transaction) -> { + processPendingOffer(openOffers, pendingOffer, (transaction) -> { latch.countDown(); }, errorMessage -> { - if (!scheduledOffer.isCanceled()) { - log.warn("Error processing unposted offer, offerId={}, attempt={}/{}, error={}", scheduledOffer.getId(), scheduledOffer.getNumProcessingAttempts(), MAX_PROCESS_ATTEMPTS, errorMessage); - if (scheduledOffer.getNumProcessingAttempts() >= MAX_PROCESS_ATTEMPTS) { - log.warn("Offer canceled after {} attempts, offerId={}, error={}", scheduledOffer.getNumProcessingAttempts(), scheduledOffer.getId(), errorMessage); - HavenoUtils.havenoSetup.getTopErrorMsg().set("Offer canceled after " + scheduledOffer.getNumProcessingAttempts() + " attempts. Please switch to a better Monero connection and try again.\n\nOffer ID: " + scheduledOffer.getId() + "\nError: " + errorMessage); - doCancelOffer(scheduledOffer); + if (!pendingOffer.isCanceled()) { + String warnMessage = "Error processing pending offer, offerId=" + pendingOffer.getId() + ", attempt=" + pendingOffer.getNumProcessingAttempts() + ": " + errorMessage; + errorMessages.add(warnMessage); + + // cancel offer if invalid + if (pendingOffer.getOffer().getState() == Offer.State.INVALID) { + log.warn("Canceling offer because it's invalid: {}", pendingOffer.getId()); + doCancelOffer(pendingOffer); } - errorMessages.add(errorMessage); } latch.countDown(); }); HavenoUtils.awaitLatch(latch); } requestPersistence(); - if (errorMessages.size() > 0) errorMessageHandler.handleErrorMessage(errorMessages.toString()); - else resultHandler.handleResult(null); + if (errorMessages.isEmpty()) { + if (resultHandler != null) resultHandler.handleResult(null); + } else { + if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage(errorMessages.toString()); + } } }, THREAD_ID); } - private void processUnpostedOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + private void processPendingOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { // skip if already processing if (openOffer.isProcessing()) { @@ -902,17 +907,18 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // process offer openOffer.setProcessing(true); - doProcessUnpostedOffer(openOffers, openOffer, (transaction) -> { + doProcessPendingOffer(openOffers, openOffer, (transaction) -> { openOffer.setProcessing(false); resultHandler.handleResult(transaction); }, (errorMsg) -> { openOffer.setProcessing(false); openOffer.setNumProcessingAttempts(openOffer.getNumProcessingAttempts() + 1); + openOffer.getOffer().setErrorMessage(errorMsg); errorMessageHandler.handleErrorMessage(errorMsg); }); } - private void doProcessUnpostedOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + private void doProcessPendingOffer(List openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { new Thread(() -> { try { @@ -929,15 +935,25 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe if (openOffer.isReserveExactAmount()) { // find tx with exact input amount - MoneroTxWallet splitOutputTx = findSplitOutputFundingTx(openOffers, openOffer); + MoneroTxWallet splitOutputTx = getSplitOutputFundingTx(openOffers, openOffer); if (splitOutputTx != null && openOffer.getSplitOutputTxHash() == null) { setSplitOutputTx(openOffer, splitOutputTx); } // if not found, create tx to split exact output if (splitOutputTx == null) { - if (openOffer.getSplitOutputTxHash() != null) log.warn("Split output tx not found for offer {}", openOffer.getId()); - splitOrSchedule(openOffers, openOffer, amountNeeded); + if (openOffer.getSplitOutputTxHash() != null) { + log.warn("Split output tx unexpectedly unavailable for offer, offerId={}, split output tx={}", openOffer.getId(), openOffer.getSplitOutputTxHash()); + setSplitOutputTx(openOffer, null); + } + try { + splitOrSchedule(openOffers, openOffer, amountNeeded); + } catch (Exception e) { + log.warn("Unable to split or schedule funds for offer {}: {}", openOffer.getId(), e.getMessage()); + openOffer.getOffer().setState(Offer.State.INVALID); + errorMessageHandler.handleErrorMessage(e.getMessage()); + return; + } } else if (!splitOutputTx.isLocked()) { // otherwise sign and post offer if split output available @@ -965,89 +981,87 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe }).start(); } - private MoneroTxWallet findSplitOutputFundingTx(List openOffers, OpenOffer openOffer) { + private MoneroTxWallet getSplitOutputFundingTx(List openOffers, OpenOffer openOffer) { XmrAddressEntry addressEntry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); - return findSplitOutputFundingTx(openOffers, openOffer, openOffer.getOffer().getAmountNeeded(), addressEntry.getSubaddressIndex()); + return getSplitOutputFundingTx(openOffers, openOffer, openOffer.getOffer().getAmountNeeded(), addressEntry.getSubaddressIndex()); } - private MoneroTxWallet findSplitOutputFundingTx(List openOffers, OpenOffer openOffer, BigInteger reserveAmount, Integer preferredSubaddressIndex) { - List fundingTxs = new ArrayList<>(); - MoneroTxWallet earliestUnscheduledTx = null; + private MoneroTxWallet getSplitOutputFundingTx(List openOffers, OpenOffer openOffer, BigInteger reserveAmount, Integer preferredSubaddressIndex) { // return split output tx if already assigned if (openOffer != null && openOffer.getSplitOutputTxHash() != null) { - return xmrWalletService.getTx(openOffer.getSplitOutputTxHash()); - } - // return earliest tx with exact amount to offer's subaddress if available - if (preferredSubaddressIndex != null) { + // get recorded split output tx + MoneroTxWallet splitOutputTx = xmrWalletService.getTx(openOffer.getSplitOutputTxHash()); - // get txs with exact output amount - fundingTxs = xmrWalletService.getTxs(new MoneroTxQuery() - .setIsConfirmed(true) - .setOutputQuery(new MoneroOutputQuery() - .setAccountIndex(0) - .setSubaddressIndex(preferredSubaddressIndex) - .setAmount(reserveAmount) - .setIsSpent(false) - .setIsFrozen(false))); - - // return earliest tx if available - earliestUnscheduledTx = getEarliestUnscheduledTx(openOffers, fundingTxs); - if (earliestUnscheduledTx != null) return earliestUnscheduledTx; - } - - // return if awaiting scheduled tx - if (openOffer.getScheduledTxHashes() != null) return null; - - // get all transactions including from pool - List allTxs = xmrWalletService.getTxs(false); - - if (preferredSubaddressIndex != null) { - - // return earliest tx with exact incoming transfer to fund offer's subaddress if available (since outputs are not available until confirmed) - fundingTxs.clear(); - for (MoneroTxWallet tx : allTxs) { - boolean hasExactTransfer = tx.getTransfers(new MoneroTransferQuery() - .setIsIncoming(true) - .setAccountIndex(0) - .setSubaddressIndex(preferredSubaddressIndex) - .setAmount(reserveAmount)).size() > 0; - if (hasExactTransfer) fundingTxs.add(tx); + // check if split output tx is available for offer + if (splitOutputTx.isLocked()) return splitOutputTx; + else { + boolean isAvailable = true; + for (MoneroOutputWallet output : splitOutputTx.getOutputsWallet()) { + if (output.isSpent() || output.isFrozen()) { + isAvailable = false; + break; + } + } + if (isAvailable || isReservedByOffer(openOffer, splitOutputTx)) return splitOutputTx; + else log.warn("Split output tx is no longer available for offer {}", openOffer.getId()); } - earliestUnscheduledTx = getEarliestUnscheduledTx(openOffers, fundingTxs); + } + + // get split output tx to offer's preferred subaddress + if (preferredSubaddressIndex != null) { + List fundingTxs = getSplitOutputFundingTxs(reserveAmount, preferredSubaddressIndex); + MoneroTxWallet earliestUnscheduledTx = getEarliestUnscheduledTx(openOffers, openOffer, fundingTxs); if (earliestUnscheduledTx != null) return earliestUnscheduledTx; } - // return earliest tx with exact confirmed output to any subaddress if available - fundingTxs.clear(); - for (MoneroTxWallet tx : allTxs) { - boolean hasExactOutput = tx.getOutputsWallet(new MoneroOutputQuery() - .setAccountIndex(0) - .setAmount(reserveAmount) - .setIsSpent(false) - .setIsFrozen(false)).size() > 0; - if (hasExactOutput) fundingTxs.add(tx); - } - earliestUnscheduledTx = getEarliestUnscheduledTx(openOffers, fundingTxs); - if (earliestUnscheduledTx != null) return earliestUnscheduledTx; - - // return earliest tx with exact incoming transfer to any subaddress if available (since outputs are not available until confirmed) - fundingTxs.clear(); - for (MoneroTxWallet tx : allTxs) { - boolean hasExactTransfer = tx.getTransfers(new MoneroTransferQuery() - .setIsIncoming(true) - .setAccountIndex(0) - .setAmount(reserveAmount)).size() > 0; - if (hasExactTransfer) fundingTxs.add(tx); - } - return getEarliestUnscheduledTx(openOffers, fundingTxs); + // get split output tx to any subaddress + List fundingTxs = getSplitOutputFundingTxs(reserveAmount, null); + return getEarliestUnscheduledTx(openOffers, openOffer, fundingTxs); } - private MoneroTxWallet getEarliestUnscheduledTx(List openOffers, List txs) { + private boolean isReservedByOffer(OpenOffer openOffer, MoneroTxWallet tx) { + if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() == null) return false; + Set offerKeyImages = new HashSet(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages()); + for (MoneroOutputWallet output : tx.getOutputsWallet()) { + if (offerKeyImages.contains(output.getKeyImage().getHex())) return true; + } + return false; + } + + private List getSplitOutputFundingTxs(BigInteger reserveAmount, Integer preferredSubaddressIndex) { + List splitOutputTxs = xmrWalletService.getTxs(new MoneroTxQuery().setIsIncoming(true).setIsFailed(false)); + Set removeTxs = new HashSet(); + for (MoneroTxWallet tx : splitOutputTxs) { + if (tx.getOutputs() != null) { // outputs not available until first confirmation + for (MoneroOutputWallet output : tx.getOutputsWallet()) { + if (output.isSpent() || output.isFrozen()) removeTxs.add(tx); + } + } + if (!hasExactAmount(tx, reserveAmount, preferredSubaddressIndex)) removeTxs.add(tx); + } + splitOutputTxs.removeAll(removeTxs); + return splitOutputTxs; + } + + private boolean hasExactAmount(MoneroTxWallet tx, BigInteger amount, Integer preferredSubaddressIndex) { + boolean hasExactOutput = (tx.getOutputsWallet(new MoneroOutputQuery() + .setAccountIndex(0) + .setSubaddressIndex(preferredSubaddressIndex) + .setAmount(amount)).size() > 0); + if (hasExactOutput) return true; + boolean hasExactTransfer = (tx.getTransfers(new MoneroTransferQuery() + .setAccountIndex(0) + .setSubaddressIndex(preferredSubaddressIndex) + .setAmount(amount)).size() > 0); + return hasExactTransfer; + } + + private MoneroTxWallet getEarliestUnscheduledTx(List openOffers, OpenOffer excludeOpenOffer, List txs) { MoneroTxWallet earliestUnscheduledTx = null; for (MoneroTxWallet tx : txs) { - if (isTxScheduled(openOffers, tx.getHash())) continue; + if (isTxScheduledByOtherOffer(openOffers, excludeOpenOffer, tx.getHash())) continue; if (earliestUnscheduledTx == null || (earliestUnscheduledTx.getNumConfirmations() < tx.getNumConfirmations())) earliestUnscheduledTx = tx; } return earliestUnscheduledTx; @@ -1084,8 +1098,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe .setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY)); break; } catch (Exception e) { - log.warn("Error creating split output tx to fund offer {} at subaddress {}, attempt={}/{}, error={}", openOffer.getShortId(), entry.getSubaddressIndex(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); + if (e.getMessage().contains("not enough")) throw e; // do not retry if not enough funds + log.warn("Error creating split output tx to fund offer, offerId={}, subaddress={}, attempt={}/{}, error={}", openOffer.getShortId(), entry.getSubaddressIndex(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); if (stopped || i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + if (xmrConnectionService.isConnected()) xmrWalletService.requestSwitchToNextBestConnection(); HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } } @@ -1099,11 +1115,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } private void setSplitOutputTx(OpenOffer openOffer, MoneroTxWallet splitOutputTx) { - openOffer.setSplitOutputTxHash(splitOutputTx.getHash()); - openOffer.setSplitOutputTxFee(splitOutputTx.getFee().longValueExact()); - openOffer.setScheduledTxHashes(Arrays.asList(splitOutputTx.getHash())); - openOffer.setScheduledAmount(openOffer.getOffer().getAmountNeeded().toString()); - openOffer.setState(OpenOffer.State.SCHEDULED); + openOffer.setSplitOutputTxHash(splitOutputTx == null ? null : splitOutputTx.getHash()); + openOffer.setSplitOutputTxFee(splitOutputTx == null ? 0l : splitOutputTx.getFee().longValueExact()); + openOffer.setScheduledTxHashes(splitOutputTx == null ? null : Arrays.asList(splitOutputTx.getHash())); + openOffer.setScheduledAmount(splitOutputTx == null ? null : openOffer.getOffer().getAmountNeeded().toString()); + if (!openOffer.isCanceled()) openOffer.setState(OpenOffer.State.PENDING); } private void scheduleWithEarliestTxs(List openOffers, OpenOffer openOffer) { @@ -1121,7 +1137,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe List scheduledTxHashes = new ArrayList(); BigInteger scheduledAmount = BigInteger.ZERO; for (MoneroTxWallet lockedTx : lockedTxs) { - if (isTxScheduled(openOffers, lockedTx.getHash())) continue; + if (isTxScheduledByOtherOffer(openOffers, openOffer, lockedTx.getHash())) continue; if (lockedTx.getIncomingTransfers() == null || lockedTx.getIncomingTransfers().isEmpty()) continue; scheduledTxHashes.add(lockedTx.getHash()); for (MoneroIncomingTransfer transfer : lockedTx.getIncomingTransfers()) { @@ -1134,13 +1150,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // schedule txs openOffer.setScheduledTxHashes(scheduledTxHashes); openOffer.setScheduledAmount(scheduledAmount.toString()); - openOffer.setState(OpenOffer.State.SCHEDULED); + openOffer.setState(OpenOffer.State.PENDING); } private BigInteger getScheduledAmount(List openOffers) { BigInteger scheduledAmount = BigInteger.ZERO; for (OpenOffer openOffer : openOffers) { - if (openOffer.getState() != OpenOffer.State.SCHEDULED) continue; + if (openOffer.getState() != OpenOffer.State.PENDING) continue; if (openOffer.getScheduledTxHashes() == null) continue; List fundingTxs = xmrWalletService.getTxs(openOffer.getScheduledTxHashes()); for (MoneroTxWallet fundingTx : fundingTxs) { @@ -1154,12 +1170,15 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return scheduledAmount; } - private boolean isTxScheduled(List openOffers, String txHash) { - for (OpenOffer openOffer : openOffers) { - if (openOffer.getState() != OpenOffer.State.SCHEDULED) continue; - if (openOffer.getScheduledTxHashes() == null) continue; - for (String scheduledTxHash : openOffer.getScheduledTxHashes()) { - if (txHash.equals(scheduledTxHash)) return true; + private boolean isTxScheduledByOtherOffer(List openOffers, OpenOffer openOffer, String txHash) { + for (OpenOffer otherOffer : openOffers) { + if (otherOffer == openOffer) continue; + if (otherOffer.getState() != OpenOffer.State.PENDING) continue; + if (txHash.equals(otherOffer.getSplitOutputTxHash())) return true; + if (otherOffer.getScheduledTxHashes() != null) { + for (String scheduledTxHash : otherOffer.getScheduledTxHashes()) { + if (txHash.equals(scheduledTxHash)) return true; + } } } return false; @@ -1721,7 +1740,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // determine if offer is valid boolean isValid = true; Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(openOffer.getOffer().getOfferPayload().getArbitratorSigner()); - if (arbitrator == null || !HavenoUtils.isArbitratorSignatureValid(openOffer.getOffer().getOfferPayload(), arbitrator)) { + if (arbitrator == null) { + log.warn("Offer {} signed by unavailable arbitrator, reposting", openOffer.getId()); + isValid = false; + } else if (!HavenoUtils.isArbitratorSignatureValid(openOffer.getOffer().getOfferPayload(), arbitrator)) { log.warn("Offer {} has invalid arbitrator signature, reposting", openOffer.getId()); isValid = false; } @@ -1756,25 +1778,29 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe }); } else { - // cancel and recreate offer - doCancelOffer(openOffer); - Offer updatedOffer = new Offer(openOffer.getOffer().getOfferPayload()); - updatedOffer.setPriceFeedService(priceFeedService); - OpenOffer updatedOpenOffer = new OpenOffer(updatedOffer, openOffer.getTriggerPrice()); + // reset offer state to pending + openOffer.getOffer().getOfferPayload().setArbitratorSignature(null); + openOffer.getOffer().getOfferPayload().setArbitratorSigner(null); + openOffer.getOffer().setState(Offer.State.UNKNOWN); + openOffer.setState(OpenOffer.State.PENDING); - // repost offer + // republish offer synchronized (processOffersLock) { CountDownLatch latch = new CountDownLatch(1); - addOpenOffer(updatedOpenOffer); - processUnpostedOffer(getOpenOffers(), updatedOpenOffer, (transaction) -> { + processPendingOffer(getOpenOffers(), openOffer, (transaction) -> { requestPersistence(); latch.countDown(); if (completeHandler != null) completeHandler.run(); }, (errorMessage) -> { - if (!updatedOpenOffer.isCanceled()) { - log.warn("Error reposting offer {}: {}", updatedOpenOffer.getId(), errorMessage); - doCancelOffer(updatedOpenOffer); - updatedOffer.setErrorMessage(errorMessage); + if (!openOffer.isCanceled()) { + log.warn("Error republishing offer {}: {}", openOffer.getId(), errorMessage); + openOffer.getOffer().setErrorMessage(errorMessage); + + // cancel offer if invalid + if (openOffer.getOffer().getState() == Offer.State.INVALID) { + log.warn("Canceling offer because it's invalid: {}", openOffer.getId()); + doCancelOffer(openOffer); + } } latch.countDown(); if (completeHandler != null) completeHandler.run(); diff --git a/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java b/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java index e6b62fdc..0b22d9e4 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/PlaceOfferProtocol.java @@ -159,7 +159,6 @@ public class PlaceOfferProtocol { if (timeoutTimer != null) { taskRunner.cancel(); if (!model.getOpenOffer().isCanceled()) { - log.error(errorMessage); model.getOpenOffer().getOffer().setErrorMessage(errorMessage); } stopTimeoutTimer(); diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java index 61cafd65..3388368a 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java @@ -66,7 +66,7 @@ public class MakerReserveOfferFunds extends Task { synchronized (XmrWalletService.WALLET_LOCK) { // reset protocol timeout - verifyScheduled(); + verifyPending(); model.getProtocol().startTimeoutTimer(); // collect relevant info @@ -86,14 +86,15 @@ public class MakerReserveOfferFunds extends Task { //if (true) throw new RuntimeException("Pretend error"); reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex); } catch (Exception e) { - log.warn("Error creating reserve tx, attempt={}/{}, offerId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, openOffer.getShortId(), e.getMessage()); + log.warn("Error creating reserve tx, offerId={}, attempt={}/{}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, openOffer.getShortId(), e.getMessage()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; model.getProtocol().startTimeoutTimer(); // reset protocol timeout + if (model.getXmrWalletService().getConnectionService().isConnected()) model.getXmrWalletService().requestSwitchToNextBestConnection(); HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } // verify still open - verifyScheduled(); + verifyPending(); if (reserveTx != null) break; } } @@ -103,6 +104,7 @@ public class MakerReserveOfferFunds extends Task { model.getXmrWalletService().resetAddressEntriesForOpenOffer(offer.getId()); if (reserveTx != null) { model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx)); + offer.getOfferPayload().setReserveTxKeyImages(null); } throw e; @@ -130,7 +132,7 @@ public class MakerReserveOfferFunds extends Task { } } - public void verifyScheduled() { - if (!model.getOpenOffer().isScheduled()) throw new RuntimeException("Offer " + model.getOpenOffer().getOffer().getId() + " is canceled"); + public void verifyPending() { + if (!model.getOpenOffer().isPending()) throw new RuntimeException("Offer " + model.getOpenOffer().getOffer().getId() + " is canceled"); } } diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java index e252e51d..2037b51d 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerSendSignOfferRequest.java @@ -116,9 +116,10 @@ public class MakerSendSignOfferRequest extends Task { model.getOpenOffer().getOffer().getOfferPayload().setArbitratorSigner(arbitratorNodeAddress); model.getOpenOffer().getOffer().setState(Offer.State.OFFER_FEE_RESERVED); resultHandler.handleResult(); - } else { - errorMessageHandler.handleErrorMessage("Arbitrator nacked SignOfferRequest for offer " + request.getOfferId() + ": " + ackMessage.getErrorMessage()); - } + } else { + model.getOpenOffer().getOffer().setState(Offer.State.INVALID); + errorMessageHandler.handleErrorMessage("Arbitrator nacked SignOfferRequest for offer " + request.getOfferId() + ": " + ackMessage.getErrorMessage()); + } } }; model.getP2PService().addDecryptedDirectMessageListener(ackListener); @@ -137,9 +138,9 @@ public class MakerSendSignOfferRequest extends Task { log.warn("Arbitrator unavailable: address={}, error={}", arbitratorNodeAddress, errorMessage); excludedArbitrators.add(arbitratorNodeAddress); - // check if offer still scheduled - if (!model.getOpenOffer().isScheduled()) { - errorMessageHandler.handleErrorMessage("Offer is no longer scheduled, offerId=" + model.getOpenOffer().getId()); + // check if offer still pending + if (!model.getOpenOffer().isPending()) { + errorMessageHandler.handleErrorMessage("Offer is no longer pending, offerId=" + model.getOpenOffer().getId()); return; } diff --git a/core/src/main/java/haveno/core/payment/CashAppAccount.java b/core/src/main/java/haveno/core/payment/CashAppAccount.java index eea4d26c..250f1c57 100644 --- a/core/src/main/java/haveno/core/payment/CashAppAccount.java +++ b/core/src/main/java/haveno/core/payment/CashAppAccount.java @@ -39,6 +39,7 @@ public final class CashAppAccount extends PaymentAccount { PaymentAccountFormField.FieldId.EMAIL_OR_MOBILE_NR_OR_CASHTAG, PaymentAccountFormField.FieldId.TRADE_CURRENCIES, PaymentAccountFormField.FieldId.ACCOUNT_NAME, + PaymentAccountFormField.FieldId.EXTRA_INFO, PaymentAccountFormField.FieldId.SALT); public CashAppAccount() { @@ -67,4 +68,12 @@ public final class CashAppAccount extends PaymentAccount { public String getEmailOrMobileNrOrCashtag() { return ((CashAppAccountPayload) paymentAccountPayload).getEmailOrMobileNrOrCashtag(); } + + public void setExtraInfo(String extraInfo) { + ((CashAppAccountPayload) paymentAccountPayload).setExtraInfo(extraInfo); + } + + public String getExtraInfo() { + return ((CashAppAccountPayload) paymentAccountPayload).getExtraInfo(); + } } diff --git a/core/src/main/java/haveno/core/payment/PayPalAccount.java b/core/src/main/java/haveno/core/payment/PayPalAccount.java index 04c8a364..c7ec1518 100644 --- a/core/src/main/java/haveno/core/payment/PayPalAccount.java +++ b/core/src/main/java/haveno/core/payment/PayPalAccount.java @@ -62,6 +62,7 @@ public final class PayPalAccount extends PaymentAccount { PaymentAccountFormField.FieldId.EMAIL_OR_MOBILE_NR_OR_USERNAME, PaymentAccountFormField.FieldId.TRADE_CURRENCIES, PaymentAccountFormField.FieldId.ACCOUNT_NAME, + PaymentAccountFormField.FieldId.EXTRA_INFO, PaymentAccountFormField.FieldId.SALT); public PayPalAccount() { @@ -91,4 +92,12 @@ public final class PayPalAccount extends PaymentAccount { public String getEmailOrMobileNrOrUsername() { return ((PayPalAccountPayload) paymentAccountPayload).getEmailOrMobileNrOrUsername(); } + + public void setExtraInfo(String extraInfo) { + ((PayPalAccountPayload) paymentAccountPayload).setExtraInfo(extraInfo); + } + + public String getExtraInfo() { + return ((PayPalAccountPayload) paymentAccountPayload).getExtraInfo(); + } } diff --git a/core/src/main/java/haveno/core/payment/payload/CashAppAccountPayload.java b/core/src/main/java/haveno/core/payment/payload/CashAppAccountPayload.java index d9f74940..bec0f340 100644 --- a/core/src/main/java/haveno/core/payment/payload/CashAppAccountPayload.java +++ b/core/src/main/java/haveno/core/payment/payload/CashAppAccountPayload.java @@ -36,6 +36,7 @@ import java.util.Map; @Slf4j public final class CashAppAccountPayload extends PaymentAccountPayload { private String emailOrMobileNrOrCashtag = ""; + private String extraInfo = ""; public CashAppAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); @@ -48,6 +49,7 @@ public final class CashAppAccountPayload extends PaymentAccountPayload { private CashAppAccountPayload(String paymentMethod, String id, String emailOrMobileNrOrCashtag, + String extraInfo, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, @@ -56,13 +58,15 @@ public final class CashAppAccountPayload extends PaymentAccountPayload { excludeFromJsonDataMap); this.emailOrMobileNrOrCashtag = emailOrMobileNrOrCashtag; + this.extraInfo = extraInfo; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setCashAppAccountPayload(protobuf.CashAppAccountPayload.newBuilder() - .setEmailOrMobileNrOrCashtag(emailOrMobileNrOrCashtag)) + .setExtraInfo(extraInfo) + .setEmailOrMobileNrOrCashtag(emailOrMobileNrOrCashtag)) .build(); } @@ -70,6 +74,7 @@ public final class CashAppAccountPayload extends PaymentAccountPayload { return new CashAppAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getCashAppAccountPayload().getEmailOrMobileNrOrCashtag(), + proto.getCashAppAccountPayload().getExtraInfo(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } @@ -81,7 +86,10 @@ public final class CashAppAccountPayload extends PaymentAccountPayload { @Override public String getPaymentDetails() { - return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.email.mobile.cashtag") + " " + emailOrMobileNrOrCashtag; + return Res.get(paymentMethodId) + " - " + + Res.getWithCol("payment.email.mobile.cashtag") + + " " + emailOrMobileNrOrCashtag + "\n" + + Res.getWithCol("payment.shared.extraInfo") + " " + extraInfo+ "\n"; } @Override diff --git a/core/src/main/java/haveno/core/payment/payload/PayPalAccountPayload.java b/core/src/main/java/haveno/core/payment/payload/PayPalAccountPayload.java index 31765f49..76e4f878 100644 --- a/core/src/main/java/haveno/core/payment/payload/PayPalAccountPayload.java +++ b/core/src/main/java/haveno/core/payment/payload/PayPalAccountPayload.java @@ -36,6 +36,7 @@ import java.util.Map; @Slf4j public final class PayPalAccountPayload extends PaymentAccountPayload { private String emailOrMobileNrOrUsername = ""; + private String extraInfo = ""; public PayPalAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); @@ -48,6 +49,7 @@ public final class PayPalAccountPayload extends PaymentAccountPayload { private PayPalAccountPayload(String paymentMethod, String id, String emailOrMobileNrOrUsername, + String extraInfo, long maxTradePeriod, Map excludeFromJsonDataMap) { super(paymentMethod, @@ -56,13 +58,15 @@ public final class PayPalAccountPayload extends PaymentAccountPayload { excludeFromJsonDataMap); this.emailOrMobileNrOrUsername = emailOrMobileNrOrUsername; + this.extraInfo = extraInfo; } @Override public Message toProtoMessage() { return getPaymentAccountPayloadBuilder() .setPaypalAccountPayload(protobuf.PayPalAccountPayload.newBuilder() - .setEmailOrMobileNrOrUsername(emailOrMobileNrOrUsername)) + .setExtraInfo(extraInfo) + .setEmailOrMobileNrOrUsername(emailOrMobileNrOrUsername)) .build(); } @@ -70,6 +74,7 @@ public final class PayPalAccountPayload extends PaymentAccountPayload { return new PayPalAccountPayload(proto.getPaymentMethodId(), proto.getId(), proto.getPaypalAccountPayload().getEmailOrMobileNrOrUsername(), + proto.getPaypalAccountPayload().getExtraInfo(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } @@ -80,8 +85,8 @@ public final class PayPalAccountPayload extends PaymentAccountPayload { @Override public String getPaymentDetails() { - return Res.getWithCol("payment.email.mobile.username") + " " - + emailOrMobileNrOrUsername; + return Res.getWithCol("payment.email.mobile.username") + " "+ emailOrMobileNrOrUsername + "\n" + + Res.getWithCol("payment.shared.extraInfo") + " " + extraInfo+ "\n"; } @Override diff --git a/core/src/main/java/haveno/core/provider/price/PriceFeedService.java b/core/src/main/java/haveno/core/provider/price/PriceFeedService.java index f7a549e0..6f3566d1 100644 --- a/core/src/main/java/haveno/core/provider/price/PriceFeedService.java +++ b/core/src/main/java/haveno/core/provider/price/PriceFeedService.java @@ -33,7 +33,6 @@ import haveno.core.monetary.Price; import haveno.core.monetary.TraditionalMoney; import haveno.core.provider.PriceHttpClient; import haveno.core.provider.ProvidersRepository; -import haveno.core.trade.HavenoUtils; import haveno.core.trade.statistics.TradeStatistics3; import haveno.core.user.Preferences; import haveno.network.http.HttpClient; @@ -144,15 +143,17 @@ public class PriceFeedService { public void awaitExternalPrices() { CountDownLatch latch = new CountDownLatch(1); ChangeListener listener = (observable, oldValue, newValue) -> { - if (hasExternalPrices() && latch.getCount() != 0) latch.countDown(); + if (hasExternalPrices()) UserThread.execute(() -> latch.countDown()); }; - updateCounter.addListener(listener); - if (hasExternalPrices()) { - updateCounter.removeListener(listener); - return; + UserThread.execute(() -> updateCounter.addListener(listener)); + if (hasExternalPrices()) UserThread.execute(() -> latch.countDown()); + try { + latch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + UserThread.execute(() -> updateCounter.removeListener(listener)); } - HavenoUtils.awaitLatch(latch); - updateCounter.removeListener(listener); } public boolean hasExternalPrices() { @@ -377,17 +378,21 @@ public class PriceFeedService { */ public synchronized Map requestAllPrices() throws ExecutionException, InterruptedException, TimeoutException, CancellationException { CountDownLatch latch = new CountDownLatch(1); - ChangeListener listener = (observable, oldValue, newValue) -> { if (latch.getCount() != 0) latch.countDown(); }; - updateCounter.addListener(listener); + ChangeListener listener = (observable, oldValue, newValue) -> latch.countDown(); + UserThread.execute(() -> updateCounter.addListener(listener)); requestAllPricesError = null; requestPrices(); UserThread.runAfter(() -> { - if (latch.getCount() == 0) return; - requestAllPricesError = "Timeout fetching market prices within 20 seconds"; - latch.countDown(); + if (latch.getCount() > 0) requestAllPricesError = "Timeout fetching market prices within 20 seconds"; + UserThread.execute(() -> latch.countDown()); }, 20); - HavenoUtils.awaitLatch(latch); - updateCounter.removeListener(listener); + try { + latch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + UserThread.execute(() -> updateCounter.removeListener(listener)); + } if (requestAllPricesError != null) throw new RuntimeException(requestAllPricesError); return cache; } diff --git a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java index 42b701d3..1d8a32d3 100644 --- a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java +++ b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java @@ -476,8 +476,9 @@ public final class ArbitrationManager extends DisputeManager { - ThreadUtils.submitToPool(() -> { // TODO: remove this? - ThreadUtils.execute(() -> onConnectionChanged(connection), getConnectionChangedThreadId()); - }); + ThreadUtils.execute(() -> onConnectionChanged(connection), getId()); }); // reset buyer's payment sent state if no ack receive @@ -679,13 +677,12 @@ public abstract class Trade implements Tradable, Model { log.info("Payout published for {} {}", getClass().getSimpleName(), getId()); // sync main wallet to update pending balance - if (!isPayoutConfirmed()) { - new Thread(() -> { - HavenoUtils.waitFor(1000); - if (isShutDownStarted) return; - if (xmrConnectionService.isConnected()) syncAndPollWallet(); - }).start(); - } + ThreadUtils.submitToPool(() -> { + HavenoUtils.waitFor(1000); + if (isPayoutConfirmed()) return; + if (isShutDownStarted) return; + if (xmrConnectionService.isConnected()) syncAndPollWallet(); + }); // complete disputed trade if (getDisputeState().isArbitrated() && !getDisputeState().isClosed()) { @@ -707,7 +704,8 @@ public abstract class Trade implements Tradable, Model { if (newValue == Trade.PayoutState.PAYOUT_UNLOCKED) { if (!isInitialized) return; log.info("Payout unlocked for {} {}, deleting multisig wallet", getClass().getSimpleName(), getId()); - clearAndShutDown(); + if (isCompleted()) clearAndShutDown(); + else deleteWallet(); } }); }); @@ -790,6 +788,11 @@ public abstract class Trade implements Tradable, Model { return getArbitrator() == null ? null : getArbitrator().getNodeAddress(); } + public void setCompleted(boolean completed) { + this.isCompleted = completed; + if (isPayoutUnlocked()) clearAndShutDown(); + } + /////////////////////////////////////////////////////////////////////////////////////////// // WALLET MANAGEMENT /////////////////////////////////////////////////////////////////////////////////////////// @@ -843,6 +846,14 @@ public abstract class Trade implements Tradable, Model { } } + public boolean requestSwitchToNextBestConnection() { + if (xmrConnectionService.requestSwitchToNextBestConnection()) { + onConnectionChanged(xmrConnectionService.getConnection()); // change connection on same thread + return true; + } + return false; + } + public boolean isIdling() { return this instanceof ArbitratorTrade && isDepositsConfirmed() && walletExists() && pollNormalStartTimeMs == null; // arbitrator idles trade after deposits confirm unless overriden } @@ -880,69 +891,8 @@ public abstract class Trade implements Tradable, Model { }).start(); } - public void importMultisigHex() { - synchronized (walletLock) { - synchronized (HavenoUtils.getDaemonLock()) { // lock on daemon because import calls full refresh - for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { - try { - doImportMultisigHex(); - break; - } catch (IllegalArgumentException e) { - throw e; - } catch (Exception e) { - log.warn("Failed to import multisig hex, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage()); - if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; - HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying - } - } - } - } - } - - private void doImportMultisigHex() { - - // ensure wallet sees deposits confirmed - if (!isDepositsConfirmed()) syncAndPollWallet(); - - // collect multisig hex from peers - List multisigHexes = new ArrayList(); - for (TradePeer peer : getOtherPeers()) if (peer.getUpdatedMultisigHex() != null) multisigHexes.add(peer.getUpdatedMultisigHex()); - - // import multisig hex - log.info("Importing multisig hexes for {} {}, count={}", getClass().getSimpleName(), getShortId(), multisigHexes.size()); - long startTime = System.currentTimeMillis(); - if (!multisigHexes.isEmpty()) { - try { - wallet.importMultisigHex(multisigHexes.toArray(new String[0])); - } catch (MoneroError e) { - - // import multisig hex individually if one is invalid - if (isInvalidImportError(e.getMessage())) { - log.warn("Peer has invalid multisig hex for {} {}, importing individually", getClass().getSimpleName(), getShortId()); - boolean imported = false; - Exception lastError = null; - for (TradePeer peer : getOtherPeers()) { - if (peer.getUpdatedMultisigHex() == null) continue; - try { - wallet.importMultisigHex(peer.getUpdatedMultisigHex()); - imported = true; - } catch (MoneroError e2) { - lastError = e2; - if (isInvalidImportError(e2.getMessage())) { - log.warn("{} has invalid multisig hex for {} {}, error={}, multisigHex={}", getPeerRole(peer), getClass().getSimpleName(), getShortId(), e2.getMessage(), peer.getUpdatedMultisigHex()); - } else { - throw e2; - } - } - } - if (!imported) throw new IllegalArgumentException("Could not import any multisig hexes for " + getClass().getSimpleName() + " " + getShortId(), lastError); - } else { - throw e; - } - } - requestSaveWallet(); - } - log.info("Done importing multisig hexes for {} {} in {} ms, count={}", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime, multisigHexes.size()); + private boolean isReadTimeoutError(String errMsg) { + return errMsg.contains("Read timed out"); } // TODO: checking error strings isn't robust, but the library doesn't provide a way to check if multisig hex is invalid. throw IllegalArgumentException from library on invalid multisig hex? @@ -958,7 +908,13 @@ public abstract class Trade implements Tradable, Model { } public void requestSaveWallet() { - ThreadUtils.submitToPool(() -> saveWallet()); // save wallet off main thread + + // save wallet off main thread + ThreadUtils.execute(() -> { + synchronized (walletLock) { + if (walletExists()) saveWallet(); + } + }, getId()); } public void saveWallet() { @@ -996,7 +952,11 @@ public abstract class Trade implements Tradable, Model { private void forceCloseWallet() { if (wallet != null) { - xmrWalletService.forceCloseWallet(wallet, wallet.getPath()); + try { + xmrWalletService.forceCloseWallet(wallet, wallet.getPath()); + } catch (Exception e) { + log.warn("Error force closing wallet for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage()); + } stopPolling(); wallet = null; } @@ -1007,29 +967,31 @@ public abstract class Trade implements Tradable, Model { if (walletExists()) { try { - // ensure wallet is initialized - boolean syncedWallet = false; - if (wallet == null) { - log.warn("Wallet is not initialized for {} {}, opening", getClass().getSimpleName(), getId()); - getWallet(); - syncWallet(true); - syncedWallet = true; - } + // check wallet state if deposit requested + if (isDepositRequested()) { - // sync wallet if deposit requested and payout not unlocked - if (isDepositRequested() && !isPayoutUnlocked() && !syncedWallet) { - log.warn("Syncing wallet on deletion for trade {} {}, syncing", getClass().getSimpleName(), getId()); - syncWallet(true); - } - - // check if deposits published and payout not unlocked - if (isDepositsPublished() && !isPayoutUnlocked()) { - throw new IllegalStateException("Refusing to delete wallet for " + getClass().getSimpleName() + " " + getId() + " because the deposit txs have been published but payout tx has not unlocked"); - } - - // check for balance - if (wallet.getBalance().compareTo(BigInteger.ZERO) > 0) { - synchronized (HavenoUtils.getDaemonLock()) { + // ensure wallet is initialized + boolean syncedWallet = false; + if (wallet == null) { + log.warn("Wallet is not initialized for {} {}, opening", getClass().getSimpleName(), getId()); + getWallet(); + syncWallet(true); + syncedWallet = true; + } + + // sync wallet if deposit requested and payout not unlocked + if (!isPayoutUnlocked() && !syncedWallet) { + log.warn("Syncing wallet on deletion for trade {} {}, syncing", getClass().getSimpleName(), getId()); + syncWallet(true); + } + + // check if deposits published and payout not unlocked + if (isDepositsPublished() && !isPayoutUnlocked()) { + throw new IllegalStateException("Refusing to delete wallet for " + getClass().getSimpleName() + " " + getId() + " because the deposit txs have been published but payout tx has not unlocked"); + } + + // check for balance + if (wallet.getBalance().compareTo(BigInteger.ZERO) > 0) { log.warn("Rescanning spent outputs for {} {}", getClass().getSimpleName(), getId()); wallet.rescanSpent(); if (wallet.getBalance().compareTo(BigInteger.ZERO) > 0) { @@ -1101,6 +1063,104 @@ public abstract class Trade implements Tradable, Model { } } + public void importMultisigHex() { + synchronized (walletLock) { + synchronized (HavenoUtils.getDaemonLock()) { // lock on daemon because import calls full refresh + for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { + try { + doImportMultisigHex(); + break; + } catch (IllegalArgumentException | IllegalStateException e) { + throw e; + } catch (Exception e) { + log.warn("Failed to import multisig hex, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); + if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection(); + if (isReadTimeoutError(e.getMessage())) forceRestartTradeWallet(); // wallet can be stuck a while + HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying + } + } + } + } + } + + private void doImportMultisigHex() { + + // ensure wallet sees deposits confirmed + if (!isDepositsConfirmed()) syncAndPollWallet(); + + // collect multisig hex from peers + List multisigHexes = new ArrayList(); + for (TradePeer peer : getOtherPeers()) if (peer.getUpdatedMultisigHex() != null) multisigHexes.add(peer.getUpdatedMultisigHex()); + + // import multisig hex + log.info("Importing multisig hexes for {} {}, count={}", getClass().getSimpleName(), getShortId(), multisigHexes.size()); + long startTime = System.currentTimeMillis(); + if (!multisigHexes.isEmpty()) { + try { + wallet.importMultisigHex(multisigHexes.toArray(new String[0])); + + // check if import is still needed // TODO: we once received a multisig hex which was too short, causing import to still be needed + if (wallet.isMultisigImportNeeded()) { + String errorMessage = "Multisig import still needed for " + getClass().getSimpleName() + " " + getShortId() + " after already importing, multisigHexes=" + multisigHexes; + log.warn(errorMessage); + + // ignore multisig hex which is significantly shorter than others + int maxLength = 0; + boolean removed = false; + for (String hex : multisigHexes) maxLength = Math.max(maxLength, hex.length()); + for (String hex : new ArrayList<>(multisigHexes)) { + if (hex.length() < maxLength / 2) { + String ignoringMessage = "Ignoring multisig hex from " + getMultisigHexRole(hex) + " for " + getClass().getSimpleName() + " " + getShortId() + " because it is too short, multisigHex=" + hex; + setErrorMessage(ignoringMessage); + log.warn(ignoringMessage); + multisigHexes.remove(hex); + removed = true; + } + } + + // re-import valid multisig hexes + if (removed) wallet.importMultisigHex(multisigHexes.toArray(new String[0])); + if (wallet.isMultisigImportNeeded()) throw new IllegalStateException(errorMessage); + } + } catch (MoneroError e) { + + // import multisig hex individually if one is invalid + if (isInvalidImportError(e.getMessage())) { + log.warn("Peer has invalid multisig hex for {} {}, importing individually", getClass().getSimpleName(), getShortId()); + boolean imported = false; + Exception lastError = null; + for (TradePeer peer : getOtherPeers()) { + if (peer.getUpdatedMultisigHex() == null) continue; + try { + wallet.importMultisigHex(peer.getUpdatedMultisigHex()); + imported = true; + } catch (MoneroError e2) { + lastError = e2; + if (isInvalidImportError(e2.getMessage())) { + log.warn("{} has invalid multisig hex for {} {}, error={}, multisigHex={}", getPeerRole(peer), getClass().getSimpleName(), getShortId(), e2.getMessage(), peer.getUpdatedMultisigHex()); + } else { + throw e2; + } + } + } + if (!imported) throw new IllegalArgumentException("Could not import any multisig hexes for " + getClass().getSimpleName() + " " + getShortId(), lastError); + } else { + throw e; + } + } + requestSaveWallet(); + } + log.info("Done importing multisig hexes for {} {} in {} ms, count={}", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime, multisigHexes.size()); + } + + private String getMultisigHexRole(String multisigHex) { + if (multisigHex.equals(getArbitrator().getUpdatedMultisigHex())) return "arbitrator"; + if (multisigHex.equals(getBuyer().getUpdatedMultisigHex())) return "buyer"; + if (multisigHex.equals(getSeller().getUpdatedMultisigHex())) return "seller"; + throw new IllegalArgumentException("Multisig hex does not belong to any peer"); + } + /** * Create the payout tx. * @@ -1117,9 +1177,12 @@ public abstract class Trade implements Tradable, Model { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { try { return doCreatePayoutTx(); + } catch (IllegalArgumentException | IllegalStateException e) { + throw e; } catch (Exception e) { - log.warn("Failed to create payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage()); + log.warn("Failed to create payout tx, tradeId={}, attempt={}/{}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection(); HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } } @@ -1131,14 +1194,10 @@ public abstract class Trade implements Tradable, Model { private MoneroTxWallet doCreatePayoutTx() { // check if multisig import needed - if (wallet.isMultisigImportNeeded()) throw new RuntimeException("Cannot create payout tx because multisig import is needed"); + if (wallet.isMultisigImportNeeded()) throw new IllegalStateException("Cannot create payout tx because multisig import is needed for " + getClass().getSimpleName() + " " + getShortId()); - // TODO: wallet sometimes returns empty data, after disconnect? - List txs = wallet.getTxs(); // TODO: this fetches from pool - if (txs.isEmpty()) { - log.warn("Restarting wallet for {} {} because deposit txs are missing to create payout tx", getClass().getSimpleName(), getId()); - forceRestartTradeWallet(); - } + // recover if missing wallet data + recoverIfMissingWalletData(); // gather info String sellerPayoutAddress = getSeller().getPayoutAddressString(); @@ -1176,11 +1235,15 @@ public abstract class Trade implements Tradable, Model { synchronized (HavenoUtils.getWalletFunctionLock()) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { try { + if (wallet.isMultisigImportNeeded()) throw new IllegalStateException("Cannot create dispute payout tx because multisig import is needed for " + getClass().getSimpleName() + " " + getShortId()); return createTx(txConfig); + } catch (IllegalArgumentException | IllegalStateException e) { + throw e; } catch (Exception e) { - if (e.getMessage().contains("not possible")) throw new RuntimeException("Loser payout is too small to cover the mining fee"); - log.warn("Failed to create dispute payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage()); + if (e.getMessage().contains("not possible")) throw new IllegalArgumentException("Loser payout is too small to cover the mining fee"); + log.warn("Failed to create dispute payout tx, tradeId={}, attempt={}/{}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection(); HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } } @@ -1197,23 +1260,30 @@ public abstract class Trade implements Tradable, Model { * @param publish publishes the signed payout tx if true */ public void processPayoutTx(String payoutTxHex, boolean sign, boolean publish) { - log.info("Processing payout tx for {} {}", getClass().getSimpleName(), getId()); - - // TODO: wallet sometimes returns empty data, after disconnect? detect this condition with failure tolerance - for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { - try { - List txs = wallet.getTxs(); // TODO: this fetches from pool - if (txs.isEmpty()) { - log.warn("Restarting wallet for {} {} because deposit txs are missing to process payout tx", getClass().getSimpleName(), getId()); - forceRestartTradeWallet(); + synchronized (walletLock) { + synchronized (HavenoUtils.getWalletFunctionLock()) { + for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { + try { + doProcessPayoutTx(payoutTxHex, sign, publish); + break; + } catch (IllegalArgumentException | IllegalStateException e) { + throw e; + } catch (Exception e) { + log.warn("Failed to process payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage()); + if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection(); + HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying + } } - break; - } catch (Exception e) { - log.warn("Failed get wallet txs, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage()); - if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; - HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } } + } + + private void doProcessPayoutTx(String payoutTxHex, boolean sign, boolean publish) { + log.info("Processing payout tx for {} {}", getClass().getSimpleName(), getId()); + + // recover if missing wallet data + recoverIfMissingWalletData(); // gather relevant info MoneroWallet wallet = getWallet(); @@ -1226,6 +1296,7 @@ public abstract class Trade implements Tradable, Model { MoneroTxSet describedTxSet = wallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex)); if (describedTxSet.getTxs() == null || describedTxSet.getTxs().size() != 1) throw new IllegalArgumentException("Bad payout tx"); // TODO (woodser): test nack MoneroTxWallet payoutTx = describedTxSet.getTxs().get(0); + if (payoutTxId == null) updatePayout(payoutTx); // update payout tx if not signed // verify payout tx has exactly 2 destinations if (payoutTx.getOutgoingTransfer() == null || payoutTx.getOutgoingTransfer().getDestinations() == null || payoutTx.getOutgoingTransfer().getDestinations().size() != 2) throw new IllegalArgumentException("Payout tx does not have exactly two destinations"); @@ -1257,10 +1328,11 @@ public abstract class Trade implements Tradable, Model { if (!sellerPayoutDestination.getAmount().equals(expectedSellerPayout)) throw new IllegalArgumentException("Seller destination amount is not deposit amount - trade amount - 1/2 tx costs, " + sellerPayoutDestination.getAmount() + " vs " + expectedSellerPayout); // check connection - if (sign || publish) verifyDaemonConnection(); + boolean doSign = sign && getPayoutTxHex() == null; + if (doSign || publish) verifyDaemonConnection(); // handle tx signing - if (sign) { + if (doSign) { // sign tx try { @@ -1275,6 +1347,7 @@ public abstract class Trade implements Tradable, Model { // describe result describedTxSet = wallet.describeMultisigTxSet(payoutTxHex); payoutTx = describedTxSet.getTxs().get(0); + updatePayout(payoutTx); // verify fee is within tolerance by recreating payout tx // TODO (monero-project): creating tx will require exchanging updated multisig hex if message needs reprocessed. provide weight with describe_transfer so fee can be estimated? @@ -1286,22 +1359,16 @@ public abstract class Trade implements Tradable, Model { log.info("Payout tx fee {} is within tolerance, diff %={}", payoutTx.getFee(), feeDiff); } - // update trade state - updatePayout(payoutTx); + // save trade state requestPersistence(); // submit payout tx if (publish) { - for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { - try { - wallet.submitMultisigTxHex(payoutTxHex); - ThreadUtils.submitToPool(() -> pollWallet()); - break; - } catch (Exception e) { - log.warn("Failed to submit payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage()); - if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; - HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying - } + try { + wallet.submitMultisigTxHex(payoutTxHex); + setPayoutStatePublished(); + } catch (Exception e) { + throw new RuntimeException("Failed to submit payout tx for " + getClass().getSimpleName() + " " + getId(), e); } } } @@ -1436,7 +1503,6 @@ public abstract class Trade implements Tradable, Model { isShutDown = true; List shutDownThreads = new ArrayList<>(); shutDownThreads.add(() -> ThreadUtils.shutDown(getId())); - shutDownThreads.add(() -> ThreadUtils.shutDown(getConnectionChangedThreadId())); ThreadUtils.awaitTasks(shutDownThreads); } @@ -1592,6 +1658,17 @@ public abstract class Trade implements Tradable, Model { private void removeTradeOnError() { log.warn("removeTradeOnError() trade={}, tradeId={}, state={}", getClass().getSimpleName(), getShortId(), getState()); + // force close and re-open wallet in case stuck + forceCloseWallet(); + if (isDepositRequested()) getWallet(); + + // 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()); + } + // clear and shut down trade clearAndShutDown(); @@ -2236,10 +2313,6 @@ public abstract class Trade implements Tradable, Model { // Private /////////////////////////////////////////////////////////////////////////////////////////// - private String getConnectionChangedThreadId() { - return getId() + ".onConnectionChanged"; - } - // lazy initialization private ObjectProperty getAmountProperty() { if (tradeAmountProperty == null) @@ -2258,6 +2331,9 @@ public abstract class Trade implements Tradable, Model { private void onConnectionChanged(MoneroRpcConnection connection) { synchronized (walletLock) { + // use current connection + connection = xmrConnectionService.getConnection(); + // check if ignored if (isShutDownStarted) return; if (getWallet() == null) return; @@ -2321,24 +2397,29 @@ public abstract class Trade implements Tradable, Model { } private void syncWallet(boolean pollWallet) { - if (getWallet() == null) throw new RuntimeException("Cannot sync trade wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId()); - if (getWallet().getDaemonConnection() == null) throw new RuntimeException("Cannot sync trade wallet because it's not connected to a Monero daemon for " + getClass().getSimpleName() + ", " + getId()); - if (isWalletBehind()) { - log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getShortId()); - long startTime = System.currentTimeMillis(); - syncWalletIfBehind(); - log.info("Done syncing wallet for {} {} in {} ms", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime); - } - - // apply tor after wallet synced depending on configuration - if (!wasWalletSynced) { - wasWalletSynced = true; - if (xmrWalletService.isProxyApplied(wasWalletSynced)) { - onConnectionChanged(xmrConnectionService.getConnection()); + try { + if (getWallet() == null) throw new RuntimeException("Cannot sync trade wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId()); + if (getWallet().getDaemonConnection() == null) throw new RuntimeException("Cannot sync trade wallet because it's not connected to a Monero daemon for " + getClass().getSimpleName() + ", " + getId()); + if (isWalletBehind()) { + log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getShortId()); + long startTime = System.currentTimeMillis(); + syncWalletIfBehind(); + log.info("Done syncing wallet for {} {} in {} ms", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime); } + + // apply tor after wallet synced depending on configuration + if (!wasWalletSynced) { + wasWalletSynced = true; + if (xmrWalletService.isProxyApplied(wasWalletSynced)) { + onConnectionChanged(xmrConnectionService.getConnection()); + } + } + + if (pollWallet) pollWallet(); + } catch (Exception e) { + ThreadUtils.execute(() -> requestSwitchToNextBestConnection(), getId()); + throw e; } - - if (pollWallet) pollWallet(); } public void updatePollPeriod() { @@ -2347,11 +2428,11 @@ public abstract class Trade implements Tradable, Model { } private void setPollPeriod(long pollPeriodMs) { - synchronized (walletLock) { + synchronized (pollLock) { if (this.isShutDownStarted) return; if (this.pollPeriodMs != null && this.pollPeriodMs == pollPeriodMs) return; this.pollPeriodMs = pollPeriodMs; - if (isPollInProgress()) { + if (isPolling()) { stopPolling(); startPolling(); } @@ -2364,8 +2445,8 @@ public abstract class Trade implements Tradable, Model { } private void startPolling() { - synchronized (walletLock) { - if (isShutDownStarted || isPollInProgress()) return; + synchronized (pollLock) { + if (isShutDownStarted || isPolling()) return; updatePollPeriod(); log.info("Starting to poll wallet for {} {}", getClass().getSimpleName(), getId()); pollLooper = new TaskLooper(() -> pollWallet()); @@ -2374,151 +2455,160 @@ public abstract class Trade implements Tradable, Model { } private void stopPolling() { - synchronized (walletLock) { - if (isPollInProgress()) { + synchronized (pollLock) { + if (isPolling()) { pollLooper.stop(); pollLooper = null; } } } - private boolean isPollInProgress() { - synchronized (walletLock) { + private boolean isPolling() { + synchronized (pollLock) { return pollLooper != null; } } private void pollWallet() { - if (pollInProgress) return; + synchronized (pollLock) { + if (pollInProgress) return; + } doPollWallet(); } private void doPollWallet() { + if (isShutDownStarted) return; synchronized (pollLock) { pollInProgress = true; - try { + } + try { - // skip if payout unlocked - if (isPayoutUnlocked()) return; + // skip if payout unlocked + if (isPayoutUnlocked()) return; - // skip if deposit txs unknown or not requested - if (processModel.getMaker().getDepositTxHash() == null || processModel.getTaker().getDepositTxHash() == null || !isDepositRequested()) return; + // skip if deposit txs unknown or not requested + if (processModel.getMaker().getDepositTxHash() == null || processModel.getTaker().getDepositTxHash() == null || !isDepositRequested()) return; - // sync if wallet too far behind daemon - if (xmrConnectionService.getTargetHeight() == null) return; - if (walletHeight.get() < xmrConnectionService.getTargetHeight() - SYNC_EVERY_NUM_BLOCKS) syncWallet(false); + // skip if daemon not synced + if (xmrConnectionService.getTargetHeight() == null || !xmrConnectionService.isSyncedWithinTolerance()) return; - // update deposit txs - if (!isDepositsUnlocked()) { + // sync if wallet too far behind daemon + if (walletHeight.get() < xmrConnectionService.getTargetHeight() - SYNC_EVERY_NUM_BLOCKS) syncWallet(false); - // sync wallet if behind - syncWalletIfBehind(); + // update deposit txs + if (!isDepositsUnlocked()) { - // get txs from trade wallet - MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true); - Boolean updatePool = !isDepositsConfirmed() && (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null); - if (!updatePool) query.setInTxPool(false); // avoid updating from pool if possible - List txs; - if (!updatePool) txs = wallet.getTxs(query); - else { - synchronized (walletLock) { - synchronized (HavenoUtils.getDaemonLock()) { - txs = wallet.getTxs(query); - } - } - } - setDepositTxs(txs); - if (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null) return; // skip if either deposit tx not seen - setStateDepositsSeen(); + // sync wallet if behind + syncWalletIfBehind(); - // set actual security deposits - if (getBuyer().getSecurityDeposit().longValueExact() == 0) { - BigInteger buyerSecurityDeposit = ((MoneroTxWallet) getBuyer().getDepositTx()).getIncomingAmount(); - BigInteger sellerSecurityDeposit = ((MoneroTxWallet) getSeller().getDepositTx()).getIncomingAmount().subtract(getAmount()); - getBuyer().setSecurityDeposit(buyerSecurityDeposit); - getSeller().setSecurityDeposit(sellerSecurityDeposit); - } - - // check for deposit txs confirmation - if (getMaker().getDepositTx().isConfirmed() && getTaker().getDepositTx().isConfirmed()) setStateDepositsConfirmed(); - - // check for deposit txs unlocked - if (getMaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK && getTaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK) { - setStateDepositsUnlocked(); - } - } - - // check for payout tx - if (isDepositsUnlocked()) { - - // determine if payout tx expected - boolean isPayoutExpected = isPaymentReceived() || hasPaymentReceivedMessage() || hasDisputeClosedMessage() || disputeState.ordinal() >= DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG.ordinal(); - - // sync wallet if payout expected or payout is published - if (isPayoutExpected || isPayoutPublished()) syncWalletIfBehind(); - - // rescan spent outputs to detect unconfirmed payout tx - if (isPayoutExpected && wallet.getBalance().compareTo(BigInteger.ZERO) > 0) { - try { - wallet.rescanSpent(); - } catch (Exception e) { - log.warn("Error rescanning spent outputs to detect payout tx for {} {}, errorMessage={}", getClass().getSimpleName(), getShortId(), e.getMessage()); - } - } - - // get txs from trade wallet - MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true); - boolean updatePool = isPayoutExpected && !isPayoutConfirmed(); - if (!updatePool) query.setInTxPool(false); // avoid updating from pool if possible - List txs = null; - if (!updatePool) txs = wallet.getTxs(query); - else { - synchronized (walletLock) { - synchronized (HavenoUtils.getDaemonLock()) { - txs = wallet.getTxs(query); - } - } - } - setDepositTxs(txs); - - // check if any outputs spent (observed on payout published) - boolean hasSpentOutput = false; - boolean hasFailedTx = false; - for (MoneroTxWallet tx : txs) { - if (tx.isFailed()) hasFailedTx = true; - for (MoneroOutputWallet output : tx.getOutputsWallet()) { - if (Boolean.TRUE.equals(output.isSpent())) hasSpentOutput = true; - } - } - if (hasSpentOutput) setPayoutStatePublished(); - else if (hasFailedTx && isPayoutPublished()) { - log.warn("{} {} is in payout published state but has failed tx and no spent outputs, resetting payout state to unpublished", getClass().getSimpleName(), getShortId()); - setPayoutState(PayoutState.PAYOUT_UNPUBLISHED); - } - - // check for outgoing txs (appears after wallet submits payout tx or on payout confirmed) - for (MoneroTxWallet tx : txs) { - if (tx.isOutgoing() && !tx.isFailed()) { - updatePayout(tx); - setPayoutStatePublished(); - if (tx.isConfirmed()) setPayoutStateConfirmed(); - if (!tx.isLocked()) setPayoutStateUnlocked(); - } - } - } - } catch (Exception e) { - boolean isConnectionRefused = e.getMessage() != null && e.getMessage().contains("Connection refused"); - if (isConnectionRefused) forceRestartTradeWallet(); + // get txs from trade wallet + MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true); + Boolean updatePool = !isDepositsConfirmed() && (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null); + if (!updatePool) query.setInTxPool(false); // avoid updating from pool if possible + List txs; + if (!updatePool) txs = wallet.getTxs(query); else { - boolean isWalletConnected = isWalletConnectedToDaemon(); - if (!isShutDownStarted && wallet != null && isWalletConnected) { - log.warn("Error polling trade wallet for {} {}, errorMessage={}. Monerod={}", getClass().getSimpleName(), getShortId(), e.getMessage(), getXmrWalletService().getConnectionService().getConnection()); - //e.printStackTrace(); + synchronized (walletLock) { + synchronized (HavenoUtils.getDaemonLock()) { + txs = wallet.getTxs(query); + } } } - } finally { + setDepositTxs(txs); + if (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null) return; // skip if either deposit tx not seen + setStateDepositsSeen(); + + // set actual security deposits + if (getBuyer().getSecurityDeposit().longValueExact() == 0) { + BigInteger buyerSecurityDeposit = ((MoneroTxWallet) getBuyer().getDepositTx()).getIncomingAmount(); + BigInteger sellerSecurityDeposit = ((MoneroTxWallet) getSeller().getDepositTx()).getIncomingAmount().subtract(getAmount()); + getBuyer().setSecurityDeposit(buyerSecurityDeposit); + getSeller().setSecurityDeposit(sellerSecurityDeposit); + } + + // check for deposit txs confirmation + if (getMaker().getDepositTx().isConfirmed() && getTaker().getDepositTx().isConfirmed()) setStateDepositsConfirmed(); + + // check for deposit txs unlocked + if (getMaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK && getTaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK) { + setStateDepositsUnlocked(); + } + } + + // check for payout tx + if (isDepositsUnlocked()) { + + // determine if payout tx expected + boolean isPayoutExpected = isPaymentReceived() || hasPaymentReceivedMessage() || hasDisputeClosedMessage() || disputeState.ordinal() >= DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG.ordinal(); + + // sync wallet if payout expected or payout is published + if (isPayoutExpected || isPayoutPublished()) syncWalletIfBehind(); + + // rescan spent outputs to detect unconfirmed payout tx + if (isPayoutExpected && wallet.getBalance().compareTo(BigInteger.ZERO) > 0) { + try { + wallet.rescanSpent(); + } catch (Exception e) { + log.warn("Failed to rescan spent outputs for {} {}, errorMessage={}", getClass().getSimpleName(), getShortId(), e.getMessage()); + ThreadUtils.execute(() -> requestSwitchToNextBestConnection(), getId()); // do not block polling thread + } + } + + // get txs from trade wallet + MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true); + boolean updatePool = isPayoutExpected && !isPayoutConfirmed(); + if (!updatePool) query.setInTxPool(false); // avoid updating from pool if possible + List txs = null; + if (!updatePool) txs = wallet.getTxs(query); + else { + synchronized (walletLock) { + synchronized (HavenoUtils.getDaemonLock()) { + txs = wallet.getTxs(query); + } + } + } + setDepositTxs(txs); + + // check if any outputs spent (observed on payout published) + boolean hasSpentOutput = false; + boolean hasFailedTx = false; + for (MoneroTxWallet tx : txs) { + if (tx.isFailed()) hasFailedTx = true; + for (MoneroOutputWallet output : tx.getOutputsWallet()) { + if (Boolean.TRUE.equals(output.isSpent())) hasSpentOutput = true; + } + } + if (hasSpentOutput) setPayoutStatePublished(); + else if (hasFailedTx && isPayoutPublished()) { + log.warn("{} {} is in payout published state but has failed tx and no spent outputs, resetting payout state to unpublished", getClass().getSimpleName(), getShortId()); + setPayoutState(PayoutState.PAYOUT_UNPUBLISHED); + } + + // check for outgoing txs (appears after wallet submits payout tx or on payout confirmed) + for (MoneroTxWallet tx : txs) { + if (tx.isOutgoing() && !tx.isFailed()) { + updatePayout(tx); + setPayoutStatePublished(); + if (tx.isConfirmed()) setPayoutStateConfirmed(); + if (!tx.isLocked()) setPayoutStateUnlocked(); + } + } + } + } catch (Exception e) { + boolean isConnectionRefused = e.getMessage() != null && e.getMessage().contains("Connection refused"); + if (isConnectionRefused) forceRestartTradeWallet(); + else { + boolean isWalletConnected = isWalletConnectedToDaemon(); + if (wallet != null && !isShutDownStarted && isWalletConnected) { + log.warn("Error polling trade wallet for {} {}, errorMessage={}. Monerod={}", getClass().getSimpleName(), getShortId(), e.getMessage(), getXmrWalletService().getConnectionService().getConnection()); + //e.printStackTrace(); + } + } + } finally { + synchronized (pollLock) { pollInProgress = false; } + requestSaveWallet(); } } @@ -2543,6 +2633,78 @@ public abstract class Trade implements Tradable, Model { depositTxsUpdateCounter.set(depositTxsUpdateCounter.get() + 1); } + // TODO: wallet is sometimes missing balance or deposits, due to specific daemon connections, not saving? + private void recoverIfMissingWalletData() { + synchronized (walletLock) { + if (isWalletMissingData()) { + log.warn("Wallet is missing data for {} {}, attempting to recover", getClass().getSimpleName(), getShortId()); + + // force restart wallet + forceRestartTradeWallet(); + + // skip if payout published in the meantime + if (isPayoutPublished()) return; + + // rescan blockchain with global daemon lock + synchronized (HavenoUtils.getDaemonLock()) { + Long timeout = null; + try { + + // extend rpc timeout for rescan + if (wallet instanceof MoneroWalletRpc) { + timeout = ((MoneroWalletRpc) wallet).getRpcConnection().getTimeout(); + ((MoneroWalletRpc) wallet).getRpcConnection().setTimeout(EXTENDED_RPC_TIMEOUT); + } + + // rescan blockchain + log.warn("Rescanning blockchain for {} {}", getClass().getSimpleName(), getShortId()); + wallet.rescanBlockchain(); + } catch (Exception e) { + if (isReadTimeoutError(e.getMessage())) forceRestartTradeWallet(); // wallet can be stuck a while + throw e; + } finally { + + // restore rpc timeout + if (wallet instanceof MoneroWalletRpc) { + ((MoneroWalletRpc) wallet).getRpcConnection().setTimeout(timeout); + } + } + } + + // import multisig hex + log.warn("Importing multisig hex to recover wallet data for {} {}", getClass().getSimpleName(), getShortId()); + importMultisigHex(); + + // poll wallet + doPollWallet(); + + // check again if missing data + if (isWalletMissingData()) throw new IllegalStateException("Wallet is still missing data after attempting recovery for " + getClass().getSimpleName() + " " + getShortId()); + } + } + } + + private boolean isWalletMissingData() { + synchronized (walletLock) { + if (!isDepositsUnlocked() || isPayoutPublished()) return false; + if (getMakerDepositTx() == null) { + log.warn("Missing maker deposit tx for {} {}", getClass().getSimpleName(), getId()); + return true; + } + if (getTakerDepositTx() == null) { + log.warn("Missing taker deposit tx for {} {}", getClass().getSimpleName(), getId()); + return true; + } + if (wallet.getBalance().equals(BigInteger.ZERO)) { + doPollWallet(); // poll once more to be sure + if (isPayoutPublished()) return false; // payout can become published while checking balance + log.warn("Wallet balance is zero for {} {}", getClass().getSimpleName(), getId()); + return true; + } + return false; + } + } + private void forceRestartTradeWallet() { if (isShutDownStarted || restartInProgress) return; log.warn("Force restarting trade wallet for {} {}", getClass().getSimpleName(), getId()); @@ -2550,7 +2712,7 @@ public abstract class Trade implements Tradable, Model { forceCloseWallet(); if (!isShutDownStarted) wallet = getWallet(); restartInProgress = false; - doPollWallet(); + pollWallet(); if (!isShutDownStarted) ThreadUtils.execute(() -> tryInitPolling(), getId()); } diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index 54480b9d..b88330a3 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -284,23 +284,25 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi /////////////////////////////////////////////////////////////////////////////////////////// @Override - public void onDirectMessage(DecryptedMessageWithPubKey message, NodeAddress peer) { + public void onDirectMessage(DecryptedMessageWithPubKey message, NodeAddress sender) { NetworkEnvelope networkEnvelope = message.getNetworkEnvelope(); if (!(networkEnvelope instanceof TradeMessage)) return; - String tradeId = ((TradeMessage) networkEnvelope).getOfferId(); + TradeMessage tradeMessage = (TradeMessage) networkEnvelope; + String tradeId = tradeMessage.getOfferId(); + log.info("TradeManager received {} for tradeId={}, sender={}, uid={}", networkEnvelope.getClass().getSimpleName(), tradeId, sender, tradeMessage.getUid()); ThreadUtils.execute(() -> { if (networkEnvelope instanceof InitTradeRequest) { - handleInitTradeRequest((InitTradeRequest) networkEnvelope, peer); + handleInitTradeRequest((InitTradeRequest) networkEnvelope, sender); } else if (networkEnvelope instanceof InitMultisigRequest) { - handleInitMultisigRequest((InitMultisigRequest) networkEnvelope, peer); + handleInitMultisigRequest((InitMultisigRequest) networkEnvelope, sender); } else if (networkEnvelope instanceof SignContractRequest) { - handleSignContractRequest((SignContractRequest) networkEnvelope, peer); + handleSignContractRequest((SignContractRequest) networkEnvelope, sender); } else if (networkEnvelope instanceof SignContractResponse) { - handleSignContractResponse((SignContractResponse) networkEnvelope, peer); + handleSignContractResponse((SignContractResponse) networkEnvelope, sender); } else if (networkEnvelope instanceof DepositRequest) { - handleDepositRequest((DepositRequest) networkEnvelope, peer); + handleDepositRequest((DepositRequest) networkEnvelope, sender); } else if (networkEnvelope instanceof DepositResponse) { - handleDepositResponse((DepositResponse) networkEnvelope, peer); + handleDepositResponse((DepositResponse) networkEnvelope, sender); } }, tradeId); } @@ -538,7 +540,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } private void handleInitTradeRequest(InitTradeRequest request, NodeAddress sender) { - log.info("Received InitTradeRequest from {} with tradeId {} and uid {}", sender, request.getOfferId(), request.getUid()); + log.info("TradeManager handling InitTradeRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); try { Validator.nonEmptyStringOf(request.getOfferId()); @@ -734,8 +736,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } } - private void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress peer) { - log.info("Received {} for trade {} from {} with uid {}", request.getClass().getSimpleName(), request.getOfferId(), peer, request.getUid()); + private void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) { + log.info("TradeManager handling InitMultisigRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); try { Validator.nonEmptyStringOf(request.getOfferId()); @@ -750,11 +752,11 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi return; } Trade trade = tradeOptional.get(); - getTradeProtocol(trade).handleInitMultisigRequest(request, peer); + getTradeProtocol(trade).handleInitMultisigRequest(request, sender); } - private void handleSignContractRequest(SignContractRequest request, NodeAddress peer) { - log.info("Received {} for trade {} from {} with uid {}", request.getClass().getSimpleName(), request.getOfferId(), peer, request.getUid()); + private void handleSignContractRequest(SignContractRequest request, NodeAddress sender) { + log.info("TradeManager handling SignContractRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); try { Validator.nonEmptyStringOf(request.getOfferId()); @@ -769,11 +771,11 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi return; } Trade trade = tradeOptional.get(); - getTradeProtocol(trade).handleSignContractRequest(request, peer); + getTradeProtocol(trade).handleSignContractRequest(request, sender); } - private void handleSignContractResponse(SignContractResponse request, NodeAddress peer) { - log.info("Received {} for trade {} from {} with uid {}", request.getClass().getSimpleName(), request.getOfferId(), peer, request.getUid()); + private void handleSignContractResponse(SignContractResponse request, NodeAddress sender) { + log.info("TradeManager handling SignContractResponse for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); try { Validator.nonEmptyStringOf(request.getOfferId()); @@ -788,11 +790,11 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi return; } Trade trade = tradeOptional.get(); - ((TraderProtocol) getTradeProtocol(trade)).handleSignContractResponse(request, peer); + ((TraderProtocol) getTradeProtocol(trade)).handleSignContractResponse(request, sender); } - private void handleDepositRequest(DepositRequest request, NodeAddress peer) { - log.info("Received {} for trade {} from {} with uid {}", request.getClass().getSimpleName(), request.getOfferId(), peer, request.getUid()); + private void handleDepositRequest(DepositRequest request, NodeAddress sender) { + log.info("TradeManager handling DepositRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid()); try { Validator.nonEmptyStringOf(request.getOfferId()); @@ -807,11 +809,11 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi return; } Trade trade = tradeOptional.get(); - ((ArbitratorProtocol) getTradeProtocol(trade)).handleDepositRequest(request, peer); + ((ArbitratorProtocol) getTradeProtocol(trade)).handleDepositRequest(request, sender); } - private void handleDepositResponse(DepositResponse response, NodeAddress peer) { - log.info("Received {} for trade {} from {} with uid {}", response.getClass().getSimpleName(), response.getOfferId(), peer, response.getUid()); + private void handleDepositResponse(DepositResponse response, NodeAddress sender) { + log.info("TradeManager handling DepositResponse for tradeId={}, sender={}, uid={}", response.getOfferId(), sender, response.getUid()); try { Validator.nonEmptyStringOf(response.getOfferId()); @@ -829,7 +831,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } } Trade trade = tradeOptional.get(); - ((TraderProtocol) getTradeProtocol(trade)).handleDepositResponse(response, peer); + ((TraderProtocol) getTradeProtocol(trade)).handleDepositResponse(response, sender); } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/haveno/core/trade/TradeUtil.java b/core/src/main/java/haveno/core/trade/TradeUtil.java index 1b55a6b2..bfebca48 100644 --- a/core/src/main/java/haveno/core/trade/TradeUtil.java +++ b/core/src/main/java/haveno/core/trade/TradeUtil.java @@ -172,7 +172,7 @@ public class TradeUtil { * @param trade Trade * @return String describing a trader's role for a given trade */ - public String getRole(Trade trade) { + public static String getRole(Trade trade) { Offer offer = trade.getOffer(); if (offer == null) throw new IllegalStateException(format("could not get role because no offer was found for trade '%s'", @@ -191,7 +191,7 @@ public class TradeUtil { * @param currencyCode String * @return String describing a trader's role */ - public String getRole(boolean isBuyerMakerAndSellerTaker, boolean isMaker, String currencyCode) { + private static String getRole(boolean isBuyerMakerAndSellerTaker, boolean isMaker, String currencyCode) { if (isTraditionalCurrency(currencyCode)) { String baseCurrencyCode = Res.getBaseCurrencyCode(); if (isBuyerMakerAndSellerTaker) diff --git a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java index 0ae5dba3..7b8b5e7d 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -296,7 +296,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) { - System.out.println(getClass().getSimpleName() + ".handleInitMultisigRequest()"); + System.out.println(getClass().getSimpleName() + ".handleInitMultisigRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); trade.addInitProgressStep(); ThreadUtils.execute(() -> { synchronized (trade) { @@ -333,7 +333,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) { - System.out.println(getClass().getSimpleName() + ".handleSignContractRequest() " + trade.getId()); + System.out.println(getClass().getSimpleName() + ".handleSignContractRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); ThreadUtils.execute(() -> { synchronized (trade) { @@ -376,7 +376,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) { - System.out.println(getClass().getSimpleName() + ".handleSignContractResponse() " + trade.getId()); + System.out.println(getClass().getSimpleName() + ".handleSignContractResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); trade.addInitProgressStep(); ThreadUtils.execute(() -> { synchronized (trade) { @@ -422,7 +422,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } public void handleDepositResponse(DepositResponse response, NodeAddress sender) { - System.out.println(getClass().getSimpleName() + ".handleDepositResponse()"); + System.out.println(getClass().getSimpleName() + ".handleDepositResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); trade.addInitProgressStep(); ThreadUtils.execute(() -> { synchronized (trade) { @@ -452,7 +452,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } public void handle(DepositsConfirmedMessage message, NodeAddress sender) { - System.out.println(getClass().getSimpleName() + ".handle(DepositsConfirmedMessage) from " + sender); + System.out.println(getClass().getSimpleName() + ".handle(DepositsConfirmedMessage) from " + sender + " for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); if (!trade.isInitialized() || trade.isShutDown()) return; ThreadUtils.execute(() -> { synchronized (trade) { @@ -481,7 +481,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D // received by seller and arbitrator protected void handle(PaymentSentMessage message, NodeAddress peer) { - System.out.println(getClass().getSimpleName() + ".handle(PaymentSentMessage)"); + System.out.println(getClass().getSimpleName() + ".handle(PaymentSentMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); if (!trade.isInitialized() || trade.isShutDown()) return; if (!(trade instanceof SellerTrade || trade instanceof ArbitratorTrade)) { log.warn("Ignoring PaymentSentMessage since not seller or arbitrator"); @@ -535,7 +535,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D } private void handle(PaymentReceivedMessage message, NodeAddress peer, boolean reprocessOnError) { - System.out.println(getClass().getSimpleName() + ".handle(PaymentReceivedMessage)"); + System.out.println(getClass().getSimpleName() + ".handle(PaymentReceivedMessage) for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); if (!trade.isInitialized() || trade.isShutDown()) return; ThreadUtils.execute(() -> { if (!(trade instanceof BuyerTrade || trade instanceof ArbitratorTrade)) { diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java b/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java index 353c167a..bcc75132 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/MaybeSendSignContractRequest.java @@ -105,6 +105,7 @@ public class MaybeSendSignContractRequest extends TradeTask { } catch (Exception e) { log.warn("Error creating deposit tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + if (trade.getXmrConnectionService().isConnected()) trade.getXmrWalletService().requestSwitchToNextBestConnection(); HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java index 9673a565..a43c667d 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessDepositsConfirmedMessage.java @@ -61,16 +61,20 @@ public class ProcessDepositsConfirmedMessage extends TradeTask { } // update multisig hex - sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex()); + if (sender.getUpdatedMultisigHex() == null) { + sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex()); - // try to import multisig hex (retry later) - ThreadUtils.submitToPool(() -> { - try { - trade.importMultisigHex(); - } catch (Exception e) { - e.printStackTrace(); + // try to import multisig hex (retry later) + if (!trade.isPayoutPublished()) { + ThreadUtils.submitToPool(() -> { + try { + trade.importMultisigHex(); + } catch (Exception e) { + e.printStackTrace(); + } + }); } - }); + } // persist processModel.getTradeManager().requestPersistence(); diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java index 6e0719c1..0f0dd1cb 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/TakerReserveTradeFunds.java @@ -71,6 +71,7 @@ public class TakerReserveTradeFunds extends TradeTask { } catch (Exception e) { log.warn("Error creating reserve tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage()); if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + if (trade.getXmrConnectionService().isConnected()) trade.getXmrWalletService().requestSwitchToNextBestConnection(); HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } diff --git a/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java b/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java index 75db5880..37bb9d7b 100644 --- a/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java +++ b/core/src/main/java/haveno/core/trade/statistics/TradeStatistics3.java @@ -54,6 +54,8 @@ import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.Random; import static com.google.common.base.Preconditions.checkNotNull; @@ -88,13 +90,32 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl Offer offer = checkNotNull(trade.getOffer()); return new TradeStatistics3(offer.getCurrencyCode(), trade.getPrice().getValue(), - trade.getAmount().longValueExact(), + fuzzTradeAmountReproducibly(trade), offer.getPaymentMethod().getId(), - trade.getTakeOfferDate().getTime(), + fuzzTradeDateReproducibly(trade), truncatedArbitratorNodeAddress, extraDataMap); } + private static long fuzzTradeAmountReproducibly(Trade trade) { // randomize completed trade info #1099 + long originalTimestamp = trade.getTakeOfferDate().getTime(); + long exactAmount = trade.getAmount().longValueExact(); + Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp + long adjustedAmount = (long) random.nextDouble( + exactAmount * 0.95, exactAmount * 1.05); + log.debug("trade {} fuzzed trade amount for tradeStatistics is {}", trade.getShortId(), adjustedAmount); + return adjustedAmount; + } + + private static long fuzzTradeDateReproducibly(Trade trade) { // randomize completed trade info #1099 + long originalTimestamp = trade.getTakeOfferDate().getTime(); + Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp + long adjustedTimestamp = random.nextLong( + originalTimestamp-TimeUnit.HOURS.toMillis(24), originalTimestamp); + log.debug("trade {} fuzzed trade datestamp for tradeStatistics is {}", trade.getShortId(), new Date(adjustedTimestamp)); + return adjustedTimestamp; + } + // This enum must not change the order as we use the ordinal for storage to reduce data size. // The payment method string can be quite long and would consume 15% more space. // When we get a new payment method we can add it to the enum at the end. Old users would add it as string if not diff --git a/core/src/main/java/haveno/core/xmr/Balances.java b/core/src/main/java/haveno/core/xmr/Balances.java index 45525e76..cfae1cb5 100644 --- a/core/src/main/java/haveno/core/xmr/Balances.java +++ b/core/src/main/java/haveno/core/xmr/Balances.java @@ -35,6 +35,8 @@ 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; @@ -103,7 +105,7 @@ public class Balances { updateBalances(); } }); - updateBalances(); + doUpdateBalances(); } public XmrBalanceInfo getBalances() { @@ -117,42 +119,48 @@ public class Balances { } private void updateBalances() { + ThreadUtils.submitToPool(() -> doUpdateBalances()); + } + + private void doUpdateBalances() { synchronized (this) { - - // get wallet balances - BigInteger balance = xmrWalletService.getWallet() == null ? BigInteger.ZERO : xmrWalletService.getBalance(); - availableBalance = xmrWalletService.getWallet() == null ? BigInteger.ZERO : xmrWalletService.getAvailableBalance(); + synchronized (XmrWalletService.WALLET_LOCK) { - // calculate pending balance by adding frozen trade balances - reserved amounts - pendingBalance = balance.subtract(availableBalance); - List trades = tradeManager.getTradesStreamWithFundsLockedIn().collect(Collectors.toList()); - for (Trade trade : trades) { - if (trade.getFrozenAmount().equals(new BigInteger("0"))) continue; - BigInteger tradeFee = trade instanceof MakerTrade ? trade.getMakerFee() : trade.getTakerFee(); - pendingBalance = pendingBalance.add(trade.getFrozenAmount()).subtract(trade.getReservedAmount()).subtract(tradeFee).subtract(trade.getSelf().getDepositTxFee()); + // get wallet balances + BigInteger balance = xmrWalletService.getWallet() == null ? BigInteger.ZERO : xmrWalletService.getBalance(); + availableBalance = xmrWalletService.getWallet() == null ? BigInteger.ZERO : xmrWalletService.getAvailableBalance(); + + // calculate pending balance by adding frozen trade balances - reserved amounts + pendingBalance = balance.subtract(availableBalance); + List trades = tradeManager.getTradesStreamWithFundsLockedIn().collect(Collectors.toList()); + for (Trade trade : trades) { + if (trade.getFrozenAmount().equals(new BigInteger("0"))) continue; + BigInteger tradeFee = trade instanceof MakerTrade ? trade.getMakerFee() : trade.getTakerFee(); + pendingBalance = pendingBalance.add(trade.getFrozenAmount()).subtract(trade.getReservedAmount()).subtract(tradeFee).subtract(trade.getSelf().getDepositTxFee()); + } + + // calculate reserved offer balance + reservedOfferBalance = BigInteger.ZERO; + if (xmrWalletService.getWallet() != null) { + List frozenOutputs = xmrWalletService.getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false)); + for (MoneroOutputWallet frozenOutput : frozenOutputs) reservedOfferBalance = reservedOfferBalance.add(frozenOutput.getAmount()); + } + for (Trade trade : trades) { + reservedOfferBalance = reservedOfferBalance.subtract(trade.getFrozenAmount()); // subtract frozen trade balances + } + + // calculate reserved trade balance + reservedTradeBalance = BigInteger.ZERO; + for (Trade trade : trades) { + reservedTradeBalance = reservedTradeBalance.add(trade.getReservedAmount()); + } + + // calculate reserved balance + reservedBalance = reservedOfferBalance.add(reservedTradeBalance); + + // notify balance update + UserThread.execute(() -> updateCounter.set(updateCounter.get() + 1)); } - - // calculate reserved offer balance - reservedOfferBalance = BigInteger.ZERO; - if (xmrWalletService.getWallet() != null) { - List frozenOutputs = xmrWalletService.getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false)); - for (MoneroOutputWallet frozenOutput : frozenOutputs) reservedOfferBalance = reservedOfferBalance.add(frozenOutput.getAmount()); - } - for (Trade trade : trades) { - reservedOfferBalance = reservedOfferBalance.subtract(trade.getFrozenAmount()); // subtract frozen trade balances - } - - // calculate reserved trade balance - reservedTradeBalance = BigInteger.ZERO; - for (Trade trade : trades) { - reservedTradeBalance = reservedTradeBalance.add(trade.getReservedAmount()); - } - - // calculate reserved balance - reservedBalance = reservedOfferBalance.add(reservedTradeBalance); - - // notify balance update - UserThread.execute(() -> updateCounter.set(updateCounter.get() + 1)); } } } diff --git a/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java b/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java index 362211d1..643a5fae 100644 --- a/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java +++ b/core/src/main/java/haveno/core/xmr/nodes/XmrNodes.java @@ -86,11 +86,13 @@ public class XmrNodes { new XmrNode(MoneroNodesOption.PROVIDED, null, null, "127.0.0.1", 18081, 1, "@local"), new XmrNode(MoneroNodesOption.PROVIDED, null, null, "xmr-node.cakewallet.com", 18081, 2, "@cakewallet"), new XmrNode(MoneroNodesOption.PROVIDED, null, null, "node.community.rino.io", 18081, 2, "@RINOwallet"), - new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node.monerodevs.org", 18089, 2, "@monerodevs.org"), - new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node3.monerodevs.org", 18089, 2, "@monerodevs.org"), - new XmrNode(MoneroNodesOption.PUBLIC, null, null, "nodex.monerujo.io", 18081, 2, "@monerujo.io"), + new XmrNode(MoneroNodesOption.PROVIDED, null, null, "nodes.hashvault.pro", 18080, 2, "@HashVault"), + new XmrNode(MoneroNodesOption.PROVIDED, null, null, "p2pmd.xmrvsbeast.com", 18080, 2, "@xmrvsbeast"), + new XmrNode(MoneroNodesOption.PROVIDED, null, null, "node.monerodevs.org", 18089, 2, "@monerodevs.org"), + new XmrNode(MoneroNodesOption.PROVIDED, null, null, "nodex.monerujo.io", 18081, 2, "@monerujo.io"), new XmrNode(MoneroNodesOption.PUBLIC, null, null, "rucknium.me", 18081, 2, "@Rucknium"), - new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node.sethforprivacy.com", 18089, 2, "@sethforprivacy") + new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node.sethforprivacy.com", 18089, 2, "@sethforprivacy"), + new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node3.monerodevs.org", 18089, 2, "@monerodevs.org") ); default: throw new IllegalStateException("Unexpected base currency network: " + Config.baseCurrencyNetwork()); @@ -150,7 +152,7 @@ public class XmrNodes { if (parts[0].contains("[") && parts[0].contains(":")) { // IPv6 address and optional port number // address part delimited by square brackets e.g. [2a01:123:456:789::2]:8333 - host = parts[0].replace("[", "").replace("]", ""); + host = parts[0] + "]"; // keep the square brackets per RFC-2732 if (parts.length == 2) port = Integer.parseInt(parts[1].replace(":", "")); } else if (parts[0].contains(":") && !parts[0].contains(".")) { diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java b/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java index 8c411158..731c1311 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrKeyImagePoller.java @@ -32,6 +32,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import haveno.core.trade.HavenoUtils; + /** * Poll for changes to the spent status of key images. * @@ -47,6 +49,7 @@ public class XmrKeyImagePoller { private TaskLooper looper; private Map lastStatuses = new HashMap(); private boolean isPolling = false; + private Long lastLogPollErrorTimestamp; /** * Construct the listener. @@ -265,7 +268,12 @@ public class XmrKeyImagePoller { spentStatuses = daemon.getKeyImageSpentStatuses(keyImages); // TODO monero-java: if order of getKeyImageSpentStatuses is guaranteed, then it should take list parameter } } catch (Exception e) { - log.warn("Error polling spent status of key images: " + e.getMessage()); + + // limit error logging + if (lastLogPollErrorTimestamp == null || System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS) { + log.warn("Error polling spent status of key images: " + e.getMessage()); + lastLogPollErrorTimestamp = System.currentTimeMillis(); + } return; } diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 21b5d14d..2e609d77 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -24,6 +24,7 @@ import com.google.inject.name.Named; import common.utils.JsonUtils; import haveno.common.ThreadUtils; +import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.config.Config; import haveno.common.file.FileUtil; @@ -67,6 +68,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.Stream; import javafx.beans.property.LongProperty; @@ -132,8 +134,6 @@ public class XmrWalletService { private static final String THREAD_ID = XmrWalletService.class.getSimpleName(); private static final long SHUTDOWN_TIMEOUT_MS = 60000; private static final long NUM_BLOCKS_BEHIND_TOLERANCE = 5; - private static final long LOG_POLL_ERROR_AFTER_MS = 180000; // log poll error if unsuccessful after this time - private static Long lastPollSuccessTimestamp; private final User user; private final Preferences preferences; @@ -155,19 +155,22 @@ public class XmrWalletService { private TradeManager tradeManager; private MoneroWallet wallet; public static final Object WALLET_LOCK = new Object(); - private boolean wasWalletSynced = false; + private boolean wasWalletSynced; private final Map> txCache = new HashMap>(); - private boolean isClosingWallet = false; - private boolean isShutDownStarted = false; + private boolean isClosingWallet; + private boolean isShutDownStarted; private ExecutorService syncWalletThreadPool = Executors.newFixedThreadPool(10); // TODO: adjust based on connection type - private Long syncStartHeight = null; - private TaskLooper syncWithProgressLooper = null; - CountDownLatch syncWithProgressLatch; + private Long syncStartHeight; + private TaskLooper syncProgressLooper; + private CountDownLatch syncProgressLatch; + private Timer syncProgressTimeout; + private static final int SYNC_PROGRESS_TIMEOUT_SECONDS = 45; // wallet polling and cache private TaskLooper pollLooper; private boolean pollInProgress; private Long pollPeriodMs; + private Long lastLogPollErrorTimestamp; private final Object pollLock = new Object(); private Long cachedHeight; private BigInteger cachedBalance; @@ -689,11 +692,12 @@ public class XmrWalletService { try { return createTradeTxFromSubaddress(feeAmount, feeAddress, sendAmount, sendAddress, subaddressIndices.get(i)); } catch (Exception e) { - if (i == subaddressIndices.size() - 1 && reserveExactAmount) throw e; // throw if no subaddress with exact output + log.info("Cannot create trade tx from preferred subaddress index " + subaddressIndices.get(i) + ": " + e.getMessage()); } } // try any subaddress + if (!subaddressIndices.isEmpty()) log.info("Could not create trade tx from preferred subaddresses, trying any subaddress"); return createTradeTxFromSubaddress(feeAmount, feeAddress, sendAmount, sendAddress, null); } } @@ -933,7 +937,7 @@ public class XmrWalletService { e.printStackTrace(); // force close wallet - forceCloseWallet(wallet, getWalletPath(MONERO_WALLET_NAME)); + forceCloseMainWallet(); } log.info("Done shutting down {}", getClass().getSimpleName()); @@ -1281,22 +1285,9 @@ public class XmrWalletService { else log.info(appliedMsg); // listen for connection changes - xmrConnectionService.addConnectionListener(connection -> { - - // force restart main wallet if connection changed before synced - if (!wasWalletSynced) { - if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) return; - ThreadUtils.submitToPool(() -> { - log.warn("Force restarting main wallet because connection changed before inital sync"); - forceRestartMainWallet(); - }); - return; - } else { - - // apply connection changes - ThreadUtils.execute(() -> onConnectionChanged(connection), THREAD_ID); - } - }); + xmrConnectionService.addConnectionListener(connection -> ThreadUtils.execute(() -> { + onConnectionChanged(connection); + }, THREAD_ID)); // initialize main wallet when daemon synced walletInitListener = (obs, oldVal, newVal) -> initMainWalletIfConnected(); @@ -1305,28 +1296,20 @@ public class XmrWalletService { } private void initMainWalletIfConnected() { - ThreadUtils.execute(() -> { - synchronized (WALLET_LOCK) { - if (wallet == null && xmrConnectionService.downloadPercentageProperty().get() == 1 && !isShutDownStarted) { - maybeInitMainWallet(true); - if (walletInitListener != null) xmrConnectionService.downloadPercentageProperty().removeListener(walletInitListener); - } - } - }, THREAD_ID); - } - - private void maybeInitMainWallet(boolean sync) { - try { - maybeInitMainWallet(sync, MAX_SYNC_ATTEMPTS); - } catch (Exception e) { - log.warn("Error initializing main wallet: " + e.getMessage()); - e.printStackTrace(); - HavenoUtils.havenoSetup.getTopErrorMsg().set(e.getMessage()); - throw e; + if (wallet == null && xmrConnectionService.downloadPercentageProperty().get() == 1 && !isShutDownStarted) { + maybeInitMainWallet(true); } } + private void maybeInitMainWallet(boolean sync) { + maybeInitMainWallet(sync, MAX_SYNC_ATTEMPTS); + } + private void maybeInitMainWallet(boolean sync, int numAttempts) { + ThreadUtils.execute(() -> doMaybeInitMainWallet(sync, numAttempts), THREAD_ID); + } + + private void doMaybeInitMainWallet(boolean sync, int numAttempts) { synchronized (WALLET_LOCK) { if (isShutDownStarted) return; @@ -1355,12 +1338,21 @@ public class XmrWalletService { if (sync && numAttempts > 0) { try { + // switch connection if disconnected + if (!wallet.isConnectedToDaemon()) { + log.warn("Switching connection before syncing with progress because disconnected"); + if (requestSwitchToNextBestConnection()) return; // calls back to this method + } + // sync main wallet log.info("Syncing main wallet"); long time = System.currentTimeMillis(); syncWithProgress(); // blocking log.info("Done syncing main wallet in " + (System.currentTimeMillis() - time) + " ms"); + + // poll wallet doPollWallet(true); + if (walletInitListener != null) xmrConnectionService.downloadPercentageProperty().removeListener(walletInitListener); // log wallet balances if (getMoneroNetworkType() != MoneroNetworkType.MAINNET) { @@ -1369,8 +1361,8 @@ public class XmrWalletService { log.info("Monero wallet unlocked balance={}, pending balance={}, total balance={}", unlockedBalance, balance.subtract(unlockedBalance), balance); } - // reapply connection after wallet synced - onConnectionChanged(xmrConnectionService.getConnection()); + // reapply connection after wallet synced (might reinitialize wallet on new thread) + ThreadUtils.execute(() -> onConnectionChanged(xmrConnectionService.getConnection()), THREAD_ID); // reset internal state if main wallet was swapped resetIfWalletChanged(); @@ -1395,12 +1387,12 @@ public class XmrWalletService { // reschedule to init main wallet UserThread.runAfter(() -> { - ThreadUtils.execute(() -> maybeInitMainWallet(true, MAX_SYNC_ATTEMPTS), THREAD_ID); + maybeInitMainWallet(true, MAX_SYNC_ATTEMPTS); }, xmrConnectionService.getRefreshPeriodMs() / 1000); } else { log.warn("Trying again in {} seconds", xmrConnectionService.getRefreshPeriodMs() / 1000); UserThread.runAfter(() -> { - ThreadUtils.execute(() -> maybeInitMainWallet(true, numAttempts - 1), THREAD_ID); + maybeInitMainWallet(true, numAttempts - 1); }, xmrConnectionService.getRefreshPeriodMs() / 1000); } } @@ -1431,6 +1423,9 @@ public class XmrWalletService { private void syncWithProgress() { + // start sync progress timeout + resetSyncProgressTimeout(); + // show sync progress updateSyncProgress(wallet.getHeight()); @@ -1458,41 +1453,34 @@ public class XmrWalletService { // poll wallet for progress wallet.startSyncing(xmrConnectionService.getRefreshPeriodMs()); - syncWithProgressLatch = new CountDownLatch(1); - syncWithProgressLooper = new TaskLooper(() -> { + syncProgressLatch = new CountDownLatch(1); + syncProgressLooper = new TaskLooper(() -> { if (wallet == null) return; long height = 0; try { height = wallet.getHeight(); // can get read timeout while syncing } catch (Exception e) { - e.printStackTrace(); + if (!isShutDownStarted) e.printStackTrace(); return; } if (height < xmrConnectionService.getTargetHeight()) updateSyncProgress(height); else { - syncWithProgressLooper.stop(); + syncProgressLooper.stop(); wasWalletSynced = true; updateSyncProgress(height); - syncWithProgressLatch.countDown(); + syncProgressLatch.countDown(); } }); - syncWithProgressLooper.start(1000); - HavenoUtils.awaitLatch(syncWithProgressLatch); + syncProgressLooper.start(1000); + HavenoUtils.awaitLatch(syncProgressLatch); wallet.stopSyncing(); if (!wasWalletSynced) throw new IllegalStateException("Failed to sync wallet with progress"); } - private void stopSyncWithProgress() { - if (syncWithProgressLooper != null) { - syncWithProgressLooper.stop(); - syncWithProgressLooper = null; - syncWithProgressLatch.countDown(); - } - } - private void updateSyncProgress(long height) { UserThread.execute(() -> { walletHeight.set(height); + resetSyncProgressTimeout(); // new wallet reports height 1 before synced if (height == 1) { @@ -1509,6 +1497,18 @@ public class XmrWalletService { }); } + private synchronized void resetSyncProgressTimeout() { + if (syncProgressTimeout != null) syncProgressTimeout.stop(); + syncProgressTimeout = UserThread.runAfter(() -> { + if (isShutDownStarted || wasWalletSynced) return; + log.warn("Sync progress timeout called"); + forceCloseMainWallet(); + requestSwitchToNextBestConnection(); + maybeInitMainWallet(true); + resetSyncProgressTimeout(); + }, SYNC_PROGRESS_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } + private MoneroWalletFull createWalletFull(MoneroWalletConfig config) { // must be connected to daemon @@ -1545,7 +1545,7 @@ public class XmrWalletService { // open wallet config.setNetworkType(getMoneroNetworkType()); config.setServer(connection); - log.info("Opening full wallet " + config.getPath() + " with monerod=" + connection.getUri()); + log.info("Opening full wallet " + config.getPath() + " with monerod=" + connection.getUri() + ", proxyUri=" + connection.getProxyUri()); walletFull = MoneroWalletFull.openWallet(config); if (walletFull.getDaemonConnection() != null) walletFull.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE); log.info("Done opening full wallet " + config.getPath()); @@ -1605,7 +1605,7 @@ public class XmrWalletService { if (!applyProxyUri) connection.setProxyUri(null); // open wallet - log.info("Opening RPC wallet " + config.getPath() + " with monerod=" + connection.getUri()); + log.info("Opening RPC wallet " + config.getPath() + " with monerod=" + connection.getUri() + ", proxyUri=" + connection.getProxyUri()); config.setServer(connection); walletRpc.openWallet(config); if (walletRpc.getDaemonConnection() != null) walletRpc.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE); @@ -1662,31 +1662,53 @@ public class XmrWalletService { private void onConnectionChanged(MoneroRpcConnection connection) { synchronized (WALLET_LOCK) { + + // use current connection + connection = xmrConnectionService.getConnection(); + + // check if ignored if (wallet == null || isShutDownStarted) return; if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) return; String oldProxyUri = wallet == null || wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getProxyUri(); String newProxyUri = connection == null ? null : connection.getProxyUri(); - log.info("Setting daemon connection for main wallet: uri={}, proxyUri={}", connection == null ? null : connection.getUri(), newProxyUri); + log.info("Setting daemon connection for main wallet, monerod={}, proxyUri={}", connection == null ? null : connection.getUri(), newProxyUri); + + // force restart main wallet if connection changed before synced + if (!wasWalletSynced) { + if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) return; + log.warn("Force restarting main wallet because connection changed before inital sync"); + forceRestartMainWallet(); + return; + } + + // update connection if (wallet instanceof MoneroWalletRpc) { if (StringUtils.equals(oldProxyUri, newProxyUri)) { wallet.setDaemonConnection(connection); } else { - log.info("Restarting main wallet because proxy URI has changed, old={}, new={}", oldProxyUri, newProxyUri); + log.info("Restarting main wallet because proxy URI has changed, old={}, new={}", oldProxyUri, newProxyUri); // TODO: set proxy without restarting wallet closeMainWallet(true); - maybeInitMainWallet(false); + doMaybeInitMainWallet(false, MAX_SYNC_ATTEMPTS); + return; // wallet is re-initialized } } else { wallet.setDaemonConnection(connection); wallet.setProxyUri(connection.getProxyUri()); } - // sync wallet on new thread + // switch if wallet disconnected + if (Boolean.TRUE.equals(connection.isConnected() && !wallet.isConnectedToDaemon())) { + log.warn("Switching to next best connection because main wallet is disconnected"); + if (requestSwitchToNextBestConnection()) return; // calls back to this method + } + + // update poll period if (connection != null && !isShutDownStarted) { wallet.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE); updatePollPeriod(); } - log.info("Done setting main wallet monerod=" + (wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getUri())); + log.info("Done setting daemon connection for main wallet, monerod=" + (wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getUri())); } } @@ -1735,25 +1757,21 @@ public class XmrWalletService { } private void forceCloseMainWallet() { + stopPolling(); isClosingWallet = true; forceCloseWallet(wallet, getWalletPath(MONERO_WALLET_NAME)); - stopPolling(); - stopSyncWithProgress(); wallet = null; } private void forceRestartMainWallet() { log.warn("Force restarting main wallet"); forceCloseMainWallet(); - synchronized (WALLET_LOCK) { - maybeInitMainWallet(true); - } + maybeInitMainWallet(true); } private void startPolling() { synchronized (WALLET_LOCK) { - if (isShutDownStarted || isPollInProgress()) return; - log.info("Starting to poll main wallet"); + if (isShutDownStarted || isPolling()) return; updatePollPeriod(); pollLooper = new TaskLooper(() -> pollWallet()); pollLooper.start(pollPeriodMs); @@ -1761,13 +1779,13 @@ public class XmrWalletService { } private void stopPolling() { - if (isPollInProgress()) { + if (isPolling()) { pollLooper.stop(); pollLooper = null; } } - private boolean isPollInProgress() { + private boolean isPolling() { return pollLooper != null; } @@ -1785,7 +1803,7 @@ public class XmrWalletService { if (this.isShutDownStarted) return; if (this.pollPeriodMs != null && this.pollPeriodMs == pollPeriodMs) return; this.pollPeriodMs = pollPeriodMs; - if (isPollInProgress()) { + if (isPolling()) { stopPolling(); startPolling(); } @@ -1793,64 +1811,83 @@ public class XmrWalletService { } private void pollWallet() { - if (pollInProgress) return; + synchronized (pollLock) { + if (pollInProgress) return; + } doPollWallet(true); } private void doPollWallet(boolean updateTxs) { synchronized (pollLock) { - if (isShutDownStarted) return; pollInProgress = true; - try { + } + if (isShutDownStarted) return; + try { - // switch to best connection if daemon is too far behind - MoneroDaemonInfo lastInfo = xmrConnectionService.getLastInfo(); - if (lastInfo == null) { - log.warn("Last daemon info is null"); - return; - } - if (wasWalletSynced && walletHeight.get() < xmrConnectionService.getTargetHeight() - NUM_BLOCKS_BEHIND_TOLERANCE && !Config.baseCurrencyNetwork().isTestnet()) { - log.warn("Updating connection because main wallet is {} blocks behind monerod, wallet height={}, monerod height={}", xmrConnectionService.getTargetHeight() - walletHeight.get(), walletHeight.get(), lastInfo.getHeight()); - xmrConnectionService.switchToBestConnection(); - } + // skip if daemon not synced + MoneroDaemonInfo lastInfo = xmrConnectionService.getLastInfo(); + if (lastInfo == null) { + log.warn("Last daemon info is null"); + return; + } + if (!xmrConnectionService.isSyncedWithinTolerance()) { + log.warn("Monero daemon is not synced within tolerance, height={}, targetHeight={}", xmrConnectionService.chainHeightProperty().get(), xmrConnectionService.getTargetHeight()); + return; + } - // sync wallet if behind daemon - if (walletHeight.get() < xmrConnectionService.getTargetHeight()) { - synchronized (WALLET_LOCK) { // avoid long sync from blocking other operations - syncMainWallet(); - } - } + // switch to best connection if wallet is too far behind + if (wasWalletSynced && walletHeight.get() < xmrConnectionService.getTargetHeight() - NUM_BLOCKS_BEHIND_TOLERANCE && !Config.baseCurrencyNetwork().isTestnet()) { + log.warn("Updating connection because main wallet is {} blocks behind monerod, wallet height={}, monerod height={}", xmrConnectionService.getTargetHeight() - walletHeight.get(), walletHeight.get(), lastInfo.getHeight()); + if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection(); + } - // fetch transactions from pool and store to cache - // TODO: ideally wallet should sync every poll and then avoid updating from pool on fetching txs? - if (updateTxs) { - synchronized (WALLET_LOCK) { // avoid long fetch from blocking other operations - synchronized (HavenoUtils.getDaemonLock()) { - try { - cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true)); - lastPollSuccessTimestamp = System.currentTimeMillis(); - } catch (Exception e) { // fetch from pool can fail - if (!isShutDownStarted) { - if (lastPollSuccessTimestamp == null || System.currentTimeMillis() - lastPollSuccessTimestamp > LOG_POLL_ERROR_AFTER_MS) { // only log if not recently successful - log.warn("Error polling main wallet's transactions from the pool: {}", e.getMessage()); - } + // sync wallet if behind daemon + if (walletHeight.get() < xmrConnectionService.getTargetHeight()) { + synchronized (WALLET_LOCK) { // avoid long sync from blocking other operations + syncMainWallet(); + } + } + + // fetch transactions from pool and store to cache + // TODO: ideally wallet should sync every poll and then avoid updating from pool on fetching txs? + if (updateTxs) { + synchronized (WALLET_LOCK) { // avoid long fetch from blocking other operations + synchronized (HavenoUtils.getDaemonLock()) { + try { + cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true)); + } catch (Exception e) { // fetch from pool can fail + if (!isShutDownStarted) { + if (lastLogPollErrorTimestamp == null || System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS) { // limit error logging + log.warn("Error polling main wallet's transactions from the pool: {}", e.getMessage()); + lastLogPollErrorTimestamp = System.currentTimeMillis(); } } } } } + } + } catch (Exception e) { + if (wallet == null || isShutDownStarted) return; + boolean isConnectionRefused = e.getMessage() != null && e.getMessage().contains("Connection refused"); + if (isConnectionRefused) forceRestartMainWallet(); + else if (isWalletConnectedToDaemon()) { + log.warn("Error polling main wallet, errorMessage={}. Monerod={}", e.getMessage(), getConnectionService().getConnection()); + //e.printStackTrace(); + } + } finally { - // cache wallet info - cacheWalletInfo(); - } catch (Exception e) { - if (wallet == null || isShutDownStarted) return; - boolean isConnectionRefused = e.getMessage() != null && e.getMessage().contains("Connection refused"); - if (isConnectionRefused) forceRestartMainWallet(); - else if (isWalletConnectedToDaemon()) { - log.warn("Error polling main wallet, errorMessage={}. Monerod={}", e.getMessage(), getConnectionService().getConnection()); - //e.printStackTrace(); + // cache wallet info last + synchronized (WALLET_LOCK) { + if (wallet != null && !isShutDownStarted) { + try { + cacheWalletInfo(); + } catch (Exception e) { + e.printStackTrace(); + } } - } finally { + } + + synchronized (pollLock) { pollInProgress = false; } } @@ -1875,6 +1912,10 @@ public class XmrWalletService { } } + public boolean requestSwitchToNextBestConnection() { + return xmrConnectionService.requestSwitchToNextBestConnection(); + } + private void onNewBlock(long height) { UserThread.execute(() -> { walletHeight.set(height); diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index e0c28fb8..a38f13bf 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -311,6 +311,9 @@ market.tabs.spreadCurrency=Offers by Currency market.tabs.spreadPayment=Offers by Payment Method market.tabs.trades=Trades +# OfferBookView +market.offerBook.filterPrompt=Filter + # OfferBookChartView market.offerBook.sellOffersHeaderLabel=Sell {0} to market.offerBook.buyOffersHeaderLabel=Buy {0} from @@ -1040,11 +1043,13 @@ funds.withdrawal.inputs=Inputs selection funds.withdrawal.useAllInputs=Use all available inputs funds.withdrawal.useCustomInputs=Use custom inputs funds.withdrawal.receiverAmount=Receiver's amount +funds.withdrawal.sendMax=Send max available funds.withdrawal.senderAmount=Sender's amount funds.withdrawal.feeExcluded=Amount excludes mining fee funds.withdrawal.feeIncluded=Amount includes mining fee funds.withdrawal.fromLabel=Withdraw from address funds.withdrawal.toLabel=Withdraw to address +funds.withdrawal.maximum=MAX funds.withdrawal.memoLabel=Withdrawal memo funds.withdrawal.memo=Optionally fill memo funds.withdrawal.withdrawButton=Withdraw selected @@ -2140,7 +2145,7 @@ popup.warning.seed=seed popup.warning.mandatoryUpdate.trading=Please update to the latest Haveno version. \ A mandatory update was released which disables trading for old versions. \ Please check out the Haveno Forum for more information. -popup.warning.noFilter=We did not receive a filter object from the seed nodes. Please inform the Haveno network administrators to register a filter object with ctrl + f. +popup.warning.noFilter=We did not receive a filter object from the seed nodes. Please inform the network administrators to register a filter object. popup.warning.burnXMR=This transaction is not possible, as the mining fees of {0} would exceed the amount to transfer of {1}. \ Please wait until the mining fees are low again or until you''ve accumulated more XMR to transfer. diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index 990ef794..bd4ee417 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -848,6 +848,7 @@ funds.withdrawal.inputs=Selección de entradas funds.withdrawal.useAllInputs=Usar todos los entradas disponibles funds.withdrawal.useCustomInputs=Usar entradas personalizados funds.withdrawal.receiverAmount=Cantidad del receptor +funds.withdrawal.sendMax=Enviar máximo disponible funds.withdrawal.senderAmount=Cantidad del emisor funds.withdrawal.feeExcluded=La cantidad no incluye comisión de minado funds.withdrawal.feeIncluded=La cantidad incluye comisión de minado diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index 462bcef5..8d3b4ce4 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -849,6 +849,7 @@ funds.withdrawal.inputs=Sélection de la valeur à saisir funds.withdrawal.useAllInputs=Utiliser toutes les valeurs disponibles funds.withdrawal.useCustomInputs=Utiliser une valeur de saisie personnalisée funds.withdrawal.receiverAmount=Montant du destinataire +funds.withdrawal.sendMax=Envoyer max disponible funds.withdrawal.senderAmount=Montant de l'expéditeur funds.withdrawal.feeExcluded=Montant excluant les frais de minage funds.withdrawal.feeIncluded=Montant incluant frais de minage diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index 97b7fb6b..044873db 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -847,6 +847,7 @@ funds.withdrawal.inputs=Selezione input funds.withdrawal.useAllInputs=Utilizza tutti gli input disponibili funds.withdrawal.useCustomInputs=Utilizza input personalizzati funds.withdrawal.receiverAmount=Importo del destinatario +funds.withdrawal.sendMax=Inviare massimo disponibile funds.withdrawal.senderAmount=Importo del mittente funds.withdrawal.feeExcluded=L'importo esclude la commissione di mining funds.withdrawal.feeIncluded=L'importo include la commissione di mining diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcPaymentAccountsService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcPaymentAccountsService.java index e29b448f..ad1acce1 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcPaymentAccountsService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcPaymentAccountsService.java @@ -34,6 +34,8 @@ import haveno.proto.grpc.CreateCryptoCurrencyPaymentAccountReply; import haveno.proto.grpc.CreateCryptoCurrencyPaymentAccountRequest; import haveno.proto.grpc.CreatePaymentAccountReply; import haveno.proto.grpc.CreatePaymentAccountRequest; +import haveno.proto.grpc.DeletePaymentAccountReply; +import haveno.proto.grpc.DeletePaymentAccountRequest; import haveno.proto.grpc.GetCryptoCurrencyPaymentMethodsReply; import haveno.proto.grpc.GetCryptoCurrencyPaymentMethodsRequest; import haveno.proto.grpc.GetPaymentAccountFormReply; @@ -160,6 +162,19 @@ class GrpcPaymentAccountsService extends PaymentAccountsImplBase { } } + @Override + public void deletePaymentAccount(DeletePaymentAccountRequest req, + StreamObserver responseObserver) { + try { + coreApi.deletePaymentAccount(req.getPaymentAccountId()); + var reply = DeletePaymentAccountReply.newBuilder().build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + @Override public void getCryptoCurrencyPaymentMethods(GetCryptoCurrencyPaymentMethodsRequest req, StreamObserver responseObserver) { diff --git a/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java b/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java index 70c2c355..123078b2 100644 --- a/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java +++ b/daemon/src/main/java/haveno/daemon/grpc/GrpcTradesService.java @@ -96,9 +96,8 @@ class GrpcTradesService extends TradesImplBase { StreamObserver responseObserver) { try { Trade trade = coreApi.getTrade(req.getTradeId()); - String role = coreApi.getTradeRole(req.getTradeId()); var reply = GetTradeReply.newBuilder() - .setTrade(toTradeInfo(trade, role).toProtoMessage()) + .setTrade(toTradeInfo(trade).toProtoMessage()) .build(); responseObserver.onNext(reply); responseObserver.onCompleted(); @@ -258,8 +257,8 @@ class GrpcTradesService extends TradesImplBase { put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); put(getCompleteTradeMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); put(getWithdrawFundsMethod().getFullMethodName(), new GrpcCallRateMeter(3, MINUTES)); - put(getGetChatMessagesMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 1, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); - put(getSendChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 1, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getGetChatMessagesMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); + put(getSendChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); }} ))); } diff --git a/desktop/package/macosx/Info.plist b/desktop/package/macosx/Info.plist index cf57e20e..1abf7683 100644 --- a/desktop/package/macosx/Info.plist +++ b/desktop/package/macosx/Info.plist @@ -5,10 +5,10 @@ CFBundleVersion - 1.0.8 + 1.0.9 CFBundleShortVersionString - 1.0.8 + 1.0.9 CFBundleExecutable Haveno diff --git a/desktop/src/main/java/haveno/desktop/components/AutoTooltipTextField.java b/desktop/src/main/java/haveno/desktop/components/AutoTooltipTextField.java new file mode 100644 index 00000000..1473ed09 --- /dev/null +++ b/desktop/src/main/java/haveno/desktop/components/AutoTooltipTextField.java @@ -0,0 +1,45 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package haveno.desktop.components; + +import com.jfoenix.controls.JFXTextField; +import com.jfoenix.skins.JFXTextFieldSkin; +import javafx.scene.control.Skin; +import javafx.scene.control.TextField; + +public class AutoTooltipTextField extends JFXTextField { + + public AutoTooltipTextField() { + super(); + } + + public AutoTooltipTextField(String text) { + super(text); + } + + @Override + protected Skin createDefaultSkin() { + return new AutoTooltipTextFieldSkin(this); + } + + private class AutoTooltipTextFieldSkin extends JFXTextFieldSkin { + public AutoTooltipTextFieldSkin(TextField textField) { + super(textField); + } + } +} diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/AustraliaPayidForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/AustraliaPayidForm.java index c830b889..234b5e88 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/AustraliaPayidForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/AustraliaPayidForm.java @@ -48,11 +48,11 @@ public class AustraliaPayidForm extends PaymentMethodForm { addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), ((AustraliaPayidAccountPayload) paymentAccountPayload).getBankAccountName()); - addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.payid"), + addCompactTopLabelTextField(gridPane, gridRow, 1, Res.get("payment.payid"), ((AustraliaPayidAccountPayload) paymentAccountPayload).getPayid()); AustraliaPayidAccountPayload payId = (AustraliaPayidAccountPayload) paymentAccountPayload; - TextArea textExtraInfo = addCompactTopLabelTextArea(gridPane, gridRow, 1, Res.get("payment.shared.extraInfo"), "").second; + TextArea textExtraInfo = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.shared.extraInfo"), "").second; textExtraInfo.setMinHeight(70); textExtraInfo.setEditable(false); textExtraInfo.setText(payId.getExtraInfo()); diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAppForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAppForm.java index ac54d7a2..60fe7127 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAppForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/CashAppForm.java @@ -17,6 +17,7 @@ package haveno.desktop.components.paymentmethods; +import com.jfoenix.controls.JFXTextArea; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.payment.CashAppAccount; @@ -29,6 +30,7 @@ import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.Layout; +import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; @@ -36,6 +38,8 @@ import javafx.scene.layout.GridPane; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addTopLabelFlowPane; +import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextArea; +import static haveno.desktop.util.FormBuilder.addTopLabelTextArea; public class CashAppForm extends PaymentMethodForm { private final CashAppAccount cashAppAccount; @@ -43,6 +47,13 @@ public class CashAppForm extends PaymentMethodForm { public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.email.mobile.cashtag"), ((CashAppAccountPayload) paymentAccountPayload).getEmailOrMobileNrOrCashtag()); + + CashAppAccountPayload payId = (CashAppAccountPayload) paymentAccountPayload; + TextArea textExtraInfo = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.shared.extraInfo"), "").second; + textExtraInfo.setMinHeight(70); + textExtraInfo.setEditable(false); + textExtraInfo.setText(payId.getExtraInfo()); + return gridRow; } @@ -66,6 +77,16 @@ public class CashAppForm extends PaymentMethodForm { cashAppAccount.setEmailOrMobileNrOrCashtag(newValue.trim()); updateFromInputs(); }); + + TextArea extraTextArea = addTopLabelTextArea(gridPane, ++gridRow, + Res.get("payment.shared.optionalExtra"), Res.get("payment.shared.extraInfo.prompt")).second; + extraTextArea.setMinHeight(70); + ((JFXTextArea) extraTextArea).setLabelFloat(false); + extraTextArea.textProperty().addListener((ov, oldValue, newValue) -> { + cashAppAccount.setExtraInfo(newValue); + updateFromInputs(); + }); + addCurrenciesGrid(true); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); @@ -96,6 +117,12 @@ public class CashAppForm extends PaymentMethodForm { addAccountNameTextFieldWithAutoFillToggleButton(); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email.mobile.cashtag"), cashAppAccount.getEmailOrMobileNrOrCashtag()).second; + + TextArea textAreaExtra = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.shared.extraInfo"), "").second; + textAreaExtra.setText(cashAppAccount.getExtraInfo()); + textAreaExtra.setMinHeight(70); + textAreaExtra.setEditable(false); + field.setMouseTransparent(false); addLimitations(true); addCurrenciesGrid(false); diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/PayPalForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/PayPalForm.java index b3c9833d..8e0d48ee 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/PayPalForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/PayPalForm.java @@ -17,6 +17,7 @@ package haveno.desktop.components.paymentmethods; +import com.jfoenix.controls.JFXTextArea; import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.locale.Res; import haveno.core.payment.PayPalAccount; @@ -29,13 +30,16 @@ import haveno.core.util.validation.InputValidator; import haveno.desktop.components.InputTextField; import haveno.desktop.util.FormBuilder; import haveno.desktop.util.Layout; +import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; -import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; +import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextArea; +import static haveno.desktop.util.FormBuilder.addTopLabelTextArea; import static haveno.desktop.util.FormBuilder.addTopLabelFlowPane; +import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField; public class PayPalForm extends PaymentMethodForm { private final PayPalAccount paypalAccount; @@ -43,6 +47,13 @@ public class PayPalForm extends PaymentMethodForm { public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.email.mobile.username"), ((PayPalAccountPayload) paymentAccountPayload).getEmailOrMobileNrOrUsername()); + + PayPalAccountPayload payId = (PayPalAccountPayload) paymentAccountPayload; + TextArea textExtraInfo = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.shared.extraInfo"), "").second; + textExtraInfo.setMinHeight(70); + textExtraInfo.setEditable(false); + textExtraInfo.setText(payId.getExtraInfo()); + return gridRow; } @@ -66,6 +77,16 @@ public class PayPalForm extends PaymentMethodForm { paypalAccount.setEmailOrMobileNrOrUsername(newValue.trim()); updateFromInputs(); }); + + TextArea extraTextArea = addTopLabelTextArea(gridPane, ++gridRow, + Res.get("payment.shared.optionalExtra"), Res.get("payment.shared.extraInfo.prompt")).second; + extraTextArea.setMinHeight(70); + ((JFXTextArea) extraTextArea).setLabelFloat(false); + extraTextArea.textProperty().addListener((ov, oldValue, newValue) -> { + paypalAccount.setExtraInfo(newValue); + updateFromInputs(); + }); + addCurrenciesGrid(true); addLimitations(false); addAccountNameTextFieldWithAutoFillToggleButton(); @@ -99,6 +120,12 @@ public class PayPalForm extends PaymentMethodForm { TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email.mobile.username"), paypalAccount.getEmailOrMobileNrOrUsername()).second; + + TextArea textAreaExtra = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.shared.extraInfo"), "").second; + textAreaExtra.setText(paypalAccount.getExtraInfo()); + textAreaExtra.setMinHeight(70); + textAreaExtra.setEditable(false); + field.setMouseTransparent(false); addLimitations(true); addCurrenciesGrid(false); diff --git a/desktop/src/main/java/haveno/desktop/components/paymentmethods/UpholdForm.java b/desktop/src/main/java/haveno/desktop/components/paymentmethods/UpholdForm.java index 4ef493cf..5443139d 100644 --- a/desktop/src/main/java/haveno/desktop/components/paymentmethods/UpholdForm.java +++ b/desktop/src/main/java/haveno/desktop/components/paymentmethods/UpholdForm.java @@ -112,7 +112,7 @@ public class UpholdForm extends PaymentMethodForm { addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(upholdAccount.getPaymentMethod().getId())); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), - Res.get(upholdAccount.getAccountOwner())); + upholdAccount.getAccountOwner()); TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.uphold.accountId"), upholdAccount.getAccountId()).second; field.setMouseTransparent(false); diff --git a/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsDataModel.java b/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsDataModel.java index d34857a1..b72b89fe 100644 --- a/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/account/content/traditionalaccounts/TraditionalAccountsDataModel.java @@ -108,6 +108,7 @@ class TraditionalAccountsDataModel extends ActivatableDataModel { TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency(); List tradeCurrencies = paymentAccount.getTradeCurrencies(); if (singleTradeCurrency != null) { + paymentAccount.setSelectedTradeCurrency(singleTradeCurrency); if (singleTradeCurrency instanceof TraditionalCurrency) preferences.addTraditionalCurrency((TraditionalCurrency) singleTradeCurrency); else diff --git a/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java b/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java index abdcde0b..cab5d715 100644 --- a/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java +++ b/desktop/src/main/java/haveno/desktop/main/funds/withdrawal/WithdrawalView.java @@ -35,7 +35,7 @@ package haveno.desktop.main.funds.withdrawal; import com.google.inject.Inject; -import haveno.common.util.Tuple4; +import haveno.common.util.Tuple3; import haveno.core.locale.Res; import haveno.core.trade.HavenoUtils; import haveno.core.trade.TradeManager; @@ -48,6 +48,7 @@ import haveno.core.xmr.wallet.XmrWalletService; import haveno.desktop.common.view.ActivatableView; import haveno.desktop.common.view.FxmlView; import haveno.desktop.components.BusyAnimation; +import haveno.desktop.components.HyperlinkWithIcon; import haveno.desktop.components.TitledGroupBg; import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.windows.TxDetails; @@ -60,13 +61,11 @@ import javafx.beans.value.ChangeListener; import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.control.TextField; -import javafx.scene.control.RadioButton; -import javafx.scene.control.ToggleGroup; -import javafx.scene.control.Toggle; import javafx.scene.control.Button; import javafx.scene.layout.GridPane; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; +import monero.common.MoneroUtils; import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroTxWallet; @@ -90,7 +89,6 @@ public class WithdrawalView extends ActivatableView { private Label amountLabel; private TextField amountTextField, withdrawToTextField, withdrawMemoTextField; - private RadioButton feeExcludedRadioButton, feeIncludedRadioButton; private final XmrWalletService xmrWalletService; private final TradeManager tradeManager; @@ -100,11 +98,8 @@ public class WithdrawalView extends ActivatableView { private BigInteger amount = BigInteger.ZERO; private ChangeListener amountListener; private ChangeListener amountFocusListener; - private ChangeListener feeToggleGroupListener; - private ToggleGroup feeToggleGroup; - private boolean feeExcluded; private int rowIndex = 0; - private final static int MAX_ATTEMPTS = 3; + boolean sendMax = false; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle @@ -141,20 +136,15 @@ public class WithdrawalView extends ActivatableView { withdrawToTextField = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("funds.withdrawal.toLabel", Res.getBaseCurrencyCode())).second; - feeToggleGroup = new ToggleGroup(); - - final Tuple4 feeTuple3 = FormBuilder.addTopLabelTextFieldRadioButtonRadioButton(gridPane, ++rowIndex, feeToggleGroup, + final Tuple3 feeTuple3 = FormBuilder.addTopLabelTextFieldHyperLink(gridPane, ++rowIndex, "", Res.get("funds.withdrawal.receiverAmount", Res.getBaseCurrencyCode()), - "", - Res.get("funds.withdrawal.feeExcluded"), - Res.get("funds.withdrawal.feeIncluded"), + Res.get("funds.withdrawal.sendMax"), 0); amountLabel = feeTuple3.first; amountTextField = feeTuple3.second; amountTextField.setMinWidth(180); - feeExcludedRadioButton = feeTuple3.third; - feeIncludedRadioButton = feeTuple3.fourth; + HyperlinkWithIcon sendMaxLink = feeTuple3.third; withdrawMemoTextField = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("funds.withdrawal.memoLabel", Res.getBaseCurrencyCode())).second; @@ -175,6 +165,12 @@ public class WithdrawalView extends ActivatableView { }).start(); }); + sendMaxLink.setOnAction(event -> { + sendMax = true; + amount = null; // set amount when tx created + amountTextField.setText(Res.get("funds.withdrawal.maximum")); + }); + balanceListener = new XmrBalanceListener() { @Override public void onBalanceChanged(BigInteger balance) { @@ -183,6 +179,7 @@ public class WithdrawalView extends ActivatableView { }; amountListener = (observable, oldValue, newValue) -> { if (amountTextField.focusedProperty().get()) { + sendMax = false; // disable max if amount changed while focused try { amount = HavenoUtils.parseXmr(amountTextField.getText()); } catch (Throwable t) { @@ -191,7 +188,9 @@ public class WithdrawalView extends ActivatableView { } }; amountFocusListener = (observable, oldValue, newValue) -> { - if (oldValue && !newValue) { + + // parse amount on focus out unless sending max + if (oldValue && !newValue && !sendMax) { if (amount.compareTo(BigInteger.ZERO) > 0) amountTextField.setText(HavenoUtils.formatXmr(amount)); else @@ -199,14 +198,6 @@ public class WithdrawalView extends ActivatableView { } }; amountLabel.setText(Res.get("funds.withdrawal.receiverAmount")); - feeExcludedRadioButton.setToggleGroup(feeToggleGroup); - feeIncludedRadioButton.setToggleGroup(feeToggleGroup); - feeToggleGroupListener = (observable, oldValue, newValue) -> { - feeExcluded = newValue == feeExcludedRadioButton; - amountLabel.setText(feeExcluded ? - Res.get("funds.withdrawal.receiverAmount") : - Res.get("funds.withdrawal.senderAmount")); - }; } @@ -229,9 +220,6 @@ public class WithdrawalView extends ActivatableView { amountTextField.textProperty().addListener(amountListener); amountTextField.focusedProperty().addListener(amountFocusListener); xmrWalletService.addBalanceListener(balanceListener); - feeToggleGroup.selectedToggleProperty().addListener(feeToggleGroupListener); - - if (feeToggleGroup.getSelectedToggle() == null) feeToggleGroup.selectToggle(feeExcludedRadioButton); GUIUtil.requestFocus(withdrawToTextField); } @@ -242,7 +230,6 @@ public class WithdrawalView extends ActivatableView { xmrWalletService.removeBalanceListener(balanceListener); amountTextField.textProperty().removeListener(amountListener); amountTextField.focusedProperty().removeListener(amountFocusListener); - feeToggleGroup.selectedToggleProperty().removeListener(feeToggleGroupListener); } @@ -254,15 +241,21 @@ public class WithdrawalView extends ActivatableView { if (GUIUtil.isReadyForTxBroadcastOrShowPopup(xmrWalletService)) { try { - // get withdraw address + // validate address final String withdrawToAddress = withdrawToTextField.getText(); + if (!MoneroUtils.isValidAddress(withdrawToAddress, XmrWalletService.getMoneroNetworkType())) { + throw new IllegalArgumentException(Res.get("validation.xmr.invalidAddress")); + } + + // set max amount if requested + if (sendMax) amount = xmrWalletService.getAvailableBalance(); // check sufficient available balance if (amount.compareTo(BigInteger.ZERO) <= 0) throw new RuntimeException(Res.get("portfolio.pending.step5_buyer.amountTooLow")); // create tx MoneroTxWallet tx = null; - for (int i = 0; i < MAX_ATTEMPTS; i++) { + for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { try { log.info("Creating withdraw tx"); long startTime = System.currentTimeMillis(); @@ -270,12 +263,14 @@ public class WithdrawalView extends ActivatableView { .setAccountIndex(0) .setAmount(amount) .setAddress(withdrawToAddress) - .setSubtractFeeFrom(feeExcluded ? null : Arrays.asList(0))); + .setSubtractFeeFrom(sendMax ? Arrays.asList(0) : null)); log.info("Done creating withdraw tx in {} ms", System.currentTimeMillis() - startTime); break; } catch (Exception e) { - log.warn("Error creating creating withdraw tx, attempt={}/{}, error={}", i + 1, MAX_ATTEMPTS, e.getMessage()); - if (i == MAX_ATTEMPTS - 1) throw e; + if (isNotEnoughMoney(e.getMessage())) throw e; + log.warn("Error creating creating withdraw tx, attempt={}/{}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); + if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; + if (xmrWalletService.getConnectionService().isConnected()) xmrWalletService.requestSwitchToNextBestConnection(); HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying } } @@ -283,15 +278,17 @@ public class WithdrawalView extends ActivatableView { // popup confirmation message popupConfirmationMessage(tx); } catch (Throwable e) { - if (e.getMessage().contains("enough")) new Popup().warning(Res.get("funds.withdrawal.warn.amountExceeds")).show(); - else { - e.printStackTrace(); - new Popup().warning(e.getMessage()).show(); - } + e.printStackTrace(); + if (isNotEnoughMoney(e.getMessage())) new Popup().warning(Res.get("funds.withdrawal.notEnoughFunds")).show(); + else new Popup().warning(e.getMessage()).show(); } } } + private static boolean isNotEnoughMoney(String errorMsg) { + return errorMsg.contains("not enough"); + } + private void popupConfirmationMessage(MoneroTxWallet tx) { // create confirmation message @@ -347,6 +344,7 @@ public class WithdrawalView extends ActivatableView { /////////////////////////////////////////////////////////////////////////////////////////// private void reset() { + sendMax = false; amount = BigInteger.ZERO; amountTextField.setText(""); amountTextField.setPromptText(Res.get("funds.withdrawal.setAmount")); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java index f94ccbd3..e6d1d9d4 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferDataModel.java @@ -466,11 +466,6 @@ public abstract class MutableOfferDataModel extends OfferDataModel { } } - public boolean hasAvailableSplitOutput() { - BigInteger reserveAmount = totalToPay.get(); - return openOfferManager.hasAvailableOutput(reserveAmount); - } - /////////////////////////////////////////////////////////////////////////////////////////// // Utils /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java index 53df033e..db008e93 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferView.java @@ -690,9 +690,11 @@ public abstract class MutableOfferView> exten }; errorMessageListener = (o, oldValue, newValue) -> { - if (newValue != null) + if (model.createOfferCanceled) return; + if (newValue != null) { UserThread.runAfter(() -> new Popup().error(Res.get("createOffer.amountPriceBox.error.message", model.errorMessage.get())) - .show(), 100, TimeUnit.MILLISECONDS); + .show(), 100, TimeUnit.MILLISECONDS); + } }; paymentAccountsComboBoxSelectionHandler = e -> onPaymentAccountsComboBoxSelected(); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java index fa0dfbbf..33233083 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java @@ -109,6 +109,7 @@ public abstract class MutableOfferViewModel ext private String addressAsString; private final String paymentLabel; private boolean createOfferRequested; + public boolean createOfferCanceled; public final StringProperty amount = new SimpleStringProperty(); public final StringProperty minAmount = new SimpleStringProperty(); @@ -608,6 +609,7 @@ public abstract class MutableOfferViewModel ext void onPlaceOffer(Offer offer, Runnable resultHandler) { errorMessage.set(null); createOfferRequested = true; + createOfferCanceled = false; dataModel.onPlaceOffer(offer, transaction -> { resultHandler.run(); @@ -631,6 +633,7 @@ public abstract class MutableOfferViewModel ext public void onCancelOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { createOfferRequested = false; + createOfferCanceled = true; OpenOfferManager openOfferManager = HavenoUtils.openOfferManager; Optional openOffer = openOfferManager.getOpenOfferById(offer.getId()); if (openOffer.isPresent()) { diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java index ac10fd7d..ead182ba 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookView.java @@ -46,6 +46,7 @@ import haveno.desktop.components.AutoTooltipButton; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.AutoTooltipSlideToggleButton; import haveno.desktop.components.AutoTooltipTableColumn; +import haveno.desktop.components.AutoTooltipTextField; import haveno.desktop.components.AutocompleteComboBox; import haveno.desktop.components.ColoredDecimalPlacesWithZerosText; import haveno.desktop.components.HyperlinkWithIcon; @@ -107,6 +108,7 @@ import java.util.Map; import java.util.Optional; import static haveno.desktop.util.FormBuilder.addTitledGroupBg; +import static haveno.desktop.util.FormBuilder.addTopLabelAutoToolTipTextField; abstract public class OfferBookView extends ActivatableViewAndModel { @@ -122,6 +124,7 @@ abstract public class OfferBookView currencyComboBox; private AutocompleteComboBox paymentMethodComboBox; private AutoTooltipButton createOfferButton; + private AutoTooltipTextField filterInputField; private AutoTooltipSlideToggleButton matchingOffersToggle; private AutoTooltipTableColumn amountColumn; private AutoTooltipTableColumn volumeColumn; @@ -185,13 +188,13 @@ abstract public class OfferBookView> paymentBoxTuple = FormBuilder.addTopLabelAutocompleteComboBox( Res.get("offerbook.filterByPaymentMethod")); paymentMethodComboBox = paymentBoxTuple.third; paymentMethodComboBox.setCellFactory(GUIUtil.getPaymentMethodCellFactory()); - paymentMethodComboBox.setPrefWidth(270); + paymentMethodComboBox.setPrefWidth(250); matchingOffersToggle = new AutoTooltipSlideToggleButton(); matchingOffersToggle.setText(Res.get("offerbook.matchingOffers")); @@ -212,8 +215,13 @@ abstract public class OfferBookView autoToolTipTextField = addTopLabelAutoToolTipTextField(""); + VBox filterBox = autoToolTipTextField.first; + filterInputField = autoToolTipTextField.third; + filterInputField.setPromptText(Res.get("market.offerBook.filterPrompt")); + offerToolsBox.getChildren().addAll(currencyBoxTuple.first, paymentBoxTuple.first, - matchingOffersToggle, getSpacer(), createOfferButtonStack); + filterBox, matchingOffersToggle, getSpacer(), createOfferButtonStack); GridPane.setHgrow(offerToolsBox, Priority.ALWAYS); GridPane.setRowIndex(offerToolsBox, gridRow); @@ -427,6 +435,10 @@ abstract public class OfferBookView { + model.onFilterKeyTyped(filterInputField.getText()); + }); } private void updatePaymentMethodComboBoxEditor() { diff --git a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java index 23446012..f5fdd745 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/offerbook/OfferBookViewModel.java @@ -121,6 +121,7 @@ abstract class OfferBookViewModel extends ActivatableViewModel { PaymentMethod selectedPaymentMethod = getShowAllEntryForPaymentMethod(); boolean isTabSelected; + String filterText = ""; final BooleanProperty showAllTradeCurrenciesProperty = new SimpleBooleanProperty(true); final BooleanProperty disableMatchToggle = new SimpleBooleanProperty(); final IntegerProperty maxPlacesForAmount = new SimpleIntegerProperty(); @@ -269,6 +270,11 @@ abstract class OfferBookViewModel extends ActivatableViewModel { } } + void onFilterKeyTyped(String filterText) { + this.filterText = filterText; + filterOffers(); + } + abstract void saveSelectedCurrencyCodeInPreferences(OfferDirection direction, String code); protected void onSetPaymentMethod(PaymentMethod paymentMethod) { @@ -566,7 +572,25 @@ abstract class OfferBookViewModel extends ActivatableViewModel { Predicate predicate = useOffersMatchingMyAccountsFilter ? getCurrencyAndMethodPredicate(direction, selectedTradeCurrency).and(getOffersMatchingMyAccountsPredicate()) : getCurrencyAndMethodPredicate(direction, selectedTradeCurrency); - filteredItems.setPredicate(predicate); + + if (!filterText.isEmpty()) { + + // filter node address + Predicate nextPredicate = offerBookListItem -> + offerBookListItem.getOffer().getOfferPayload().getOwnerNodeAddress().getFullAddress().toLowerCase().contains(filterText.toLowerCase()); + + // filter offer id + nextPredicate = nextPredicate.or(offerBookListItem -> + offerBookListItem.getOffer().getId().toLowerCase().contains(filterText.toLowerCase())); + + // filter payment method + nextPredicate = nextPredicate.or(offerBookListItem -> + Res.get(offerBookListItem.getOffer().getPaymentMethod().getId()).toLowerCase().contains(filterText.toLowerCase())); + + filteredItems.setPredicate(predicate.and(nextPredicate)); + } else { + filteredItems.setPredicate(predicate); + } } abstract Predicate getCurrencyAndMethodPredicate(OfferDirection direction, diff --git a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java index 2b8899c3..1b0867ef 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/takeoffer/TakeOfferView.java @@ -158,7 +158,8 @@ public class TakeOfferView extends ActivatableViewAndModel paymentAccountWarningDisplayed = new HashMap<>(); private boolean offerDetailsWindowDisplayed, zelleWarningDisplayed, fasterPaymentsWarningDisplayed, - takeOfferFromUnsignedAccountWarningDisplayed, payByMailWarningDisplayed, cashAtAtmWarningDisplayed, australiaPayidWarningDisplayed; + takeOfferFromUnsignedAccountWarningDisplayed, payByMailWarningDisplayed, cashAtAtmWarningDisplayed, + australiaPayidWarningDisplayed, paypalWarningDisplayed, cashAppWarningDisplayed; private SimpleBooleanProperty errorPopupDisplayed; private ChangeListener amountFocusedListener, getShowWalletFundedNotificationListener; @@ -268,6 +269,8 @@ public class TakeOfferView extends ActivatableViewAndModel { + new GenericMessageWindow() + .preamble(Res.get("payment.tradingRestrictions")) + .instruction(offer.getExtraInfo()) + .actionButtonText(Res.get("shared.iConfirm")) + .closeButtonText(Res.get("shared.close")) + .width(Layout.INITIAL_WINDOW_WIDTH) + .onClose(() -> close(false)) + .show(); + }, 500, TimeUnit.MILLISECONDS); + } + } + + private void maybeShowCashAppWarning(PaymentAccount paymentAccount, Offer offer) { + if (paymentAccount.getPaymentMethod().getId().equals(PaymentMethod.CASH_APP_ID) && + !cashAppWarningDisplayed && !offer.getExtraInfo().isEmpty()) { + cashAppWarningDisplayed = true; + UserThread.runAfter(() -> { + new GenericMessageWindow() + .preamble(Res.get("payment.tradingRestrictions")) + .instruction(offer.getExtraInfo()) + .actionButtonText(Res.get("shared.iConfirm")) + .closeButtonText(Res.get("shared.close")) + .width(Layout.INITIAL_WINDOW_WIDTH) + .onClose(() -> close(false)) + .show(); + }, 500, TimeUnit.MILLISECONDS); + } + } + private Tuple2 getTradeInputBox(HBox amountValueBox, String promptText) { Label descriptionLabel = new AutoTooltipLabel(promptText); descriptionLabel.setId("input-description-label"); diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java index 0692162e..e93dc647 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/OfferDetailsWindow.java @@ -174,7 +174,11 @@ public class OfferDetailsWindow extends Overlay { List acceptedCountryCodes = offer.getAcceptedCountryCodes(); boolean showAcceptedCountryCodes = acceptedCountryCodes != null && !acceptedCountryCodes.isEmpty(); boolean isF2F = offer.getPaymentMethod().equals(PaymentMethod.F2F); - boolean showExtraInfo = offer.getPaymentMethod().equals(PaymentMethod.F2F) || offer.getPaymentMethod().equals(PaymentMethod.PAY_BY_MAIL) || offer.getPaymentMethod().equals(PaymentMethod.AUSTRALIA_PAYID); + boolean showExtraInfo = offer.getPaymentMethod().equals(PaymentMethod.F2F) || + offer.getPaymentMethod().equals(PaymentMethod.PAY_BY_MAIL) || + offer.getPaymentMethod().equals(PaymentMethod.AUSTRALIA_PAYID)|| + offer.getPaymentMethod().equals(PaymentMethod.PAYPAL_ID)|| + offer.getPaymentMethod().equals(PaymentMethod.CASH_APP_ID); if (!takeOfferHandlerOptional.isPresent()) rows++; if (showAcceptedBanks) diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java index d3f2d787..c1f55663 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java @@ -296,7 +296,7 @@ public class OpenOffersView extends ActivatableViewAndModel availableItems = sortedList.stream() - .filter(openOfferListItem -> !openOfferListItem.getOpenOffer().isScheduled()) + .filter(openOfferListItem -> !openOfferListItem.getOpenOffer().isPending()) .collect(Collectors.toList()); if (availableItems.size() == 0) { selectToggleButton.setDisable(true); @@ -710,7 +710,7 @@ public class OpenOffersView extends ActivatableViewAndModel { }, errorMessage -> { busyAnimation.stop(); - new Popup().warning(Res.get("popup.warning.sendMsgFailed")).show(); + new Popup().warning(Res.get("popup.warning.sendMsgFailed") + "\n\n" + errorMessage).show(); confirmButton.setDisable(!confirmPaymentReceivedPermitted()); UserThread.execute(() -> statusLabel.setText("Error confirming payment received.")); }); diff --git a/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java b/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java index 89c0e750..b3409371 100644 --- a/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java +++ b/desktop/src/main/java/haveno/desktop/main/settings/network/NetworkSettingsView.java @@ -107,7 +107,7 @@ public class NetworkSettingsView extends ActivatableView { private final ClockWatcher clockWatcher; private final WalletsSetup walletsSetup; private final P2PService p2PService; - private final XmrConnectionService connectionManager; + private final XmrConnectionService connectionService; private final ObservableList p2pNetworkListItems = FXCollections.observableArrayList(); private final SortedList p2pSortedList = new SortedList<>(p2pNetworkListItems); @@ -131,7 +131,7 @@ public class NetworkSettingsView extends ActivatableView { @Inject public NetworkSettingsView(WalletsSetup walletsSetup, P2PService p2PService, - XmrConnectionService connectionManager, + XmrConnectionService connectionService, Preferences preferences, XmrNodes xmrNodes, FilterManager filterManager, @@ -141,7 +141,7 @@ public class NetworkSettingsView extends ActivatableView { super(); this.walletsSetup = walletsSetup; this.p2PService = p2PService; - this.connectionManager = connectionManager; + this.connectionService = connectionService; this.preferences = preferences; this.xmrNodes = xmrNodes; this.filterManager = filterManager; @@ -303,10 +303,10 @@ public class NetworkSettingsView extends ActivatableView { rescanOutputsButton.setOnAction(event -> GUIUtil.rescanOutputs(preferences)); - moneroPeersSubscription = EasyBind.subscribe(connectionManager.peerConnectionsProperty(), + moneroPeersSubscription = EasyBind.subscribe(connectionService.peerConnectionsProperty(), this::updateMoneroPeersTable); - moneroBlockHeightSubscription = EasyBind.subscribe(connectionManager.chainHeightProperty(), + moneroBlockHeightSubscription = EasyBind.subscribe(connectionService.chainHeightProperty(), this::updateChainHeightTextField); nodeAddressSubscription = EasyBind.subscribe(p2PService.getNetworkNode().nodeAddressProperty(), @@ -503,6 +503,7 @@ public class NetworkSettingsView extends ActivatableView { } private void updateP2PTable() { + if (connectionService.isShutDownStarted()) return; // ignore if shutting down p2pPeersTableView.getItems().forEach(P2pNetworkListItem::cleanup); p2pNetworkListItems.clear(); p2pNetworkListItems.setAll(p2PService.getNetworkNode().getAllConnections().stream() diff --git a/desktop/src/main/java/haveno/desktop/util/FormBuilder.java b/desktop/src/main/java/haveno/desktop/util/FormBuilder.java index ba698752..4181245d 100644 --- a/desktop/src/main/java/haveno/desktop/util/FormBuilder.java +++ b/desktop/src/main/java/haveno/desktop/util/FormBuilder.java @@ -36,6 +36,7 @@ import haveno.desktop.components.AutoTooltipCheckBox; import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.AutoTooltipRadioButton; import haveno.desktop.components.AutoTooltipSlideToggleButton; +import haveno.desktop.components.AutoTooltipTextField; import haveno.desktop.components.AutocompleteComboBox; import haveno.desktop.components.BalanceTextField; import haveno.desktop.components.BusyAnimation; @@ -1159,35 +1160,28 @@ public class FormBuilder { } /////////////////////////////////////////////////////////////////////////////////////////// - // Label + TextField + RadioButton + RadioButton + // Label + TextField + HyperlinkWithIcon /////////////////////////////////////////////////////////////////////////////////////////// - public static Tuple4 addTopLabelTextFieldRadioButtonRadioButton(GridPane gridPane, - int rowIndex, - ToggleGroup toggleGroup, - String title, - String textFieldTitle, - String radioButtonTitle1, - String radioButtonTitle2, - double top) { + public static Tuple3 addTopLabelTextFieldHyperLink(GridPane gridPane, + int rowIndex, + String title, + String textFieldTitle, + String maxButtonTitle, + double top) { TextField textField = new HavenoTextField(); textField.setPromptText(textFieldTitle); - RadioButton radioButton1 = new AutoTooltipRadioButton(radioButtonTitle1); - radioButton1.setToggleGroup(toggleGroup); - radioButton1.setPadding(new Insets(6, 0, 0, 0)); - - RadioButton radioButton2 = new AutoTooltipRadioButton(radioButtonTitle2); - radioButton2.setToggleGroup(toggleGroup); - radioButton2.setPadding(new Insets(6, 0, 0, 0)); + HyperlinkWithIcon maxLink = new ExternalHyperlink(maxButtonTitle); HBox hBox = new HBox(); hBox.setSpacing(10); - hBox.getChildren().addAll(textField, radioButton1, radioButton2); + hBox.getChildren().addAll(textField, maxLink); + hBox.setAlignment(Pos.CENTER_LEFT); final Tuple2 labelVBoxTuple2 = addTopLabelWithVBox(gridPane, rowIndex, title, hBox, top); - return new Tuple4<>(labelVBoxTuple2.first, textField, radioButton1, radioButton2); + return new Tuple3<>(labelVBoxTuple2.first, textField, maxLink); } @@ -1293,6 +1287,20 @@ public class FormBuilder { return new Tuple3<>(vBox, label, comboBox); } + public static Tuple3 addTopLabelAutoToolTipTextField(String title) { + return addTopLabelAutoToolTipTextField(title, 0); + } + + public static Tuple3 addTopLabelAutoToolTipTextField(String title, int top) { + Label label = getTopLabel(title); + VBox vBox = getTopLabelVBox(top); + + final AutoTooltipTextField textField = new AutoTooltipTextField(); + vBox.getChildren().addAll(label, textField); + + return new Tuple3<>(vBox, label, textField); + } + @NotNull private static VBox getTopLabelVBox(int top) { VBox vBox = new VBox(); diff --git a/docs/deployment-guide.md b/docs/deployment-guide.md index 3a814dd8..97aaf046 100644 --- a/docs/deployment-guide.md +++ b/docs/deployment-guide.md @@ -22,9 +22,35 @@ Arbitrators can be started in a Screen session and then detached to run in the b Some good hints about how to secure a VPS are in [Monero's meta repository](https://github.com/monero-project/meta/blob/master/SERVER_SETUP_HARDENING.md). +## Install dependencies + +On Linux and macOS, install Java JDK 21: + +``` +curl -s "https://get.sdkman.io" | bash +sdk install java 21.0.2.fx-librca +``` + +Alternatively, on Ubuntu 22.04: + +`sudo apt-get install openjdk-21-jdk` + +On Windows, install MSYS2 and Java JDK 21: + +1. Install [MSYS2](https://www.msys2.org/). +2. Start MSYS2 MINGW64 or MSYS MINGW32 depending on your system. Use MSYS2 for all commands throughout this document. +4. Update pacman: `pacman -Syy` +5. Install dependencies. During installation, use default=all by leaving the input blank and pressing enter. + + 64-bit: `pacman -S mingw-w64-x86_64-toolchain make mingw-w64-x86_64-cmake git` + + 32-bit: `pacman -S mingw-w64-i686-toolchain make mingw-w64-i686-cmake git` +6. `curl -s "https://get.sdkman.io" | bash` +7. `sdk install java 21.0.2.fx-librca` + ## Fork and build Haveno -First fork Haveno to a public repository. Then build Haveno: +Fork Haveno to a public repository. Then build Haveno: ``` git clone @@ -57,9 +83,12 @@ For each seed node: 1. [Build the Haveno repository](#fork-and-build-haveno). 2. [Start a local Monero node](#start-a-local-monero-node). -3. Run `make seednode` to run a seednode on Monero's mainnet or `make seednode-stagenet` to run a seednode on Monero's stagenet. -4. The node will print its onion address to the console. Record the onion address in `core/src/main/resources/xmr_.seednodes`. Be careful to record full addresses correctly. -5. Update all seed nodes, arbitrators, and user applications for the change to take effect. +3. Modify `./scripts/deployment/haveno-seednode.service` and `./scripts/deployment/haveno-seednode2.service` as needed. +4. Copy `./scripts/deployment/haveno-seednode.service` to `/etc/systemd/system` (if you are the very first seed in a new network also copy `./scripts/deployment/haveno-seednode2.service` to `/etc/systemd/system`). +5. Run `sudo systemctl start haveno-seednode.service` to start the seednode and also run `sudo systemctl start haveno-seednode2.service` if you are the very first seed in a new network and coppied haveno-seednode2.service to your systemd folder. +6. Run `journalctl -u haveno-seednode.service -b -f` which will print the log and show the `.onion` address of the seed node. Press `Ctrl+C` to stop printing the log and record the `.onion` address given. +7. Add the `.onion` address to `core/src/main/resources/xmr_.seednodes` along with the port specified in the haveno-seednode.service file(s) `(ex: example.onion:1002)`. Be careful to record full addresses correctly. +8. Update all seed nodes, arbitrators, and user applications for the change to take effect. Customize and deploy haveno-seednode.service to run a seed node as a system service. @@ -222,4 +251,4 @@ Arbitrators can manually sign payment accounts. First open the legacy UI. ## Other tips * If a dispute does not open properly, try manually reopening the dispute with a keyboard shortcut: `ctrl + o`. -* To send a private notification to a peer: click the user icon and enter `alt + r`. Enter a private key which is registered to send private notifications. \ No newline at end of file +* To send a private notification to a peer: click the user icon and enter `alt + r`. Enter a private key which is registered to send private notifications. diff --git a/p2p/src/main/java/haveno/network/p2p/network/Connection.java b/p2p/src/main/java/haveno/network/p2p/network/Connection.java index 137e84a5..406cfe25 100644 --- a/p2p/src/main/java/haveno/network/p2p/network/Connection.java +++ b/p2p/src/main/java/haveno/network/p2p/network/Connection.java @@ -170,6 +170,11 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { private final Capabilities capabilities = new Capabilities(); + // throttle logs of reported invalid requests + private static long lastLoggedInvalidRequestReport = 0; + private static int unloggedInvalidRequestReports = 0; + private static final long LOG_INVALID_REQUEST_REPORTS_INTERVAL_MS = 60000; // log invalid request reports once every 60s + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// @@ -606,35 +611,55 @@ public class Connection implements HasCapabilities, Runnable, MessageListener { */ public boolean reportInvalidRequest(RuleViolation ruleViolation) { - log.info("We got reported the ruleViolation {} at connection with address{} and uid {}", ruleViolation, this.getPeersNodeAddressProperty(), this.getUid()); + return Connection.reportInvalidRequest(this, ruleViolation); + } + + private static synchronized boolean reportInvalidRequest(Connection connection, RuleViolation ruleViolation) { + + // determine if report should be logged to avoid spamming the logs + boolean logReport = System.currentTimeMillis() - lastLoggedInvalidRequestReport > LOG_INVALID_REQUEST_REPORTS_INTERVAL_MS; + + // count the number of unlogged reports since last log entry + if (!logReport) unloggedInvalidRequestReports++; + + // handle report + if (logReport) log.info("We got reported the ruleViolation {} at connection with address {} and uid {}", ruleViolation, connection.getPeersNodeAddressProperty(), connection.getUid()); int numRuleViolations; - numRuleViolations = ruleViolations.getOrDefault(ruleViolation, 0); - + numRuleViolations = connection.ruleViolations.getOrDefault(ruleViolation, 0); numRuleViolations++; - ruleViolations.put(ruleViolation, numRuleViolations); - + connection.ruleViolations.put(ruleViolation, numRuleViolations); if (numRuleViolations >= ruleViolation.maxTolerance) { - log.warn("We close connection as we received too many corrupt requests. " + + if (logReport) log.warn("We close connection as we received too many corrupt requests. " + "ruleViolations={} " + - "connection with address{} and uid {}", ruleViolations, peersNodeAddressProperty, uid); - this.ruleViolation = ruleViolation; + "connection with address {} and uid {}", connection.ruleViolations, connection.peersNodeAddressProperty, connection.uid); + connection.ruleViolation = ruleViolation; if (ruleViolation == RuleViolation.PEER_BANNED) { - log.debug("We close connection due RuleViolation.PEER_BANNED. peersNodeAddress={}", getPeersNodeAddressOptional()); - shutDown(CloseConnectionReason.PEER_BANNED); + if (logReport) log.debug("We close connection due RuleViolation.PEER_BANNED. peersNodeAddress={}", connection.getPeersNodeAddressOptional()); + connection.shutDown(CloseConnectionReason.PEER_BANNED); } else if (ruleViolation == RuleViolation.INVALID_CLASS) { - log.warn("We close connection due RuleViolation.INVALID_CLASS"); - shutDown(CloseConnectionReason.INVALID_CLASS_RECEIVED); + if (logReport) log.warn("We close connection due RuleViolation.INVALID_CLASS"); + connection.shutDown(CloseConnectionReason.INVALID_CLASS_RECEIVED); } else { - log.warn("We close connection due RuleViolation.RULE_VIOLATION"); - shutDown(CloseConnectionReason.RULE_VIOLATION); + if (logReport) log.warn("We close connection due RuleViolation.RULE_VIOLATION"); + connection.shutDown(CloseConnectionReason.RULE_VIOLATION); } + resetReportedInvalidRequestsThrottle(logReport); return true; } else { + resetReportedInvalidRequestsThrottle(logReport); return false; } } + private static synchronized void resetReportedInvalidRequestsThrottle(boolean logReport) { + if (logReport) { + if (unloggedInvalidRequestReports > 0) log.warn("We received {} other reports of invalid requests since the last log entry", unloggedInvalidRequestReports); + unloggedInvalidRequestReports = 0; + lastLoggedInvalidRequestReport = System.currentTimeMillis(); + } + } + private void handleException(Throwable e) { CloseConnectionReason closeConnectionReason; diff --git a/p2p/src/main/java/haveno/network/p2p/network/NetworkNode.java b/p2p/src/main/java/haveno/network/p2p/network/NetworkNode.java index d74ef1fd..f24fdf6f 100644 --- a/p2p/src/main/java/haveno/network/p2p/network/NetworkNode.java +++ b/p2p/src/main/java/haveno/network/p2p/network/NetworkNode.java @@ -38,6 +38,7 @@ import com.google.common.util.concurrent.SettableFuture; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.SimpleObjectProperty; +import lombok.Getter; import java.net.ServerSocket; import java.net.Socket; @@ -82,7 +83,8 @@ public abstract class NetworkNode implements MessageListener { private final ListeningExecutorService sendMessageExecutor; private Server server; - private volatile boolean shutDownInProgress; + @Getter + private volatile boolean isShutDownStarted; // accessed from different threads private final CopyOnWriteArraySet outBoundConnections = new CopyOnWriteArraySet<>(); protected final ObjectProperty nodeAddressProperty = new SimpleObjectProperty<>(); @@ -181,7 +183,7 @@ public abstract class NetworkNode implements MessageListener { try { socket.close(); } catch (Throwable throwable) { - if (!shutDownInProgress) { + if (!isShutDownStarted) { log.error("Error at closing socket " + throwable); } } @@ -362,8 +364,8 @@ public abstract class NetworkNode implements MessageListener { public void shutDown(Runnable shutDownCompleteHandler) { log.info("NetworkNode shutdown started"); - if (!shutDownInProgress) { - shutDownInProgress = true; + if (!isShutDownStarted) { + isShutDownStarted = true; if (server != null) { server.shutDown(); server = null; diff --git a/p2p/src/main/java/haveno/network/p2p/peers/BroadcastHandler.java b/p2p/src/main/java/haveno/network/p2p/peers/BroadcastHandler.java index 238a509b..fca906ff 100644 --- a/p2p/src/main/java/haveno/network/p2p/peers/BroadcastHandler.java +++ b/p2p/src/main/java/haveno/network/p2p/peers/BroadcastHandler.java @@ -352,7 +352,13 @@ public class BroadcastHandler implements PeerManager.Listener { sendMessageFutures.stream() .filter(future -> !future.isCancelled() && !future.isDone()) - .forEach(future -> future.cancel(true)); + .forEach(future -> { + try { + future.cancel(true); + } catch (Exception e) { + if (!networkNode.isShutDownStarted()) throw e; + } + }); sendMessageFutures.clear(); peerManager.removeListener(this); diff --git a/p2p/src/main/java/haveno/network/p2p/peers/getdata/RequestDataHandler.java b/p2p/src/main/java/haveno/network/p2p/peers/getdata/RequestDataHandler.java index 85a1cc89..f155c0e8 100644 --- a/p2p/src/main/java/haveno/network/p2p/peers/getdata/RequestDataHandler.java +++ b/p2p/src/main/java/haveno/network/p2p/peers/getdata/RequestDataHandler.java @@ -140,33 +140,38 @@ class RequestDataHandler implements MessageListener { getDataRequestType = getDataRequest.getClass().getSimpleName(); log.info("\n\n>> We send a {} to peer {}\n", getDataRequestType, nodeAddress); networkNode.addMessageListener(this); - SettableFuture future = networkNode.sendMessage(nodeAddress, getDataRequest); - //noinspection UnstableApiUsage - Futures.addCallback(future, new FutureCallback<>() { - @Override - public void onSuccess(Connection connection) { - if (!stopped) { - log.trace("Send {} to {} succeeded.", getDataRequest, nodeAddress); - } else { - log.trace("We have stopped already. We ignore that networkNode.sendMessage.onSuccess call." + - "Might be caused by a previous timeout."); - } - } - @Override - public void onFailure(@NotNull Throwable throwable) { - if (!stopped) { - String errorMessage = "Sending getDataRequest to " + nodeAddress + - " failed. That is expected if the peer is offline.\n\t" + - "getDataRequest=" + getDataRequest + "." + - "\n\tException=" + throwable.getMessage(); - handleFault(errorMessage, nodeAddress, CloseConnectionReason.SEND_MSG_FAILURE); - } else { - log.trace("We have stopped already. We ignore that networkNode.sendMessage.onFailure call. " + - "Might be caused by a previous timeout."); + try { + SettableFuture future = networkNode.sendMessage(nodeAddress, getDataRequest); + //noinspection UnstableApiUsage + Futures.addCallback(future, new FutureCallback<>() { + @Override + public void onSuccess(Connection connection) { + if (!stopped) { + log.trace("Send {} to {} succeeded.", getDataRequest, nodeAddress); + } else { + log.trace("We have stopped already. We ignore that networkNode.sendMessage.onSuccess call." + + "Might be caused by a previous timeout."); + } } - } - }, MoreExecutors.directExecutor()); + + @Override + public void onFailure(@NotNull Throwable throwable) { + if (!stopped) { + String errorMessage = "Sending getDataRequest to " + nodeAddress + + " failed. That is expected if the peer is offline.\n\t" + + "getDataRequest=" + getDataRequest + "." + + "\n\tException=" + throwable.getMessage(); + handleFault(errorMessage, nodeAddress, CloseConnectionReason.SEND_MSG_FAILURE); + } else { + log.trace("We have stopped already. We ignore that networkNode.sendMessage.onFailure call. " + + "Might be caused by a previous timeout."); + } + } + }, MoreExecutors.directExecutor()); + } catch (Exception e) { + if (!networkNode.isShutDownStarted()) throw e; + } } else { log.warn("We have stopped already. We ignore that requestData call."); } diff --git a/p2p/src/main/java/haveno/network/p2p/peers/peerexchange/PeerExchangeHandler.java b/p2p/src/main/java/haveno/network/p2p/peers/peerexchange/PeerExchangeHandler.java index cd12c17a..1a929289 100644 --- a/p2p/src/main/java/haveno/network/p2p/peers/peerexchange/PeerExchangeHandler.java +++ b/p2p/src/main/java/haveno/network/p2p/peers/peerexchange/PeerExchangeHandler.java @@ -115,35 +115,39 @@ class PeerExchangeHandler implements MessageListener { TIMEOUT, TimeUnit.SECONDS); } - SettableFuture future = networkNode.sendMessage(nodeAddress, getPeersRequest); - Futures.addCallback(future, new FutureCallback() { - @Override - public void onSuccess(Connection connection) { - if (!stopped) { - //TODO - /*if (!connection.getPeersNodeAddressOptional().isPresent()) { - connection.setPeersNodeAddress(nodeAddress); - log.warn("sendGetPeersRequest: !connection.getPeersNodeAddressOptional().isPresent()"); - }*/ - - PeerExchangeHandler.this.connection = connection; - connection.addMessageListener(PeerExchangeHandler.this); - } else { - log.trace("We have stopped that handler already. We ignore that sendGetPeersRequest.onSuccess call."); + try { + SettableFuture future = networkNode.sendMessage(nodeAddress, getPeersRequest); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(Connection connection) { + if (!stopped) { + //TODO + /*if (!connection.getPeersNodeAddressOptional().isPresent()) { + connection.setPeersNodeAddress(nodeAddress); + log.warn("sendGetPeersRequest: !connection.getPeersNodeAddressOptional().isPresent()"); + }*/ + + PeerExchangeHandler.this.connection = connection; + connection.addMessageListener(PeerExchangeHandler.this); + } else { + log.trace("We have stopped that handler already. We ignore that sendGetPeersRequest.onSuccess call."); + } } - } - - @Override - public void onFailure(@NotNull Throwable throwable) { - if (!stopped) { - String errorMessage = "Sending getPeersRequest to " + nodeAddress + - " failed. That is expected if the peer is offline. Exception=" + throwable.getMessage(); - handleFault(errorMessage, CloseConnectionReason.SEND_MSG_FAILURE, nodeAddress); - } else { - log.trace("We have stopped that handler already. We ignore that sendGetPeersRequest.onFailure call."); + + @Override + public void onFailure(@NotNull Throwable throwable) { + if (!stopped) { + String errorMessage = "Sending getPeersRequest to " + nodeAddress + + " failed. That is expected if the peer is offline. Exception=" + throwable.getMessage(); + handleFault(errorMessage, CloseConnectionReason.SEND_MSG_FAILURE, nodeAddress); + } else { + log.trace("We have stopped that handler already. We ignore that sendGetPeersRequest.onFailure call."); + } } - } - }, MoreExecutors.directExecutor()); + }, MoreExecutors.directExecutor()); + } catch (Exception e) { + if (!networkNode.isShutDownStarted()) throw e; + } } else { log.debug("My node address is still null at sendGetPeersRequest. We ignore that call."); } diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index c3dcf498..1bb4c9f5 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -586,6 +586,8 @@ service PaymentAccounts { } rpc CreateCryptoCurrencyPaymentAccount (CreateCryptoCurrencyPaymentAccountRequest) returns (CreateCryptoCurrencyPaymentAccountReply) { } + rpc DeletePaymentAccount (DeletePaymentAccountRequest) returns (DeletePaymentAccountReply) { + } rpc GetCryptoCurrencyPaymentMethods (GetCryptoCurrencyPaymentMethodsRequest) returns (GetCryptoCurrencyPaymentMethodsReply) { } rpc ValidateFormField (ValidateFormFieldRequest) returns (ValidateFormFieldReply) { @@ -639,6 +641,13 @@ message CreateCryptoCurrencyPaymentAccountRequest { bool trade_instant = 4; } +message DeletePaymentAccountRequest { + string payment_account_id = 1; +} + +message DeletePaymentAccountReply { +} + message CreateCryptoCurrencyPaymentAccountReply { PaymentAccount payment_account = 1; } diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 8e988bbe..03cc6b23 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -1061,6 +1061,7 @@ message UpholdAccountPayload { message CashAppAccountPayload { string email_or_mobile_nr_or_cashtag = 1; + string extra_info = 2; } message MoneyBeamAccountPayload { @@ -1072,6 +1073,7 @@ message VenmoAccountPayload { } message PayPalAccountPayload { string email_or_mobile_nr_or_username = 1; + string extra_info = 2; } message PopmoneyAccountPayload { @@ -1391,7 +1393,7 @@ message SignedOffer { message OpenOffer { enum State { PB_ERROR = 0; - SCHEDULED = 1; + PENDING = 1; AVAILABLE = 2; RESERVED = 3; CLOSED = 4; diff --git a/relay/torrc b/relay/torrc index 29f757a7..3bfc6285 100644 --- a/relay/torrc +++ b/relay/torrc @@ -1,3 +1,2 @@ HiddenServiceDir build/tor-hidden-service HiddenServicePort 80 127.0.0.1:8080 -HiddenServicePoWDefensesEnabled 1 diff --git a/scripts/deployment/haveno-seednode.service b/scripts/deployment/haveno-seednode.service index 3e7ced65..fdb32569 100644 --- a/scripts/deployment/haveno-seednode.service +++ b/scripts/deployment/haveno-seednode.service @@ -13,7 +13,7 @@ ExecStart=/bin/sh /home/haveno/haveno/haveno-seednode --baseCurrencyNetwork=XMR_ --nodePort=2002\ --appName=haveno-XMR_STAGENET_Seed_2002\ # --logLevel=trace\ - --xmrNode=http://127.0.0.1:38088\ + --xmrNode=http://[::1]:38088\ --xmrNodeUsername=admin\ --xmrNodePassword=password @@ -33,4 +33,4 @@ RestrictSUIDSGID=true LimitRSS=2000000000 [Install] -WantedBy=multi-user.target \ No newline at end of file +WantedBy=multi-user.target diff --git a/scripts/deployment/haveno-seednode2.service b/scripts/deployment/haveno-seednode2.service index d5937f2a..7b6c139a 100644 --- a/scripts/deployment/haveno-seednode2.service +++ b/scripts/deployment/haveno-seednode2.service @@ -13,7 +13,7 @@ ExecStart=/bin/sh /home/haveno/haveno/haveno-seednode --baseCurrencyNetwork=XMR_ --nodePort=3003\ --appName=haveno-XMR_STAGENET_Seed_3003\ # --logLevel=trace\ - --xmrNode=http://127.0.0.1:38088\ + --xmrNode=http://[::1]:38088\ --xmrNodeUsername=admin\ --xmrNodePassword=password @@ -33,4 +33,4 @@ RestrictSUIDSGID=true LimitRSS=2000000000 [Install] -WantedBy=multi-user.target \ No newline at end of file +WantedBy=multi-user.target diff --git a/seednode/haveno-seednode.service b/seednode/haveno-seednode.service index 7551c54f..131e49e8 100644 --- a/seednode/haveno-seednode.service +++ b/seednode/haveno-seednode.service @@ -13,8 +13,9 @@ ExecStart=/bin/sh $PATH/haveno-seednode --baseCurrencyNetwork=XMR_STAGENET\ --useDevPrivilegeKeys=false\ --nodePort=2002\ --appName=haveno-XMR_STAGENET_Seed_2002 + --xmrNode=[::1]:38088 -ExecStop=/bin/kill ${MAINPID} +ExecStop=/bin/kill ${MAINPID} ; sleep 5 Restart=always # Hardening diff --git a/seednode/haveno.env b/seednode/haveno.env index 31e07d7a..9b7ff903 100644 --- a/seednode/haveno.env +++ b/seednode/haveno.env @@ -5,7 +5,7 @@ JAVA_HOME=/usr/lib/jvm/openjdk-21 # java memory and remote management options -JAVA_OPTS="-Xms4096M -Xmx4096M" -XX:+ExitOnOutOfMemoryError" +JAVA_OPTS="-Xms4096M -Xmx4096M -XX:+ExitOnOutOfMemoryError" # use external tor (change to -1 for internal tor binary) HAVENO_EXTERNAL_TOR_PORT=9051 diff --git a/seednode/src/main/java/haveno/seednode/SeedNodeMain.java b/seednode/src/main/java/haveno/seednode/SeedNodeMain.java index e4e47684..30f3f130 100644 --- a/seednode/src/main/java/haveno/seednode/SeedNodeMain.java +++ b/seednode/src/main/java/haveno/seednode/SeedNodeMain.java @@ -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.8"; + private static final String VERSION = "1.0.9"; private SeedNode seedNode; private Timer checkConnectionLossTime;