update to v1.0.9

v1.0.9
This commit is contained in:
retoaccess1 2024-07-21 14:39:41 +02:00 committed by GitHub
commit 76eea4c009
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
76 changed files with 1620 additions and 918 deletions

View File

@ -40,24 +40,9 @@ deploy:
screen -S localnet -X screen -t $$target; \ screen -S localnet -X screen -t $$target; \
screen -S localnet -p $$target -X stuff "make $$target\n"; \ screen -S localnet -p $$target -X stuff "make $$target\n"; \
done; done;
# give bitcoind rpc server time to start # give time to start
sleep 5 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 .PHONY: build seednode localnet
# Local network # Local network

View File

@ -163,28 +163,33 @@ configure([project(':cli'),
// edit generated shell scripts such that they expect to be executed in the // edit generated shell scripts such that they expect to be executed in the
// project root dir as opposed to a 'bin' subdirectory // project root dir as opposed to a 'bin' subdirectory
def windowsScriptFile = file("${rootProject.projectDir}/haveno-${applicationName}.bat") if (osdetector.os == 'windows') {
windowsScriptFile.text = windowsScriptFile.text.replace( def windowsScriptFile = file("${rootProject.projectDir}/haveno-${applicationName}.bat")
'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') {
windowsScriptFile.text = windowsScriptFile.text.replace( windowsScriptFile.text = windowsScriptFile.text.replace(
'DEFAULT_JVM_OPTS=', 'DEFAULT_JVM_OPTS=-XX:MaxRAM=4g ' + 'set APP_HOME=%DIRNAME%..', 'set APP_HOME=%DIRNAME%')
'--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 == '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( unixScriptFile.text = unixScriptFile.text.replace(
'DEFAULT_JVM_OPTS=""', 'DEFAULT_JVM_OPTS="-XX:MaxRAM=4g ' + 'APP_HOME=$( cd "${APP_HOME:-./}.." > /dev/null && pwd -P ) || exit', 'APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit')
'--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=ALL-UNNAMED ' +
'--add-opens=javafx.controls/com.sun.javafx.scene.control=ALL-UNNAMED ' + if (applicationName == 'desktop') {
'--add-opens=java.base/java.lang.reflect=ALL-UNNAMED ' + unixScriptFile.text = unixScriptFile.text.replace(
'--add-opens=javafx.graphics/com.sun.javafx.scene=ALL-UNNAMED"') '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') { if (applicationName == 'apitest') {
@ -605,7 +610,7 @@ configure(project(':desktop')) {
apply plugin: 'com.github.johnrengelman.shadow' apply plugin: 'com.github.johnrengelman.shadow'
apply from: 'package/package.gradle' apply from: 'package/package.gradle'
version = '1.0.8-SNAPSHOT' version = '1.0.9-SNAPSHOT'
jar.manifest.attributes( jar.manifest.attributes(
"Implementation-Title": project.name, "Implementation-Title": project.name,

View File

@ -47,6 +47,7 @@ public class ThreadUtils {
synchronized (THREADS) { synchronized (THREADS) {
THREADS.put(threadId, Thread.currentThread()); THREADS.put(threadId, Thread.currentThread());
} }
Thread.currentThread().setName(threadId);
command.run(); command.run();
}); });
} }

View File

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

View File

@ -496,6 +496,10 @@ public class CoreApi {
tradeInstant); tradeInstant);
} }
public void deletePaymentAccount(String paymentAccountId) {
paymentAccountsService.deletePaymentAccount(paymentAccountId);
}
public List<PaymentMethod> getCryptoCurrencyPaymentMethods() { public List<PaymentMethod> getCryptoCurrencyPaymentMethods() {
return paymentAccountsService.getCryptoCurrencyPaymentMethods(); return paymentAccountsService.getCryptoCurrencyPaymentMethods();
} }
@ -557,10 +561,6 @@ public class CoreApi {
return coreTradesService.getTrades(); return coreTradesService.getTrades();
} }
public String getTradeRole(String tradeId) {
return coreTradesService.getTradeRole(tradeId);
}
public List<ChatMessage> getChatMessages(String tradeId) { public List<ChatMessage> getChatMessages(String tradeId) {
return coreTradesService.getChatMessages(tradeId); return coreTradesService.getChatMessages(tradeId);
} }

View File

@ -145,6 +145,16 @@ class CorePaymentAccountsService {
return cryptoCurrencyAccount; 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. // TODO Support all alt coin payment methods supported by UI.
// The getCryptoCurrencyPaymentMethods method below will be // The getCryptoCurrencyPaymentMethods method below will be
// callable from the CLI when more are supported. // callable from the CLI when more are supported.

View File

@ -36,7 +36,10 @@ import haveno.network.Socks5ProxyProvider;
import haveno.network.p2p.P2PService; import haveno.network.p2p.P2PService;
import haveno.network.p2p.P2PServiceListener; import haveno.network.p2p.P2PServiceListener;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javafx.beans.property.IntegerProperty; import javafx.beans.property.IntegerProperty;
import javafx.beans.property.LongProperty; 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 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_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 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 lock = new Object();
private final Object pollLock = new Object(); private final Object pollLock = new Object();
@ -97,11 +98,21 @@ public final class XmrConnectionService {
private Boolean isConnected = false; private Boolean isConnected = false;
@Getter @Getter
private MoneroDaemonInfo lastInfo; private MoneroDaemonInfo lastInfo;
private Long lastLogPollErrorTimestamp;
private Long syncStartHeight = null; private Long syncStartHeight = null;
private TaskLooper daemonPollLooper; private TaskLooper daemonPollLooper;
@Getter
private boolean isShutDownStarted; private boolean isShutDownStarted;
private List<MoneroConnectionManagerListener> listeners = new ArrayList<>(); private List<MoneroConnectionManagerListener> 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<MoneroRpcConnection> excludedConnections = new HashSet<>();
@Inject @Inject
public XmrConnectionService(P2PService p2PService, public XmrConnectionService(P2PService p2PService,
Config config, Config config,
@ -200,12 +211,6 @@ public final class XmrConnectionService {
return connectionManager.getConnections(); return connectionManager.getConnections();
} }
public void switchToBestConnection() {
if (isFixedConnection() || !connectionManager.getAutoSwitch()) return;
MoneroRpcConnection bestConnection = getBestAvailableConnection();
if (bestConnection != null) setConnection(bestConnection);
}
public void setConnection(String connectionUri) { public void setConnection(String connectionUri) {
accountService.checkAccountOpen(); accountService.checkAccountOpen();
connectionManager.setConnection(connectionUri); // listener will update connection list connectionManager.setConnection(connectionUri); // listener will update connection list
@ -243,10 +248,86 @@ public final class XmrConnectionService {
public MoneroRpcConnection getBestAvailableConnection() { public MoneroRpcConnection getBestAvailableConnection() {
accountService.checkAccountOpen(); accountService.checkAccountOpen();
List<MoneroRpcConnection> ignoredConnections = new ArrayList<MoneroRpcConnection>(); List<MoneroRpcConnection> ignoredConnections = new ArrayList<MoneroRpcConnection>();
if (xmrLocalNode.shouldBeIgnored() && connectionManager.hasConnection(xmrLocalNode.getUri())) ignoredConnections.add(connectionManager.getConnectionByUri(xmrLocalNode.getUri())); addLocalNodeIfIgnored(ignoredConnections);
return connectionManager.getBestAvailableConnection(ignoredConnections.toArray(new MoneroRpcConnection[0])); return connectionManager.getBestAvailableConnection(ignoredConnections.toArray(new MoneroRpcConnection[0]));
} }
private MoneroRpcConnection getBestAvailableConnection(Collection<MoneroRpcConnection> ignoredConnections) {
accountService.checkAccountOpen();
Set<MoneroRpcConnection> ignoredConnectionsSet = new HashSet<>(ignoredConnections);
addLocalNodeIfIgnored(ignoredConnectionsSet);
return connectionManager.getBestAvailableConnection(ignoredConnectionsSet.toArray(new MoneroRpcConnection[0]));
}
private void addLocalNodeIfIgnored(Collection<MoneroRpcConnection> 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) { public void setAutoSwitch(boolean autoSwitch) {
accountService.checkAccountOpen(); accountService.checkAccountOpen();
connectionManager.setAutoSwitch(autoSwitch); connectionManager.setAutoSwitch(autoSwitch);
@ -504,7 +585,6 @@ public final class XmrConnectionService {
// register connection listener // register connection listener
connectionManager.addListener(this::onConnectionChanged); connectionManager.addListener(this::onConnectionChanged);
isInitialized = true; isInitialized = true;
} }
@ -620,8 +700,14 @@ public final class XmrConnectionService {
return; 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 // switch to best connection
log.warn("Failed to fetch daemon info, trying to switch to best connection: " + e.getMessage());
switchToBestConnection(); switchToBestConnection();
lastInfo = daemon.getInfo(); // caught internally if still fails lastInfo = daemon.getInfo(); // caught internally if still fails
} }
@ -662,9 +748,9 @@ public final class XmrConnectionService {
}); });
// handle error recovery // handle error recovery
if (lastErrorTimestamp != null) { if (lastLogPollErrorTimestamp != null) {
log.info("Successfully fetched daemon info after previous error"); log.info("Successfully fetched daemon info after previous error");
lastErrorTimestamp = null; lastLogPollErrorTimestamp = null;
} }
// clear error message // clear error message
@ -677,13 +763,6 @@ public final class XmrConnectionService {
// skip if shut down // skip if shut down
if (isShutDownStarted) return; 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 // set error message
getConnectionServiceErrorMsg().set(e.getMessage()); getConnectionServiceErrorMsg().set(e.getMessage());
} finally { } finally {

View File

@ -21,6 +21,7 @@ import haveno.common.Payload;
import haveno.core.api.model.builder.TradeInfoV1Builder; import haveno.core.api.model.builder.TradeInfoV1Builder;
import haveno.core.trade.Contract; import haveno.core.trade.Contract;
import haveno.core.trade.Trade; import haveno.core.trade.Trade;
import haveno.core.trade.TradeUtil;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
@ -142,10 +143,7 @@ public class TradeInfo implements Payload {
} }
public static TradeInfo toTradeInfo(Trade trade) { public static TradeInfo toTradeInfo(Trade trade) {
return toTradeInfo(trade, null); String role = TradeUtil.getRole(trade);
}
public static TradeInfo toTradeInfo(Trade trade, String role) {
ContractInfo contractInfo; ContractInfo contractInfo;
if (trade.getContract() != null) { if (trade.getContract() != null) {
Contract contract = trade.getContract(); Contract contract = trade.getContract();

View File

@ -113,7 +113,7 @@ import org.fxmisc.easybind.monadic.MonadicBinding;
public class HavenoSetup { public class HavenoSetup {
private static final String VERSION_FILE_NAME = "version"; 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 DomainInitialisation domainInitialisation;
private final P2PNetworkSetup p2PNetworkSetup; private final P2PNetworkSetup p2PNetworkSetup;

View File

@ -410,6 +410,10 @@ public class Offer implements NetworkPayload, PersistablePayload {
return getExtraDataMap().get(OfferPayload.PAY_BY_MAIL_EXTRA_INFO); return getExtraDataMap().get(OfferPayload.PAY_BY_MAIL_EXTRA_INFO);
else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.AUSTRALIA_PAYID_EXTRA_INFO)) else if (getExtraDataMap() != null && getExtraDataMap().containsKey(OfferPayload.AUSTRALIA_PAYID_EXTRA_INFO))
return getExtraDataMap().get(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 else
return ""; return "";
} }

View File

@ -94,12 +94,14 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
// Keys for extra map // Keys for extra map
// Only set for traditional offers // Only set for traditional offers
public static final String ACCOUNT_AGE_WITNESS_HASH = "accountAgeWitnessHash"; public static final String ACCOUNT_AGE_WITNESS_HASH = "accountAgeWitnessHash";
public static final String CASHAPP_EXTRA_INFO = "cashAppExtraInfo";
public static final String REFERRAL_ID = "referralId"; public static final String REFERRAL_ID = "referralId";
// Only used in payment method F2F // Only used in payment method F2F
public static final String F2F_CITY = "f2fCity"; public static final String F2F_CITY = "f2fCity";
public static final String F2F_EXTRA_INFO = "f2fExtraInfo"; public static final String F2F_EXTRA_INFO = "f2fExtraInfo";
public static final String PAY_BY_MAIL_EXTRA_INFO = "payByMailExtraInfo"; public static final String PAY_BY_MAIL_EXTRA_INFO = "payByMailExtraInfo";
public static final String AUSTRALIA_PAYID_EXTRA_INFO = "australiaPayidExtraInfo"; 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 // 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 // Capability.SIGNED_ACCOUNT_AGE_WITNESS is 11 and Capability.MEDIATION is 12 so if we want to signal that maker

View File

@ -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.ACCOUNT_AGE_WITNESS_HASH;
import static haveno.core.offer.OfferPayload.AUSTRALIA_PAYID_EXTRA_INFO; import static haveno.core.offer.OfferPayload.AUSTRALIA_PAYID_EXTRA_INFO;
import static haveno.core.offer.OfferPayload.CAPABILITIES; 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_CITY;
import static haveno.core.offer.OfferPayload.F2F_EXTRA_INFO; 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.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.REFERRAL_ID;
import static haveno.core.offer.OfferPayload.XMR_AUTO_CONF; import static haveno.core.offer.OfferPayload.XMR_AUTO_CONF;
import static haveno.core.offer.OfferPayload.XMR_AUTO_CONF_ENABLED_VALUE; import static haveno.core.offer.OfferPayload.XMR_AUTO_CONF_ENABLED_VALUE;
import haveno.core.payment.AustraliaPayidAccount; import haveno.core.payment.AustraliaPayidAccount;
import haveno.core.payment.CashAppAccount;
import haveno.core.payment.F2FAccount; import haveno.core.payment.F2FAccount;
import haveno.core.payment.PayByMailAccount; import haveno.core.payment.PayByMailAccount;
import haveno.core.payment.PayPalAccount;
import haveno.core.payment.PaymentAccount; import haveno.core.payment.PaymentAccount;
import haveno.core.provider.price.MarketPrice; import haveno.core.provider.price.MarketPrice;
import haveno.core.provider.price.PriceFeedService; import haveno.core.provider.price.PriceFeedService;
@ -200,6 +204,14 @@ public class OfferUtil {
extraDataMap.put(PAY_BY_MAIL_EXTRA_INFO, ((PayByMailAccount) paymentAccount).getExtraInfo()); 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) { if (paymentAccount instanceof AustraliaPayidAccount) {
extraDataMap.put(AUSTRALIA_PAYID_EXTRA_INFO, ((AustraliaPayidAccount) paymentAccount).getExtraInfo()); extraDataMap.put(AUSTRALIA_PAYID_EXTRA_INFO, ((AustraliaPayidAccount) paymentAccount).getExtraInfo());
} }

View File

@ -42,7 +42,6 @@ import javafx.beans.property.SimpleObjectProperty;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
@ -51,11 +50,10 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
@EqualsAndHashCode @EqualsAndHashCode
@Slf4j
public final class OpenOffer implements Tradable { public final class OpenOffer implements Tradable {
public enum State { public enum State {
SCHEDULED, PENDING,
AVAILABLE, AVAILABLE,
RESERVED, RESERVED,
CLOSED, CLOSED,
@ -122,7 +120,7 @@ public final class OpenOffer implements Tradable {
this.offer = offer; this.offer = offer;
this.triggerPrice = triggerPrice; this.triggerPrice = triggerPrice;
this.reserveExactAmount = reserveExactAmount; this.reserveExactAmount = reserveExactAmount;
state = State.SCHEDULED; state = State.PENDING;
} }
public OpenOffer(Offer offer, long triggerPrice, OpenOffer openOffer) { public OpenOffer(Offer offer, long triggerPrice, OpenOffer openOffer) {
@ -167,8 +165,8 @@ public final class OpenOffer implements Tradable {
this.reserveTxHex = reserveTxHex; this.reserveTxHex = reserveTxHex;
this.reserveTxKey = reserveTxKey; this.reserveTxKey = reserveTxKey;
if (this.state == State.RESERVED) // reset reserved state to available
setState(State.AVAILABLE); if (this.state == State.RESERVED) setState(State.AVAILABLE);
} }
@Override @Override
@ -234,8 +232,8 @@ public final class OpenOffer implements Tradable {
return stateProperty; return stateProperty;
} }
public boolean isScheduled() { public boolean isPending() {
return state == State.SCHEDULED; return state == State.PENDING;
} }
public boolean isAvailable() { public boolean isAvailable() {

View File

@ -96,6 +96,7 @@ import java.math.BigInteger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
@ -113,6 +114,7 @@ import monero.daemon.model.MoneroKeyImageSpentStatus;
import monero.daemon.model.MoneroTx; import monero.daemon.model.MoneroTx;
import monero.wallet.model.MoneroIncomingTransfer; import monero.wallet.model.MoneroIncomingTransfer;
import monero.wallet.model.MoneroOutputQuery; import monero.wallet.model.MoneroOutputQuery;
import monero.wallet.model.MoneroOutputWallet;
import monero.wallet.model.MoneroTransferQuery; import monero.wallet.model.MoneroTransferQuery;
import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroTxConfig;
import monero.wallet.model.MoneroTxQuery; 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_AGAIN_AT_STARTUP_DELAY_SEC = 30;
private static final long REPUBLISH_INTERVAL_MS = TimeUnit.MINUTES.toMillis(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 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 CoreContext coreContext;
private final KeyRing keyRing; private final KeyRing keyRing;
@ -250,17 +252,17 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// read open offers // read open offers
persistenceManager.readPersisted(persisted -> { persistenceManager.readPersisted(persisted -> {
openOffers.setAll(persisted.getList()); openOffers.setAll(persisted.getList());
openOffers.forEach(openOffer -> openOffer.getOffer().setPriceFeedService(priceFeedService)); openOffers.forEach(openOffer -> openOffer.getOffer().setPriceFeedService(priceFeedService));
// read signed offers // read signed offers
signedOfferPersistenceManager.readPersisted(signedOfferPersisted -> { signedOfferPersistenceManager.readPersisted(signedOfferPersisted -> {
signedOffers.setAll(signedOfferPersisted.getList()); signedOffers.setAll(signedOfferPersisted.getList());
completeHandler.run(); completeHandler.run();
}, },
completeHandler); completeHandler);
}, },
completeHandler); completeHandler);
} }
private synchronized void maybeInitializeKeyImagePoller() { private synchronized void maybeInitializeKeyImagePoller() {
@ -470,17 +472,19 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// .forEach(openOffer -> OfferUtil.getInvalidMakerFeeTxErrorMessage(openOffer.getOffer(), btcWalletService) // .forEach(openOffer -> OfferUtil.getInvalidMakerFeeTxErrorMessage(openOffer.getOffer(), btcWalletService)
// .ifPresent(errorMsg -> invalidOffers.add(new Tuple2<>(openOffer, errorMsg)))); // .ifPresent(errorMsg -> invalidOffers.add(new Tuple2<>(openOffer, errorMsg))));
// process scheduled offers // process pending offers
processScheduledOffers((transaction) -> {}, (errorMessage) -> { processPendingOffers(false, (transaction) -> {}, (errorMessage) -> {
log.warn("Error processing unposted offers: " + 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() { xmrWalletService.addWalletListener(new MoneroWalletListener() {
@Override @Override
public void onNewBlock(long height) { 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) { synchronized (processOffersLock) {
CountDownLatch latch = new CountDownLatch(1); CountDownLatch latch = new CountDownLatch(1);
addOpenOffer(openOffer); addOpenOffer(openOffer);
processUnpostedOffer(getOpenOffers(), openOffer, (transaction) -> { processPendingOffer(getOpenOffers(), openOffer, (transaction) -> {
requestPersistence(); requestPersistence();
latch.countDown(); latch.countDown();
resultHandler.handleResult(transaction); resultHandler.handleResult(transaction);
}, (errorMessage) -> { }, (errorMessage) -> {
if (openOffer.isCanceled()) latch.countDown(); if (!openOffer.isCanceled()) {
else { log.warn("Error processing pending offer {}: {}", openOffer.getId(), errorMessage);
log.warn("Error processing unposted offer {}: {}", openOffer.getId(), errorMessage);
doCancelOffer(openOffer); doCancelOffer(openOffer);
offer.setErrorMessage(errorMessage);
latch.countDown();
errorMessageHandler.handleErrorMessage(errorMessage);
} }
latch.countDown();
errorMessageHandler.handleErrorMessage(errorMessage);
}); });
HavenoUtils.awaitLatch(latch); HavenoUtils.awaitLatch(latch);
} }
@ -581,9 +583,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
public void activateOpenOffer(OpenOffer openOffer, public void activateOpenOffer(OpenOffer openOffer,
ResultHandler resultHandler, ResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) { ErrorMessageHandler errorMessageHandler) {
if (openOffer.isScheduled()) { if (openOffer.isPending()) {
resultHandler.handleResult(); // ignore if scheduled resultHandler.handleResult(); // ignore if pending
} else if (!offersToBeEdited.containsKey(openOffer.getId())) { } else if (offersToBeEdited.containsKey(openOffer.getId())) {
errorMessageHandler.handleErrorMessage("You can't activate an offer that is currently edited.");
} else {
Offer offer = openOffer.getOffer(); Offer offer = openOffer.getOffer();
offerBookService.activateOffer(offer, offerBookService.activateOffer(offer,
() -> { () -> {
@ -593,8 +597,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
resultHandler.handleResult(); resultHandler.handleResult();
}, },
errorMessageHandler); 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()); log.info("Canceling open offer: {}", openOffer.getId());
if (!offersToBeEdited.containsKey(openOffer.getId())) { if (!offersToBeEdited.containsKey(openOffer.getId())) {
if (openOffer.isAvailable()) { if (openOffer.isAvailable()) {
openOffer.setState(OpenOffer.State.CANCELED);
offerBookService.removeOffer(openOffer.getOffer().getOfferPayload(), offerBookService.removeOffer(openOffer.getOffer().getOfferPayload(),
() -> { () -> {
ThreadUtils.submitToPool(() -> { // TODO: this runs off thread and then shows popup when done. should show overlay spinner until done 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); errorMessageHandler);
} else { } else {
openOffer.setState(OpenOffer.State.CANCELED);
ThreadUtils.submitToPool(() -> { ThreadUtils.submitToPool(() -> {
doCancelOffer(openOffer); doCancelOffer(openOffer);
if (resultHandler != null) resultHandler.handleResult(); 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 // Place offer helpers
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
private void processPendingOffers(boolean skipOffersWithTooManyAttempts,
private void processScheduledOffers(TransactionResultHandler resultHandler, // TODO (woodser): transaction not needed with result handler TransactionResultHandler resultHandler, // TODO (woodser): transaction not needed with result handler
ErrorMessageHandler errorMessageHandler) { ErrorMessageHandler errorMessageHandler) {
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
synchronized (processOffersLock) { synchronized (processOffersLock) {
List<String> errorMessages = new ArrayList<String>(); List<String> errorMessages = new ArrayList<String>();
List<OpenOffer> openOffers = getOpenOffers(); List<OpenOffer> openOffers = getOpenOffers();
for (OpenOffer scheduledOffer : openOffers) { for (OpenOffer pendingOffer : openOffers) {
if (scheduledOffer.getState() != OpenOffer.State.SCHEDULED) continue; 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); CountDownLatch latch = new CountDownLatch(1);
processUnpostedOffer(openOffers, scheduledOffer, (transaction) -> { processPendingOffer(openOffers, pendingOffer, (transaction) -> {
latch.countDown(); latch.countDown();
}, errorMessage -> { }, errorMessage -> {
if (!scheduledOffer.isCanceled()) { if (!pendingOffer.isCanceled()) {
log.warn("Error processing unposted offer, offerId={}, attempt={}/{}, error={}", scheduledOffer.getId(), scheduledOffer.getNumProcessingAttempts(), MAX_PROCESS_ATTEMPTS, errorMessage); String warnMessage = "Error processing pending offer, offerId=" + pendingOffer.getId() + ", attempt=" + pendingOffer.getNumProcessingAttempts() + ": " + errorMessage;
if (scheduledOffer.getNumProcessingAttempts() >= MAX_PROCESS_ATTEMPTS) { errorMessages.add(warnMessage);
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); // cancel offer if invalid
doCancelOffer(scheduledOffer); if (pendingOffer.getOffer().getState() == Offer.State.INVALID) {
log.warn("Canceling offer because it's invalid: {}", pendingOffer.getId());
doCancelOffer(pendingOffer);
} }
errorMessages.add(errorMessage);
} }
latch.countDown(); latch.countDown();
}); });
HavenoUtils.awaitLatch(latch); HavenoUtils.awaitLatch(latch);
} }
requestPersistence(); requestPersistence();
if (errorMessages.size() > 0) errorMessageHandler.handleErrorMessage(errorMessages.toString()); if (errorMessages.isEmpty()) {
else resultHandler.handleResult(null); if (resultHandler != null) resultHandler.handleResult(null);
} else {
if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage(errorMessages.toString());
}
} }
}, THREAD_ID); }, THREAD_ID);
} }
private void processUnpostedOffer(List<OpenOffer> openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { private void processPendingOffer(List<OpenOffer> openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
// skip if already processing // skip if already processing
if (openOffer.isProcessing()) { if (openOffer.isProcessing()) {
@ -902,17 +907,18 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// process offer // process offer
openOffer.setProcessing(true); openOffer.setProcessing(true);
doProcessUnpostedOffer(openOffers, openOffer, (transaction) -> { doProcessPendingOffer(openOffers, openOffer, (transaction) -> {
openOffer.setProcessing(false); openOffer.setProcessing(false);
resultHandler.handleResult(transaction); resultHandler.handleResult(transaction);
}, (errorMsg) -> { }, (errorMsg) -> {
openOffer.setProcessing(false); openOffer.setProcessing(false);
openOffer.setNumProcessingAttempts(openOffer.getNumProcessingAttempts() + 1); openOffer.setNumProcessingAttempts(openOffer.getNumProcessingAttempts() + 1);
openOffer.getOffer().setErrorMessage(errorMsg);
errorMessageHandler.handleErrorMessage(errorMsg); errorMessageHandler.handleErrorMessage(errorMsg);
}); });
} }
private void doProcessUnpostedOffer(List<OpenOffer> openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { private void doProcessPendingOffer(List<OpenOffer> openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
new Thread(() -> { new Thread(() -> {
try { try {
@ -929,15 +935,25 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
if (openOffer.isReserveExactAmount()) { if (openOffer.isReserveExactAmount()) {
// find tx with exact input amount // find tx with exact input amount
MoneroTxWallet splitOutputTx = findSplitOutputFundingTx(openOffers, openOffer); MoneroTxWallet splitOutputTx = getSplitOutputFundingTx(openOffers, openOffer);
if (splitOutputTx != null && openOffer.getSplitOutputTxHash() == null) { if (splitOutputTx != null && openOffer.getSplitOutputTxHash() == null) {
setSplitOutputTx(openOffer, splitOutputTx); setSplitOutputTx(openOffer, splitOutputTx);
} }
// if not found, create tx to split exact output // if not found, create tx to split exact output
if (splitOutputTx == null) { if (splitOutputTx == null) {
if (openOffer.getSplitOutputTxHash() != null) log.warn("Split output tx not found for offer {}", openOffer.getId()); if (openOffer.getSplitOutputTxHash() != null) {
splitOrSchedule(openOffers, openOffer, amountNeeded); 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()) { } else if (!splitOutputTx.isLocked()) {
// otherwise sign and post offer if split output available // otherwise sign and post offer if split output available
@ -965,89 +981,87 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}).start(); }).start();
} }
private MoneroTxWallet findSplitOutputFundingTx(List<OpenOffer> openOffers, OpenOffer openOffer) { private MoneroTxWallet getSplitOutputFundingTx(List<OpenOffer> openOffers, OpenOffer openOffer) {
XmrAddressEntry addressEntry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); 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<OpenOffer> openOffers, OpenOffer openOffer, BigInteger reserveAmount, Integer preferredSubaddressIndex) { private MoneroTxWallet getSplitOutputFundingTx(List<OpenOffer> openOffers, OpenOffer openOffer, BigInteger reserveAmount, Integer preferredSubaddressIndex) {
List<MoneroTxWallet> fundingTxs = new ArrayList<>();
MoneroTxWallet earliestUnscheduledTx = null;
// return split output tx if already assigned // return split output tx if already assigned
if (openOffer != null && openOffer.getSplitOutputTxHash() != null) { if (openOffer != null && openOffer.getSplitOutputTxHash() != null) {
return xmrWalletService.getTx(openOffer.getSplitOutputTxHash());
}
// return earliest tx with exact amount to offer's subaddress if available // get recorded split output tx
if (preferredSubaddressIndex != null) { MoneroTxWallet splitOutputTx = xmrWalletService.getTx(openOffer.getSplitOutputTxHash());
// get txs with exact output amount // check if split output tx is available for offer
fundingTxs = xmrWalletService.getTxs(new MoneroTxQuery() if (splitOutputTx.isLocked()) return splitOutputTx;
.setIsConfirmed(true) else {
.setOutputQuery(new MoneroOutputQuery() boolean isAvailable = true;
.setAccountIndex(0) for (MoneroOutputWallet output : splitOutputTx.getOutputsWallet()) {
.setSubaddressIndex(preferredSubaddressIndex) if (output.isSpent() || output.isFrozen()) {
.setAmount(reserveAmount) isAvailable = false;
.setIsSpent(false) break;
.setIsFrozen(false))); }
}
// return earliest tx if available if (isAvailable || isReservedByOffer(openOffer, splitOutputTx)) return splitOutputTx;
earliestUnscheduledTx = getEarliestUnscheduledTx(openOffers, fundingTxs); else log.warn("Split output tx is no longer available for offer {}", openOffer.getId());
if (earliestUnscheduledTx != null) return earliestUnscheduledTx;
}
// return if awaiting scheduled tx
if (openOffer.getScheduledTxHashes() != null) return null;
// get all transactions including from pool
List<MoneroTxWallet> 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);
} }
earliestUnscheduledTx = getEarliestUnscheduledTx(openOffers, fundingTxs); }
// get split output tx to offer's preferred subaddress
if (preferredSubaddressIndex != null) {
List<MoneroTxWallet> fundingTxs = getSplitOutputFundingTxs(reserveAmount, preferredSubaddressIndex);
MoneroTxWallet earliestUnscheduledTx = getEarliestUnscheduledTx(openOffers, openOffer, fundingTxs);
if (earliestUnscheduledTx != null) return earliestUnscheduledTx; if (earliestUnscheduledTx != null) return earliestUnscheduledTx;
} }
// return earliest tx with exact confirmed output to any subaddress if available // get split output tx to any subaddress
fundingTxs.clear(); List<MoneroTxWallet> fundingTxs = getSplitOutputFundingTxs(reserveAmount, null);
for (MoneroTxWallet tx : allTxs) { return getEarliestUnscheduledTx(openOffers, openOffer, fundingTxs);
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);
} }
private MoneroTxWallet getEarliestUnscheduledTx(List<OpenOffer> openOffers, List<MoneroTxWallet> txs) { private boolean isReservedByOffer(OpenOffer openOffer, MoneroTxWallet tx) {
if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() == null) return false;
Set<String> offerKeyImages = new HashSet<String>(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages());
for (MoneroOutputWallet output : tx.getOutputsWallet()) {
if (offerKeyImages.contains(output.getKeyImage().getHex())) return true;
}
return false;
}
private List<MoneroTxWallet> getSplitOutputFundingTxs(BigInteger reserveAmount, Integer preferredSubaddressIndex) {
List<MoneroTxWallet> splitOutputTxs = xmrWalletService.getTxs(new MoneroTxQuery().setIsIncoming(true).setIsFailed(false));
Set<MoneroTxWallet> removeTxs = new HashSet<MoneroTxWallet>();
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<OpenOffer> openOffers, OpenOffer excludeOpenOffer, List<MoneroTxWallet> txs) {
MoneroTxWallet earliestUnscheduledTx = null; MoneroTxWallet earliestUnscheduledTx = null;
for (MoneroTxWallet tx : txs) { 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; if (earliestUnscheduledTx == null || (earliestUnscheduledTx.getNumConfirmations() < tx.getNumConfirmations())) earliestUnscheduledTx = tx;
} }
return earliestUnscheduledTx; return earliestUnscheduledTx;
@ -1084,8 +1098,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
.setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY)); .setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY));
break; break;
} catch (Exception e) { } 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 (stopped || i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
if (xmrConnectionService.isConnected()) xmrWalletService.requestSwitchToNextBestConnection();
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying 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) { private void setSplitOutputTx(OpenOffer openOffer, MoneroTxWallet splitOutputTx) {
openOffer.setSplitOutputTxHash(splitOutputTx.getHash()); openOffer.setSplitOutputTxHash(splitOutputTx == null ? null : splitOutputTx.getHash());
openOffer.setSplitOutputTxFee(splitOutputTx.getFee().longValueExact()); openOffer.setSplitOutputTxFee(splitOutputTx == null ? 0l : splitOutputTx.getFee().longValueExact());
openOffer.setScheduledTxHashes(Arrays.asList(splitOutputTx.getHash())); openOffer.setScheduledTxHashes(splitOutputTx == null ? null : Arrays.asList(splitOutputTx.getHash()));
openOffer.setScheduledAmount(openOffer.getOffer().getAmountNeeded().toString()); openOffer.setScheduledAmount(splitOutputTx == null ? null : openOffer.getOffer().getAmountNeeded().toString());
openOffer.setState(OpenOffer.State.SCHEDULED); if (!openOffer.isCanceled()) openOffer.setState(OpenOffer.State.PENDING);
} }
private void scheduleWithEarliestTxs(List<OpenOffer> openOffers, OpenOffer openOffer) { private void scheduleWithEarliestTxs(List<OpenOffer> openOffers, OpenOffer openOffer) {
@ -1121,7 +1137,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
List<String> scheduledTxHashes = new ArrayList<String>(); List<String> scheduledTxHashes = new ArrayList<String>();
BigInteger scheduledAmount = BigInteger.ZERO; BigInteger scheduledAmount = BigInteger.ZERO;
for (MoneroTxWallet lockedTx : lockedTxs) { 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; if (lockedTx.getIncomingTransfers() == null || lockedTx.getIncomingTransfers().isEmpty()) continue;
scheduledTxHashes.add(lockedTx.getHash()); scheduledTxHashes.add(lockedTx.getHash());
for (MoneroIncomingTransfer transfer : lockedTx.getIncomingTransfers()) { for (MoneroIncomingTransfer transfer : lockedTx.getIncomingTransfers()) {
@ -1134,13 +1150,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// schedule txs // schedule txs
openOffer.setScheduledTxHashes(scheduledTxHashes); openOffer.setScheduledTxHashes(scheduledTxHashes);
openOffer.setScheduledAmount(scheduledAmount.toString()); openOffer.setScheduledAmount(scheduledAmount.toString());
openOffer.setState(OpenOffer.State.SCHEDULED); openOffer.setState(OpenOffer.State.PENDING);
} }
private BigInteger getScheduledAmount(List<OpenOffer> openOffers) { private BigInteger getScheduledAmount(List<OpenOffer> openOffers) {
BigInteger scheduledAmount = BigInteger.ZERO; BigInteger scheduledAmount = BigInteger.ZERO;
for (OpenOffer openOffer : openOffers) { for (OpenOffer openOffer : openOffers) {
if (openOffer.getState() != OpenOffer.State.SCHEDULED) continue; if (openOffer.getState() != OpenOffer.State.PENDING) continue;
if (openOffer.getScheduledTxHashes() == null) continue; if (openOffer.getScheduledTxHashes() == null) continue;
List<MoneroTxWallet> fundingTxs = xmrWalletService.getTxs(openOffer.getScheduledTxHashes()); List<MoneroTxWallet> fundingTxs = xmrWalletService.getTxs(openOffer.getScheduledTxHashes());
for (MoneroTxWallet fundingTx : fundingTxs) { for (MoneroTxWallet fundingTx : fundingTxs) {
@ -1154,12 +1170,15 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
return scheduledAmount; return scheduledAmount;
} }
private boolean isTxScheduled(List<OpenOffer> openOffers, String txHash) { private boolean isTxScheduledByOtherOffer(List<OpenOffer> openOffers, OpenOffer openOffer, String txHash) {
for (OpenOffer openOffer : openOffers) { for (OpenOffer otherOffer : openOffers) {
if (openOffer.getState() != OpenOffer.State.SCHEDULED) continue; if (otherOffer == openOffer) continue;
if (openOffer.getScheduledTxHashes() == null) continue; if (otherOffer.getState() != OpenOffer.State.PENDING) continue;
for (String scheduledTxHash : openOffer.getScheduledTxHashes()) { if (txHash.equals(otherOffer.getSplitOutputTxHash())) return true;
if (txHash.equals(scheduledTxHash)) return true; if (otherOffer.getScheduledTxHashes() != null) {
for (String scheduledTxHash : otherOffer.getScheduledTxHashes()) {
if (txHash.equals(scheduledTxHash)) return true;
}
} }
} }
return false; return false;
@ -1721,7 +1740,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// determine if offer is valid // determine if offer is valid
boolean isValid = true; boolean isValid = true;
Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(openOffer.getOffer().getOfferPayload().getArbitratorSigner()); 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()); log.warn("Offer {} has invalid arbitrator signature, reposting", openOffer.getId());
isValid = false; isValid = false;
} }
@ -1756,25 +1778,29 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}); });
} else { } else {
// cancel and recreate offer // reset offer state to pending
doCancelOffer(openOffer); openOffer.getOffer().getOfferPayload().setArbitratorSignature(null);
Offer updatedOffer = new Offer(openOffer.getOffer().getOfferPayload()); openOffer.getOffer().getOfferPayload().setArbitratorSigner(null);
updatedOffer.setPriceFeedService(priceFeedService); openOffer.getOffer().setState(Offer.State.UNKNOWN);
OpenOffer updatedOpenOffer = new OpenOffer(updatedOffer, openOffer.getTriggerPrice()); openOffer.setState(OpenOffer.State.PENDING);
// repost offer // republish offer
synchronized (processOffersLock) { synchronized (processOffersLock) {
CountDownLatch latch = new CountDownLatch(1); CountDownLatch latch = new CountDownLatch(1);
addOpenOffer(updatedOpenOffer); processPendingOffer(getOpenOffers(), openOffer, (transaction) -> {
processUnpostedOffer(getOpenOffers(), updatedOpenOffer, (transaction) -> {
requestPersistence(); requestPersistence();
latch.countDown(); latch.countDown();
if (completeHandler != null) completeHandler.run(); if (completeHandler != null) completeHandler.run();
}, (errorMessage) -> { }, (errorMessage) -> {
if (!updatedOpenOffer.isCanceled()) { if (!openOffer.isCanceled()) {
log.warn("Error reposting offer {}: {}", updatedOpenOffer.getId(), errorMessage); log.warn("Error republishing offer {}: {}", openOffer.getId(), errorMessage);
doCancelOffer(updatedOpenOffer); openOffer.getOffer().setErrorMessage(errorMessage);
updatedOffer.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(); latch.countDown();
if (completeHandler != null) completeHandler.run(); if (completeHandler != null) completeHandler.run();

View File

@ -159,7 +159,6 @@ public class PlaceOfferProtocol {
if (timeoutTimer != null) { if (timeoutTimer != null) {
taskRunner.cancel(); taskRunner.cancel();
if (!model.getOpenOffer().isCanceled()) { if (!model.getOpenOffer().isCanceled()) {
log.error(errorMessage);
model.getOpenOffer().getOffer().setErrorMessage(errorMessage); model.getOpenOffer().getOffer().setErrorMessage(errorMessage);
} }
stopTimeoutTimer(); stopTimeoutTimer();

View File

@ -66,7 +66,7 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
synchronized (XmrWalletService.WALLET_LOCK) { synchronized (XmrWalletService.WALLET_LOCK) {
// reset protocol timeout // reset protocol timeout
verifyScheduled(); verifyPending();
model.getProtocol().startTimeoutTimer(); model.getProtocol().startTimeoutTimer();
// collect relevant info // collect relevant info
@ -86,14 +86,15 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
//if (true) throw new RuntimeException("Pretend error"); //if (true) throw new RuntimeException("Pretend error");
reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex); reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex);
} catch (Exception e) { } 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; if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
model.getProtocol().startTimeoutTimer(); // reset protocol timeout model.getProtocol().startTimeoutTimer(); // reset protocol timeout
if (model.getXmrWalletService().getConnectionService().isConnected()) model.getXmrWalletService().requestSwitchToNextBestConnection();
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} }
// verify still open // verify still open
verifyScheduled(); verifyPending();
if (reserveTx != null) break; if (reserveTx != null) break;
} }
} }
@ -103,6 +104,7 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
model.getXmrWalletService().resetAddressEntriesForOpenOffer(offer.getId()); model.getXmrWalletService().resetAddressEntriesForOpenOffer(offer.getId());
if (reserveTx != null) { if (reserveTx != null) {
model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx)); model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx));
offer.getOfferPayload().setReserveTxKeyImages(null);
} }
throw e; throw e;
@ -130,7 +132,7 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
} }
} }
public void verifyScheduled() { public void verifyPending() {
if (!model.getOpenOffer().isScheduled()) throw new RuntimeException("Offer " + model.getOpenOffer().getOffer().getId() + " is canceled"); if (!model.getOpenOffer().isPending()) throw new RuntimeException("Offer " + model.getOpenOffer().getOffer().getId() + " is canceled");
} }
} }

View File

@ -116,9 +116,10 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
model.getOpenOffer().getOffer().getOfferPayload().setArbitratorSigner(arbitratorNodeAddress); model.getOpenOffer().getOffer().getOfferPayload().setArbitratorSigner(arbitratorNodeAddress);
model.getOpenOffer().getOffer().setState(Offer.State.OFFER_FEE_RESERVED); model.getOpenOffer().getOffer().setState(Offer.State.OFFER_FEE_RESERVED);
resultHandler.handleResult(); resultHandler.handleResult();
} else { } else {
errorMessageHandler.handleErrorMessage("Arbitrator nacked SignOfferRequest for offer " + request.getOfferId() + ": " + ackMessage.getErrorMessage()); model.getOpenOffer().getOffer().setState(Offer.State.INVALID);
} errorMessageHandler.handleErrorMessage("Arbitrator nacked SignOfferRequest for offer " + request.getOfferId() + ": " + ackMessage.getErrorMessage());
}
} }
}; };
model.getP2PService().addDecryptedDirectMessageListener(ackListener); model.getP2PService().addDecryptedDirectMessageListener(ackListener);
@ -137,9 +138,9 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
log.warn("Arbitrator unavailable: address={}, error={}", arbitratorNodeAddress, errorMessage); log.warn("Arbitrator unavailable: address={}, error={}", arbitratorNodeAddress, errorMessage);
excludedArbitrators.add(arbitratorNodeAddress); excludedArbitrators.add(arbitratorNodeAddress);
// check if offer still scheduled // check if offer still pending
if (!model.getOpenOffer().isScheduled()) { if (!model.getOpenOffer().isPending()) {
errorMessageHandler.handleErrorMessage("Offer is no longer scheduled, offerId=" + model.getOpenOffer().getId()); errorMessageHandler.handleErrorMessage("Offer is no longer pending, offerId=" + model.getOpenOffer().getId());
return; return;
} }

View File

@ -39,6 +39,7 @@ public final class CashAppAccount extends PaymentAccount {
PaymentAccountFormField.FieldId.EMAIL_OR_MOBILE_NR_OR_CASHTAG, PaymentAccountFormField.FieldId.EMAIL_OR_MOBILE_NR_OR_CASHTAG,
PaymentAccountFormField.FieldId.TRADE_CURRENCIES, PaymentAccountFormField.FieldId.TRADE_CURRENCIES,
PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.ACCOUNT_NAME,
PaymentAccountFormField.FieldId.EXTRA_INFO,
PaymentAccountFormField.FieldId.SALT); PaymentAccountFormField.FieldId.SALT);
public CashAppAccount() { public CashAppAccount() {
@ -67,4 +68,12 @@ public final class CashAppAccount extends PaymentAccount {
public String getEmailOrMobileNrOrCashtag() { public String getEmailOrMobileNrOrCashtag() {
return ((CashAppAccountPayload) paymentAccountPayload).getEmailOrMobileNrOrCashtag(); return ((CashAppAccountPayload) paymentAccountPayload).getEmailOrMobileNrOrCashtag();
} }
public void setExtraInfo(String extraInfo) {
((CashAppAccountPayload) paymentAccountPayload).setExtraInfo(extraInfo);
}
public String getExtraInfo() {
return ((CashAppAccountPayload) paymentAccountPayload).getExtraInfo();
}
} }

View File

@ -62,6 +62,7 @@ public final class PayPalAccount extends PaymentAccount {
PaymentAccountFormField.FieldId.EMAIL_OR_MOBILE_NR_OR_USERNAME, PaymentAccountFormField.FieldId.EMAIL_OR_MOBILE_NR_OR_USERNAME,
PaymentAccountFormField.FieldId.TRADE_CURRENCIES, PaymentAccountFormField.FieldId.TRADE_CURRENCIES,
PaymentAccountFormField.FieldId.ACCOUNT_NAME, PaymentAccountFormField.FieldId.ACCOUNT_NAME,
PaymentAccountFormField.FieldId.EXTRA_INFO,
PaymentAccountFormField.FieldId.SALT); PaymentAccountFormField.FieldId.SALT);
public PayPalAccount() { public PayPalAccount() {
@ -91,4 +92,12 @@ public final class PayPalAccount extends PaymentAccount {
public String getEmailOrMobileNrOrUsername() { public String getEmailOrMobileNrOrUsername() {
return ((PayPalAccountPayload) paymentAccountPayload).getEmailOrMobileNrOrUsername(); return ((PayPalAccountPayload) paymentAccountPayload).getEmailOrMobileNrOrUsername();
} }
public void setExtraInfo(String extraInfo) {
((PayPalAccountPayload) paymentAccountPayload).setExtraInfo(extraInfo);
}
public String getExtraInfo() {
return ((PayPalAccountPayload) paymentAccountPayload).getExtraInfo();
}
} }

View File

@ -36,6 +36,7 @@ import java.util.Map;
@Slf4j @Slf4j
public final class CashAppAccountPayload extends PaymentAccountPayload { public final class CashAppAccountPayload extends PaymentAccountPayload {
private String emailOrMobileNrOrCashtag = ""; private String emailOrMobileNrOrCashtag = "";
private String extraInfo = "";
public CashAppAccountPayload(String paymentMethod, String id) { public CashAppAccountPayload(String paymentMethod, String id) {
super(paymentMethod, id); super(paymentMethod, id);
@ -48,6 +49,7 @@ public final class CashAppAccountPayload extends PaymentAccountPayload {
private CashAppAccountPayload(String paymentMethod, private CashAppAccountPayload(String paymentMethod,
String id, String id,
String emailOrMobileNrOrCashtag, String emailOrMobileNrOrCashtag,
String extraInfo,
long maxTradePeriod, long maxTradePeriod,
Map<String, String> excludeFromJsonDataMap) { Map<String, String> excludeFromJsonDataMap) {
super(paymentMethod, super(paymentMethod,
@ -56,13 +58,15 @@ public final class CashAppAccountPayload extends PaymentAccountPayload {
excludeFromJsonDataMap); excludeFromJsonDataMap);
this.emailOrMobileNrOrCashtag = emailOrMobileNrOrCashtag; this.emailOrMobileNrOrCashtag = emailOrMobileNrOrCashtag;
this.extraInfo = extraInfo;
} }
@Override @Override
public Message toProtoMessage() { public Message toProtoMessage() {
return getPaymentAccountPayloadBuilder() return getPaymentAccountPayloadBuilder()
.setCashAppAccountPayload(protobuf.CashAppAccountPayload.newBuilder() .setCashAppAccountPayload(protobuf.CashAppAccountPayload.newBuilder()
.setEmailOrMobileNrOrCashtag(emailOrMobileNrOrCashtag)) .setExtraInfo(extraInfo)
.setEmailOrMobileNrOrCashtag(emailOrMobileNrOrCashtag))
.build(); .build();
} }
@ -70,6 +74,7 @@ public final class CashAppAccountPayload extends PaymentAccountPayload {
return new CashAppAccountPayload(proto.getPaymentMethodId(), return new CashAppAccountPayload(proto.getPaymentMethodId(),
proto.getId(), proto.getId(),
proto.getCashAppAccountPayload().getEmailOrMobileNrOrCashtag(), proto.getCashAppAccountPayload().getEmailOrMobileNrOrCashtag(),
proto.getCashAppAccountPayload().getExtraInfo(),
proto.getMaxTradePeriod(), proto.getMaxTradePeriod(),
new HashMap<>(proto.getExcludeFromJsonDataMap())); new HashMap<>(proto.getExcludeFromJsonDataMap()));
} }
@ -81,7 +86,10 @@ public final class CashAppAccountPayload extends PaymentAccountPayload {
@Override @Override
public String getPaymentDetails() { 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 @Override

View File

@ -36,6 +36,7 @@ import java.util.Map;
@Slf4j @Slf4j
public final class PayPalAccountPayload extends PaymentAccountPayload { public final class PayPalAccountPayload extends PaymentAccountPayload {
private String emailOrMobileNrOrUsername = ""; private String emailOrMobileNrOrUsername = "";
private String extraInfo = "";
public PayPalAccountPayload(String paymentMethod, String id) { public PayPalAccountPayload(String paymentMethod, String id) {
super(paymentMethod, id); super(paymentMethod, id);
@ -48,6 +49,7 @@ public final class PayPalAccountPayload extends PaymentAccountPayload {
private PayPalAccountPayload(String paymentMethod, private PayPalAccountPayload(String paymentMethod,
String id, String id,
String emailOrMobileNrOrUsername, String emailOrMobileNrOrUsername,
String extraInfo,
long maxTradePeriod, long maxTradePeriod,
Map<String, String> excludeFromJsonDataMap) { Map<String, String> excludeFromJsonDataMap) {
super(paymentMethod, super(paymentMethod,
@ -56,13 +58,15 @@ public final class PayPalAccountPayload extends PaymentAccountPayload {
excludeFromJsonDataMap); excludeFromJsonDataMap);
this.emailOrMobileNrOrUsername = emailOrMobileNrOrUsername; this.emailOrMobileNrOrUsername = emailOrMobileNrOrUsername;
this.extraInfo = extraInfo;
} }
@Override @Override
public Message toProtoMessage() { public Message toProtoMessage() {
return getPaymentAccountPayloadBuilder() return getPaymentAccountPayloadBuilder()
.setPaypalAccountPayload(protobuf.PayPalAccountPayload.newBuilder() .setPaypalAccountPayload(protobuf.PayPalAccountPayload.newBuilder()
.setEmailOrMobileNrOrUsername(emailOrMobileNrOrUsername)) .setExtraInfo(extraInfo)
.setEmailOrMobileNrOrUsername(emailOrMobileNrOrUsername))
.build(); .build();
} }
@ -70,6 +74,7 @@ public final class PayPalAccountPayload extends PaymentAccountPayload {
return new PayPalAccountPayload(proto.getPaymentMethodId(), return new PayPalAccountPayload(proto.getPaymentMethodId(),
proto.getId(), proto.getId(),
proto.getPaypalAccountPayload().getEmailOrMobileNrOrUsername(), proto.getPaypalAccountPayload().getEmailOrMobileNrOrUsername(),
proto.getPaypalAccountPayload().getExtraInfo(),
proto.getMaxTradePeriod(), proto.getMaxTradePeriod(),
new HashMap<>(proto.getExcludeFromJsonDataMap())); new HashMap<>(proto.getExcludeFromJsonDataMap()));
} }
@ -80,8 +85,8 @@ public final class PayPalAccountPayload extends PaymentAccountPayload {
@Override @Override
public String getPaymentDetails() { public String getPaymentDetails() {
return Res.getWithCol("payment.email.mobile.username") + " " return Res.getWithCol("payment.email.mobile.username") + " "+ emailOrMobileNrOrUsername + "\n" +
+ emailOrMobileNrOrUsername; Res.getWithCol("payment.shared.extraInfo") + " " + extraInfo+ "\n";
} }
@Override @Override

View File

@ -33,7 +33,6 @@ import haveno.core.monetary.Price;
import haveno.core.monetary.TraditionalMoney; import haveno.core.monetary.TraditionalMoney;
import haveno.core.provider.PriceHttpClient; import haveno.core.provider.PriceHttpClient;
import haveno.core.provider.ProvidersRepository; import haveno.core.provider.ProvidersRepository;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.statistics.TradeStatistics3; import haveno.core.trade.statistics.TradeStatistics3;
import haveno.core.user.Preferences; import haveno.core.user.Preferences;
import haveno.network.http.HttpClient; import haveno.network.http.HttpClient;
@ -144,15 +143,17 @@ public class PriceFeedService {
public void awaitExternalPrices() { public void awaitExternalPrices() {
CountDownLatch latch = new CountDownLatch(1); CountDownLatch latch = new CountDownLatch(1);
ChangeListener<? super Number> listener = (observable, oldValue, newValue) -> { ChangeListener<? super Number> listener = (observable, oldValue, newValue) -> {
if (hasExternalPrices() && latch.getCount() != 0) latch.countDown(); if (hasExternalPrices()) UserThread.execute(() -> latch.countDown());
}; };
updateCounter.addListener(listener); UserThread.execute(() -> updateCounter.addListener(listener));
if (hasExternalPrices()) { if (hasExternalPrices()) UserThread.execute(() -> latch.countDown());
updateCounter.removeListener(listener); try {
return; latch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
UserThread.execute(() -> updateCounter.removeListener(listener));
} }
HavenoUtils.awaitLatch(latch);
updateCounter.removeListener(listener);
} }
public boolean hasExternalPrices() { public boolean hasExternalPrices() {
@ -377,17 +378,21 @@ public class PriceFeedService {
*/ */
public synchronized Map<String, MarketPrice> requestAllPrices() throws ExecutionException, InterruptedException, TimeoutException, CancellationException { public synchronized Map<String, MarketPrice> requestAllPrices() throws ExecutionException, InterruptedException, TimeoutException, CancellationException {
CountDownLatch latch = new CountDownLatch(1); CountDownLatch latch = new CountDownLatch(1);
ChangeListener<? super Number> listener = (observable, oldValue, newValue) -> { if (latch.getCount() != 0) latch.countDown(); }; ChangeListener<? super Number> listener = (observable, oldValue, newValue) -> latch.countDown();
updateCounter.addListener(listener); UserThread.execute(() -> updateCounter.addListener(listener));
requestAllPricesError = null; requestAllPricesError = null;
requestPrices(); requestPrices();
UserThread.runAfter(() -> { UserThread.runAfter(() -> {
if (latch.getCount() == 0) return; if (latch.getCount() > 0) requestAllPricesError = "Timeout fetching market prices within 20 seconds";
requestAllPricesError = "Timeout fetching market prices within 20 seconds"; UserThread.execute(() -> latch.countDown());
latch.countDown();
}, 20); }, 20);
HavenoUtils.awaitLatch(latch); try {
updateCounter.removeListener(listener); latch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
UserThread.execute(() -> updateCounter.removeListener(listener));
}
if (requestAllPricesError != null) throw new RuntimeException(requestAllPricesError); if (requestAllPricesError != null) throw new RuntimeException(requestAllPricesError);
return cache; return cache;
} }

View File

@ -476,8 +476,9 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
break; break;
} catch (Exception e) { } catch (Exception e) {
if (trade.isPayoutPublished()) throw new IllegalStateException("Payout tx already published for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); if (trade.isPayoutPublished()) throw new IllegalStateException("Payout tx already published for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
log.warn("Failed to submit dispute payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage()); log.warn("Failed to submit dispute payout tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
if (trade.getXmrConnectionService().isConnected()) trade.requestSwitchToNextBestConnection();
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} }
} }

View File

@ -78,8 +78,11 @@ public class HavenoUtils {
public static final double TAKER_FEE_PCT = 0.001; // 0.1% public static final double TAKER_FEE_PCT = 0.001; // 0.1%
public static final double PENALTY_FEE_PCT = 0.02; // 2% public static final double PENALTY_FEE_PCT = 0.02; // 2%
// other configuration
public static final long LOG_POLL_ERROR_PERIOD_MS = 1000 * 60 * 4; // log poll errors up to once every 4 minutes
// synchronize requests to the daemon // synchronize requests to the daemon
private static boolean SYNC_DAEMON_REQUESTS = true; // sync long requests to daemon (e.g. refresh, update pool) private static boolean SYNC_DAEMON_REQUESTS = false; // sync long requests to daemon (e.g. refresh, update pool) // TODO: performance suffers by syncing daemon requests, but otherwise we sometimes get sporadic errors?
private static boolean SYNC_WALLET_REQUESTS = false; // additionally sync wallet functions to daemon (e.g. create txs) private static boolean SYNC_WALLET_REQUESTS = false; // additionally sync wallet functions to daemon (e.g. create txs)
private static Object DAEMON_LOCK = new Object(); private static Object DAEMON_LOCK = new Object();
public static Object getDaemonLock() { public static Object getDaemonLock() {
@ -99,7 +102,7 @@ public class HavenoUtils {
public static final DecimalFormat XMR_FORMATTER = new DecimalFormat("##############0.000000000000", DECIMAL_FORMAT_SYMBOLS); public static final DecimalFormat XMR_FORMATTER = new DecimalFormat("##############0.000000000000", DECIMAL_FORMAT_SYMBOLS);
public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss"); public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss");
// TODO: better way to share references? // shared references TODO: better way to share references?
public static HavenoSetup havenoSetup; public static HavenoSetup havenoSetup;
public static ArbitrationManager arbitrationManager; public static ArbitrationManager arbitrationManager;
public static XmrWalletService xmrWalletService; public static XmrWalletService xmrWalletService;

View File

@ -141,6 +141,7 @@ public abstract class Trade implements Tradable, Model {
private static final long SHUTDOWN_TIMEOUT_MS = 60000; private static final long SHUTDOWN_TIMEOUT_MS = 60000;
private static final long SYNC_EVERY_NUM_BLOCKS = 360; // ~1/2 day private static final long SYNC_EVERY_NUM_BLOCKS = 360; // ~1/2 day
private static final long DELETE_AFTER_NUM_BLOCKS = 2; // if deposit requested but not published private static final long DELETE_AFTER_NUM_BLOCKS = 2; // if deposit requested but not published
private static final long EXTENDED_RPC_TIMEOUT = 600000; // 10 minutes
private static final long DELETE_AFTER_MS = TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS; private static final long DELETE_AFTER_MS = TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS;
private final Object walletLock = new Object(); private final Object walletLock = new Object();
private final Object pollLock = new Object(); private final Object pollLock = new Object();
@ -490,7 +491,6 @@ public abstract class Trade implements Tradable, Model {
private Long payoutHeight; private Long payoutHeight;
private IdlePayoutSyncer idlePayoutSyncer; private IdlePayoutSyncer idlePayoutSyncer;
@Getter @Getter
@Setter
private boolean isCompleted; private boolean isCompleted;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -614,8 +614,8 @@ public abstract class Trade implements Tradable, Model {
public void initialize(ProcessModelServiceProvider serviceProvider) { public void initialize(ProcessModelServiceProvider serviceProvider) {
if (isInitialized) throw new IllegalStateException(getClass().getSimpleName() + " " + getId() + " is already initialized"); if (isInitialized) throw new IllegalStateException(getClass().getSimpleName() + " " + getId() + " is already initialized");
// done if payout unlocked // done if payout unlocked and marked complete
if (isPayoutUnlocked()) { if (isPayoutUnlocked() && isCompleted()) {
clearAndShutDown(); clearAndShutDown();
return; return;
} }
@ -627,9 +627,7 @@ public abstract class Trade implements Tradable, Model {
// handle connection change on dedicated thread // handle connection change on dedicated thread
xmrConnectionService.addConnectionListener(connection -> { xmrConnectionService.addConnectionListener(connection -> {
ThreadUtils.submitToPool(() -> { // TODO: remove this? ThreadUtils.execute(() -> onConnectionChanged(connection), getId());
ThreadUtils.execute(() -> onConnectionChanged(connection), getConnectionChangedThreadId());
});
}); });
// reset buyer's payment sent state if no ack receive // 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()); log.info("Payout published for {} {}", getClass().getSimpleName(), getId());
// sync main wallet to update pending balance // sync main wallet to update pending balance
if (!isPayoutConfirmed()) { ThreadUtils.submitToPool(() -> {
new Thread(() -> { HavenoUtils.waitFor(1000);
HavenoUtils.waitFor(1000); if (isPayoutConfirmed()) return;
if (isShutDownStarted) return; if (isShutDownStarted) return;
if (xmrConnectionService.isConnected()) syncAndPollWallet(); if (xmrConnectionService.isConnected()) syncAndPollWallet();
}).start(); });
}
// complete disputed trade // complete disputed trade
if (getDisputeState().isArbitrated() && !getDisputeState().isClosed()) { if (getDisputeState().isArbitrated() && !getDisputeState().isClosed()) {
@ -707,7 +704,8 @@ public abstract class Trade implements Tradable, Model {
if (newValue == Trade.PayoutState.PAYOUT_UNLOCKED) { if (newValue == Trade.PayoutState.PAYOUT_UNLOCKED) {
if (!isInitialized) return; if (!isInitialized) return;
log.info("Payout unlocked for {} {}, deleting multisig wallet", getClass().getSimpleName(), getId()); 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(); return getArbitrator() == null ? null : getArbitrator().getNodeAddress();
} }
public void setCompleted(boolean completed) {
this.isCompleted = completed;
if (isPayoutUnlocked()) clearAndShutDown();
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// WALLET MANAGEMENT // 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() { public boolean isIdling() {
return this instanceof ArbitratorTrade && isDepositsConfirmed() && walletExists() && pollNormalStartTimeMs == null; // arbitrator idles trade after deposits confirm unless overriden 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(); }).start();
} }
public void importMultisigHex() { private boolean isReadTimeoutError(String errMsg) {
synchronized (walletLock) { return errMsg.contains("Read timed out");
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<String> multisigHexes = new ArrayList<String>();
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());
} }
// 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? // 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() { 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() { public void saveWallet() {
@ -996,7 +952,11 @@ public abstract class Trade implements Tradable, Model {
private void forceCloseWallet() { private void forceCloseWallet() {
if (wallet != null) { 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(); stopPolling();
wallet = null; wallet = null;
} }
@ -1007,29 +967,31 @@ public abstract class Trade implements Tradable, Model {
if (walletExists()) { if (walletExists()) {
try { try {
// ensure wallet is initialized // check wallet state if deposit requested
boolean syncedWallet = false; if (isDepositRequested()) {
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 // ensure wallet is initialized
if (isDepositRequested() && !isPayoutUnlocked() && !syncedWallet) { boolean syncedWallet = false;
log.warn("Syncing wallet on deletion for trade {} {}, syncing", getClass().getSimpleName(), getId()); if (wallet == null) {
syncWallet(true); log.warn("Wallet is not initialized for {} {}, opening", getClass().getSimpleName(), getId());
} getWallet();
syncWallet(true);
// check if deposits published and payout not unlocked syncedWallet = true;
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");
} // sync wallet if deposit requested and payout not unlocked
if (!isPayoutUnlocked() && !syncedWallet) {
// check for balance log.warn("Syncing wallet on deletion for trade {} {}, syncing", getClass().getSimpleName(), getId());
if (wallet.getBalance().compareTo(BigInteger.ZERO) > 0) { syncWallet(true);
synchronized (HavenoUtils.getDaemonLock()) { }
// 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()); log.warn("Rescanning spent outputs for {} {}", getClass().getSimpleName(), getId());
wallet.rescanSpent(); wallet.rescanSpent();
if (wallet.getBalance().compareTo(BigInteger.ZERO) > 0) { 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<String> multisigHexes = new ArrayList<String>();
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. * Create the payout tx.
* *
@ -1117,9 +1177,12 @@ public abstract class Trade implements Tradable, Model {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
try { try {
return doCreatePayoutTx(); return doCreatePayoutTx();
} catch (IllegalArgumentException | IllegalStateException e) {
throw e;
} catch (Exception 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 (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} }
} }
@ -1131,14 +1194,10 @@ public abstract class Trade implements Tradable, Model {
private MoneroTxWallet doCreatePayoutTx() { private MoneroTxWallet doCreatePayoutTx() {
// check if multisig import needed // 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? // recover if missing wallet data
List<MoneroTxWallet> txs = wallet.getTxs(); // TODO: this fetches from pool recoverIfMissingWalletData();
if (txs.isEmpty()) {
log.warn("Restarting wallet for {} {} because deposit txs are missing to create payout tx", getClass().getSimpleName(), getId());
forceRestartTradeWallet();
}
// gather info // gather info
String sellerPayoutAddress = getSeller().getPayoutAddressString(); String sellerPayoutAddress = getSeller().getPayoutAddressString();
@ -1176,11 +1235,15 @@ public abstract class Trade implements Tradable, Model {
synchronized (HavenoUtils.getWalletFunctionLock()) { synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
try { try {
if (wallet.isMultisigImportNeeded()) throw new IllegalStateException("Cannot create dispute payout tx because multisig import is needed for " + getClass().getSimpleName() + " " + getShortId());
return createTx(txConfig); return createTx(txConfig);
} catch (IllegalArgumentException | IllegalStateException e) {
throw e;
} catch (Exception e) { } catch (Exception e) {
if (e.getMessage().contains("not possible")) throw new RuntimeException("Loser payout is too small to cover the mining fee"); 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, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage()); 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 (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying 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 * @param publish publishes the signed payout tx if true
*/ */
public void processPayoutTx(String payoutTxHex, boolean sign, boolean publish) { public void processPayoutTx(String payoutTxHex, boolean sign, boolean publish) {
log.info("Processing payout tx for {} {}", getClass().getSimpleName(), getId()); synchronized (walletLock) {
synchronized (HavenoUtils.getWalletFunctionLock()) {
// TODO: wallet sometimes returns empty data, after disconnect? detect this condition with failure tolerance for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { try {
try { doProcessPayoutTx(payoutTxHex, sign, publish);
List<MoneroTxWallet> txs = wallet.getTxs(); // TODO: this fetches from pool break;
if (txs.isEmpty()) { } catch (IllegalArgumentException | IllegalStateException e) {
log.warn("Restarting wallet for {} {} because deposit txs are missing to process payout tx", getClass().getSimpleName(), getId()); throw e;
forceRestartTradeWallet(); } 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 // gather relevant info
MoneroWallet wallet = getWallet(); MoneroWallet wallet = getWallet();
@ -1226,6 +1296,7 @@ public abstract class Trade implements Tradable, Model {
MoneroTxSet describedTxSet = wallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex)); 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 if (describedTxSet.getTxs() == null || describedTxSet.getTxs().size() != 1) throw new IllegalArgumentException("Bad payout tx"); // TODO (woodser): test nack
MoneroTxWallet payoutTx = describedTxSet.getTxs().get(0); MoneroTxWallet payoutTx = describedTxSet.getTxs().get(0);
if (payoutTxId == null) updatePayout(payoutTx); // update payout tx if not signed
// verify payout tx has exactly 2 destinations // 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"); 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); 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 // check connection
if (sign || publish) verifyDaemonConnection(); boolean doSign = sign && getPayoutTxHex() == null;
if (doSign || publish) verifyDaemonConnection();
// handle tx signing // handle tx signing
if (sign) { if (doSign) {
// sign tx // sign tx
try { try {
@ -1275,6 +1347,7 @@ public abstract class Trade implements Tradable, Model {
// describe result // describe result
describedTxSet = wallet.describeMultisigTxSet(payoutTxHex); describedTxSet = wallet.describeMultisigTxSet(payoutTxHex);
payoutTx = describedTxSet.getTxs().get(0); payoutTx = describedTxSet.getTxs().get(0);
updatePayout(payoutTx);
// verify fee is within tolerance by recreating payout tx // 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? // 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); log.info("Payout tx fee {} is within tolerance, diff %={}", payoutTx.getFee(), feeDiff);
} }
// update trade state // save trade state
updatePayout(payoutTx);
requestPersistence(); requestPersistence();
// submit payout tx // submit payout tx
if (publish) { if (publish) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { try {
try { wallet.submitMultisigTxHex(payoutTxHex);
wallet.submitMultisigTxHex(payoutTxHex); setPayoutStatePublished();
ThreadUtils.submitToPool(() -> pollWallet()); } catch (Exception e) {
break; throw new RuntimeException("Failed to submit payout tx for " + getClass().getSimpleName() + " " + getId(), e);
} 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
}
} }
} }
} }
@ -1436,7 +1503,6 @@ public abstract class Trade implements Tradable, Model {
isShutDown = true; isShutDown = true;
List<Runnable> shutDownThreads = new ArrayList<>(); List<Runnable> shutDownThreads = new ArrayList<>();
shutDownThreads.add(() -> ThreadUtils.shutDown(getId())); shutDownThreads.add(() -> ThreadUtils.shutDown(getId()));
shutDownThreads.add(() -> ThreadUtils.shutDown(getConnectionChangedThreadId()));
ThreadUtils.awaitTasks(shutDownThreads); ThreadUtils.awaitTasks(shutDownThreads);
} }
@ -1592,6 +1658,17 @@ public abstract class Trade implements Tradable, Model {
private void removeTradeOnError() { private void removeTradeOnError() {
log.warn("removeTradeOnError() trade={}, tradeId={}, state={}", getClass().getSimpleName(), getShortId(), getState()); log.warn("removeTradeOnError() trade={}, tradeId={}, state={}", getClass().getSimpleName(), getShortId(), getState());
// force close and re-open wallet in case stuck
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 // clear and shut down trade
clearAndShutDown(); clearAndShutDown();
@ -2236,10 +2313,6 @@ public abstract class Trade implements Tradable, Model {
// Private // Private
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
private String getConnectionChangedThreadId() {
return getId() + ".onConnectionChanged";
}
// lazy initialization // lazy initialization
private ObjectProperty<BigInteger> getAmountProperty() { private ObjectProperty<BigInteger> getAmountProperty() {
if (tradeAmountProperty == null) if (tradeAmountProperty == null)
@ -2258,6 +2331,9 @@ public abstract class Trade implements Tradable, Model {
private void onConnectionChanged(MoneroRpcConnection connection) { private void onConnectionChanged(MoneroRpcConnection connection) {
synchronized (walletLock) { synchronized (walletLock) {
// use current connection
connection = xmrConnectionService.getConnection();
// check if ignored // check if ignored
if (isShutDownStarted) return; if (isShutDownStarted) return;
if (getWallet() == null) return; if (getWallet() == null) return;
@ -2321,24 +2397,29 @@ public abstract class Trade implements Tradable, Model {
} }
private void syncWallet(boolean pollWallet) { private void syncWallet(boolean pollWallet) {
if (getWallet() == null) throw new RuntimeException("Cannot sync trade wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId()); try {
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 (getWallet() == null) throw new RuntimeException("Cannot sync trade wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId());
if (isWalletBehind()) { if (getWallet().getDaemonConnection() == null) throw new RuntimeException("Cannot sync trade wallet because it's not connected to a Monero daemon for " + getClass().getSimpleName() + ", " + getId());
log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getShortId()); if (isWalletBehind()) {
long startTime = System.currentTimeMillis(); log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getShortId());
syncWalletIfBehind(); long startTime = System.currentTimeMillis();
log.info("Done syncing wallet for {} {} in {} ms", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime); 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());
} }
// 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() { public void updatePollPeriod() {
@ -2347,11 +2428,11 @@ public abstract class Trade implements Tradable, Model {
} }
private void setPollPeriod(long pollPeriodMs) { private void setPollPeriod(long pollPeriodMs) {
synchronized (walletLock) { synchronized (pollLock) {
if (this.isShutDownStarted) return; if (this.isShutDownStarted) return;
if (this.pollPeriodMs != null && this.pollPeriodMs == pollPeriodMs) return; if (this.pollPeriodMs != null && this.pollPeriodMs == pollPeriodMs) return;
this.pollPeriodMs = pollPeriodMs; this.pollPeriodMs = pollPeriodMs;
if (isPollInProgress()) { if (isPolling()) {
stopPolling(); stopPolling();
startPolling(); startPolling();
} }
@ -2364,8 +2445,8 @@ public abstract class Trade implements Tradable, Model {
} }
private void startPolling() { private void startPolling() {
synchronized (walletLock) { synchronized (pollLock) {
if (isShutDownStarted || isPollInProgress()) return; if (isShutDownStarted || isPolling()) return;
updatePollPeriod(); updatePollPeriod();
log.info("Starting to poll wallet for {} {}", getClass().getSimpleName(), getId()); log.info("Starting to poll wallet for {} {}", getClass().getSimpleName(), getId());
pollLooper = new TaskLooper(() -> pollWallet()); pollLooper = new TaskLooper(() -> pollWallet());
@ -2374,151 +2455,160 @@ public abstract class Trade implements Tradable, Model {
} }
private void stopPolling() { private void stopPolling() {
synchronized (walletLock) { synchronized (pollLock) {
if (isPollInProgress()) { if (isPolling()) {
pollLooper.stop(); pollLooper.stop();
pollLooper = null; pollLooper = null;
} }
} }
} }
private boolean isPollInProgress() { private boolean isPolling() {
synchronized (walletLock) { synchronized (pollLock) {
return pollLooper != null; return pollLooper != null;
} }
} }
private void pollWallet() { private void pollWallet() {
if (pollInProgress) return; synchronized (pollLock) {
if (pollInProgress) return;
}
doPollWallet(); doPollWallet();
} }
private void doPollWallet() { private void doPollWallet() {
if (isShutDownStarted) return;
synchronized (pollLock) { synchronized (pollLock) {
pollInProgress = true; pollInProgress = true;
try { }
try {
// skip if payout unlocked // skip if payout unlocked
if (isPayoutUnlocked()) return; if (isPayoutUnlocked()) return;
// skip if deposit txs unknown or not requested // skip if deposit txs unknown or not requested
if (processModel.getMaker().getDepositTxHash() == null || processModel.getTaker().getDepositTxHash() == null || !isDepositRequested()) return; if (processModel.getMaker().getDepositTxHash() == null || processModel.getTaker().getDepositTxHash() == null || !isDepositRequested()) return;
// sync if wallet too far behind daemon // skip if daemon not synced
if (xmrConnectionService.getTargetHeight() == null) return; if (xmrConnectionService.getTargetHeight() == null || !xmrConnectionService.isSyncedWithinTolerance()) return;
if (walletHeight.get() < xmrConnectionService.getTargetHeight() - SYNC_EVERY_NUM_BLOCKS) syncWallet(false);
// update deposit txs // sync if wallet too far behind daemon
if (!isDepositsUnlocked()) { if (walletHeight.get() < xmrConnectionService.getTargetHeight() - SYNC_EVERY_NUM_BLOCKS) syncWallet(false);
// sync wallet if behind // update deposit txs
syncWalletIfBehind(); if (!isDepositsUnlocked()) {
// get txs from trade wallet // sync wallet if behind
MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true); syncWalletIfBehind();
Boolean updatePool = !isDepositsConfirmed() && (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null);
if (!updatePool) query.setInTxPool(false); // avoid updating from pool if possible
List<MoneroTxWallet> 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();
// set actual security deposits // get txs from trade wallet
if (getBuyer().getSecurityDeposit().longValueExact() == 0) { MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true);
BigInteger buyerSecurityDeposit = ((MoneroTxWallet) getBuyer().getDepositTx()).getIncomingAmount(); Boolean updatePool = !isDepositsConfirmed() && (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null);
BigInteger sellerSecurityDeposit = ((MoneroTxWallet) getSeller().getDepositTx()).getIncomingAmount().subtract(getAmount()); if (!updatePool) query.setInTxPool(false); // avoid updating from pool if possible
getBuyer().setSecurityDeposit(buyerSecurityDeposit); List<MoneroTxWallet> txs;
getSeller().setSecurityDeposit(sellerSecurityDeposit); if (!updatePool) txs = wallet.getTxs(query);
}
// 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<MoneroTxWallet> 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 { else {
boolean isWalletConnected = isWalletConnectedToDaemon(); synchronized (walletLock) {
if (!isShutDownStarted && wallet != null && isWalletConnected) { synchronized (HavenoUtils.getDaemonLock()) {
log.warn("Error polling trade wallet for {} {}, errorMessage={}. Monerod={}", getClass().getSimpleName(), getShortId(), e.getMessage(), getXmrWalletService().getConnectionService().getConnection()); txs = wallet.getTxs(query);
//e.printStackTrace(); }
} }
} }
} 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<MoneroTxWallet> 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; pollInProgress = false;
} }
requestSaveWallet();
} }
} }
@ -2543,6 +2633,78 @@ public abstract class Trade implements Tradable, Model {
depositTxsUpdateCounter.set(depositTxsUpdateCounter.get() + 1); 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() { private void forceRestartTradeWallet() {
if (isShutDownStarted || restartInProgress) return; if (isShutDownStarted || restartInProgress) return;
log.warn("Force restarting trade wallet for {} {}", getClass().getSimpleName(), getId()); log.warn("Force restarting trade wallet for {} {}", getClass().getSimpleName(), getId());
@ -2550,7 +2712,7 @@ public abstract class Trade implements Tradable, Model {
forceCloseWallet(); forceCloseWallet();
if (!isShutDownStarted) wallet = getWallet(); if (!isShutDownStarted) wallet = getWallet();
restartInProgress = false; restartInProgress = false;
doPollWallet(); pollWallet();
if (!isShutDownStarted) ThreadUtils.execute(() -> tryInitPolling(), getId()); if (!isShutDownStarted) ThreadUtils.execute(() -> tryInitPolling(), getId());
} }

View File

@ -284,23 +284,25 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@Override @Override
public void onDirectMessage(DecryptedMessageWithPubKey message, NodeAddress peer) { public void onDirectMessage(DecryptedMessageWithPubKey message, NodeAddress sender) {
NetworkEnvelope networkEnvelope = message.getNetworkEnvelope(); NetworkEnvelope networkEnvelope = message.getNetworkEnvelope();
if (!(networkEnvelope instanceof TradeMessage)) return; 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(() -> { ThreadUtils.execute(() -> {
if (networkEnvelope instanceof InitTradeRequest) { if (networkEnvelope instanceof InitTradeRequest) {
handleInitTradeRequest((InitTradeRequest) networkEnvelope, peer); handleInitTradeRequest((InitTradeRequest) networkEnvelope, sender);
} else if (networkEnvelope instanceof InitMultisigRequest) { } else if (networkEnvelope instanceof InitMultisigRequest) {
handleInitMultisigRequest((InitMultisigRequest) networkEnvelope, peer); handleInitMultisigRequest((InitMultisigRequest) networkEnvelope, sender);
} else if (networkEnvelope instanceof SignContractRequest) { } else if (networkEnvelope instanceof SignContractRequest) {
handleSignContractRequest((SignContractRequest) networkEnvelope, peer); handleSignContractRequest((SignContractRequest) networkEnvelope, sender);
} else if (networkEnvelope instanceof SignContractResponse) { } else if (networkEnvelope instanceof SignContractResponse) {
handleSignContractResponse((SignContractResponse) networkEnvelope, peer); handleSignContractResponse((SignContractResponse) networkEnvelope, sender);
} else if (networkEnvelope instanceof DepositRequest) { } else if (networkEnvelope instanceof DepositRequest) {
handleDepositRequest((DepositRequest) networkEnvelope, peer); handleDepositRequest((DepositRequest) networkEnvelope, sender);
} else if (networkEnvelope instanceof DepositResponse) { } else if (networkEnvelope instanceof DepositResponse) {
handleDepositResponse((DepositResponse) networkEnvelope, peer); handleDepositResponse((DepositResponse) networkEnvelope, sender);
} }
}, tradeId); }, tradeId);
} }
@ -538,7 +540,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
} }
private void handleInitTradeRequest(InitTradeRequest request, NodeAddress sender) { 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 { try {
Validator.nonEmptyStringOf(request.getOfferId()); Validator.nonEmptyStringOf(request.getOfferId());
@ -734,8 +736,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
} }
} }
private void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress peer) { private void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
log.info("Received {} for trade {} from {} with uid {}", request.getClass().getSimpleName(), request.getOfferId(), peer, request.getUid()); log.info("TradeManager handling InitMultisigRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid());
try { try {
Validator.nonEmptyStringOf(request.getOfferId()); Validator.nonEmptyStringOf(request.getOfferId());
@ -750,11 +752,11 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
return; return;
} }
Trade trade = tradeOptional.get(); Trade trade = tradeOptional.get();
getTradeProtocol(trade).handleInitMultisigRequest(request, peer); getTradeProtocol(trade).handleInitMultisigRequest(request, sender);
} }
private void handleSignContractRequest(SignContractRequest request, NodeAddress peer) { private void handleSignContractRequest(SignContractRequest request, NodeAddress sender) {
log.info("Received {} for trade {} from {} with uid {}", request.getClass().getSimpleName(), request.getOfferId(), peer, request.getUid()); log.info("TradeManager handling SignContractRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid());
try { try {
Validator.nonEmptyStringOf(request.getOfferId()); Validator.nonEmptyStringOf(request.getOfferId());
@ -769,11 +771,11 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
return; return;
} }
Trade trade = tradeOptional.get(); Trade trade = tradeOptional.get();
getTradeProtocol(trade).handleSignContractRequest(request, peer); getTradeProtocol(trade).handleSignContractRequest(request, sender);
} }
private void handleSignContractResponse(SignContractResponse request, NodeAddress peer) { private void handleSignContractResponse(SignContractResponse request, NodeAddress sender) {
log.info("Received {} for trade {} from {} with uid {}", request.getClass().getSimpleName(), request.getOfferId(), peer, request.getUid()); log.info("TradeManager handling SignContractResponse for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid());
try { try {
Validator.nonEmptyStringOf(request.getOfferId()); Validator.nonEmptyStringOf(request.getOfferId());
@ -788,11 +790,11 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
return; return;
} }
Trade trade = tradeOptional.get(); Trade trade = tradeOptional.get();
((TraderProtocol) getTradeProtocol(trade)).handleSignContractResponse(request, peer); ((TraderProtocol) getTradeProtocol(trade)).handleSignContractResponse(request, sender);
} }
private void handleDepositRequest(DepositRequest request, NodeAddress peer) { private void handleDepositRequest(DepositRequest request, NodeAddress sender) {
log.info("Received {} for trade {} from {} with uid {}", request.getClass().getSimpleName(), request.getOfferId(), peer, request.getUid()); log.info("TradeManager handling DepositRequest for tradeId={}, sender={}, uid={}", request.getOfferId(), sender, request.getUid());
try { try {
Validator.nonEmptyStringOf(request.getOfferId()); Validator.nonEmptyStringOf(request.getOfferId());
@ -807,11 +809,11 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
return; return;
} }
Trade trade = tradeOptional.get(); Trade trade = tradeOptional.get();
((ArbitratorProtocol) getTradeProtocol(trade)).handleDepositRequest(request, peer); ((ArbitratorProtocol) getTradeProtocol(trade)).handleDepositRequest(request, sender);
} }
private void handleDepositResponse(DepositResponse response, NodeAddress peer) { private void handleDepositResponse(DepositResponse response, NodeAddress sender) {
log.info("Received {} for trade {} from {} with uid {}", response.getClass().getSimpleName(), response.getOfferId(), peer, response.getUid()); log.info("TradeManager handling DepositResponse for tradeId={}, sender={}, uid={}", response.getOfferId(), sender, response.getUid());
try { try {
Validator.nonEmptyStringOf(response.getOfferId()); Validator.nonEmptyStringOf(response.getOfferId());
@ -829,7 +831,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
} }
} }
Trade trade = tradeOptional.get(); Trade trade = tradeOptional.get();
((TraderProtocol) getTradeProtocol(trade)).handleDepositResponse(response, peer); ((TraderProtocol) getTradeProtocol(trade)).handleDepositResponse(response, sender);
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -172,7 +172,7 @@ public class TradeUtil {
* @param trade Trade * @param trade Trade
* @return String describing a trader's role for a given 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(); Offer offer = trade.getOffer();
if (offer == null) if (offer == null)
throw new IllegalStateException(format("could not get role because no offer was found for trade '%s'", 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 * @param currencyCode String
* @return String describing a trader's role * @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)) { if (isTraditionalCurrency(currencyCode)) {
String baseCurrencyCode = Res.getBaseCurrencyCode(); String baseCurrencyCode = Res.getBaseCurrencyCode();
if (isBuyerMakerAndSellerTaker) if (isBuyerMakerAndSellerTaker)

View File

@ -296,7 +296,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
} }
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) { 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(); trade.addInitProgressStep();
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
synchronized (trade) { synchronized (trade) {
@ -333,7 +333,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
} }
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) { 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(() -> { ThreadUtils.execute(() -> {
synchronized (trade) { synchronized (trade) {
@ -376,7 +376,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
} }
public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) { 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(); trade.addInitProgressStep();
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
synchronized (trade) { synchronized (trade) {
@ -422,7 +422,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
} }
public void handleDepositResponse(DepositResponse response, NodeAddress sender) { 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(); trade.addInitProgressStep();
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
synchronized (trade) { synchronized (trade) {
@ -452,7 +452,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
} }
public void handle(DepositsConfirmedMessage message, NodeAddress sender) { 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; if (!trade.isInitialized() || trade.isShutDown()) return;
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
synchronized (trade) { synchronized (trade) {
@ -481,7 +481,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
// received by seller and arbitrator // received by seller and arbitrator
protected void handle(PaymentSentMessage message, NodeAddress peer) { 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.isInitialized() || trade.isShutDown()) return;
if (!(trade instanceof SellerTrade || trade instanceof ArbitratorTrade)) { if (!(trade instanceof SellerTrade || trade instanceof ArbitratorTrade)) {
log.warn("Ignoring PaymentSentMessage since not seller or arbitrator"); 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) { 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; if (!trade.isInitialized() || trade.isShutDown()) return;
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
if (!(trade instanceof BuyerTrade || trade instanceof ArbitratorTrade)) { if (!(trade instanceof BuyerTrade || trade instanceof ArbitratorTrade)) {

View File

@ -105,6 +105,7 @@ public class MaybeSendSignContractRequest extends TradeTask {
} catch (Exception e) { } catch (Exception e) {
log.warn("Error creating deposit tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage()); 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 (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
if (trade.getXmrConnectionService().isConnected()) trade.getXmrWalletService().requestSwitchToNextBestConnection();
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} }

View File

@ -61,16 +61,20 @@ public class ProcessDepositsConfirmedMessage extends TradeTask {
} }
// update multisig hex // update multisig hex
sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex()); if (sender.getUpdatedMultisigHex() == null) {
sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex());
// try to import multisig hex (retry later) // try to import multisig hex (retry later)
ThreadUtils.submitToPool(() -> { if (!trade.isPayoutPublished()) {
try { ThreadUtils.submitToPool(() -> {
trade.importMultisigHex(); try {
} catch (Exception e) { trade.importMultisigHex();
e.printStackTrace(); } catch (Exception e) {
e.printStackTrace();
}
});
} }
}); }
// persist // persist
processModel.getTradeManager().requestPersistence(); processModel.getTradeManager().requestPersistence();

View File

@ -71,6 +71,7 @@ public class TakerReserveTradeFunds extends TradeTask {
} catch (Exception e) { } catch (Exception e) {
log.warn("Error creating reserve tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage()); 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 (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
if (trade.getXmrConnectionService().isConnected()) trade.getXmrWalletService().requestSwitchToNextBestConnection();
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} }

View File

@ -54,6 +54,8 @@ import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.Random;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
@ -88,13 +90,32 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
Offer offer = checkNotNull(trade.getOffer()); Offer offer = checkNotNull(trade.getOffer());
return new TradeStatistics3(offer.getCurrencyCode(), return new TradeStatistics3(offer.getCurrencyCode(),
trade.getPrice().getValue(), trade.getPrice().getValue(),
trade.getAmount().longValueExact(), fuzzTradeAmountReproducibly(trade),
offer.getPaymentMethod().getId(), offer.getPaymentMethod().getId(),
trade.getTakeOfferDate().getTime(), fuzzTradeDateReproducibly(trade),
truncatedArbitratorNodeAddress, truncatedArbitratorNodeAddress,
extraDataMap); 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. // 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. // 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 // 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

View File

@ -35,6 +35,8 @@
package haveno.core.xmr; package haveno.core.xmr;
import com.google.inject.Inject; import com.google.inject.Inject;
import haveno.common.ThreadUtils;
import haveno.common.UserThread; import haveno.common.UserThread;
import haveno.core.api.model.XmrBalanceInfo; import haveno.core.api.model.XmrBalanceInfo;
import haveno.core.offer.OpenOffer; import haveno.core.offer.OpenOffer;
@ -103,7 +105,7 @@ public class Balances {
updateBalances(); updateBalances();
} }
}); });
updateBalances(); doUpdateBalances();
} }
public XmrBalanceInfo getBalances() { public XmrBalanceInfo getBalances() {
@ -117,42 +119,48 @@ public class Balances {
} }
private void updateBalances() { private void updateBalances() {
ThreadUtils.submitToPool(() -> doUpdateBalances());
}
private void doUpdateBalances() {
synchronized (this) { synchronized (this) {
synchronized (XmrWalletService.WALLET_LOCK) {
// 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 // get wallet balances
pendingBalance = balance.subtract(availableBalance); BigInteger balance = xmrWalletService.getWallet() == null ? BigInteger.ZERO : xmrWalletService.getBalance();
List<Trade> trades = tradeManager.getTradesStreamWithFundsLockedIn().collect(Collectors.toList()); availableBalance = xmrWalletService.getWallet() == null ? BigInteger.ZERO : xmrWalletService.getAvailableBalance();
for (Trade trade : trades) {
if (trade.getFrozenAmount().equals(new BigInteger("0"))) continue; // calculate pending balance by adding frozen trade balances - reserved amounts
BigInteger tradeFee = trade instanceof MakerTrade ? trade.getMakerFee() : trade.getTakerFee(); pendingBalance = balance.subtract(availableBalance);
pendingBalance = pendingBalance.add(trade.getFrozenAmount()).subtract(trade.getReservedAmount()).subtract(tradeFee).subtract(trade.getSelf().getDepositTxFee()); List<Trade> 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<MoneroOutputWallet> 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<MoneroOutputWallet> 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));
} }
} }
} }

View File

@ -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, "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, "xmr-node.cakewallet.com", 18081, 2, "@cakewallet"),
new XmrNode(MoneroNodesOption.PROVIDED, null, null, "node.community.rino.io", 18081, 2, "@RINOwallet"), 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.PROVIDED, null, null, "nodes.hashvault.pro", 18080, 2, "@HashVault"),
new XmrNode(MoneroNodesOption.PUBLIC, null, null, "node3.monerodevs.org", 18089, 2, "@monerodevs.org"), new XmrNode(MoneroNodesOption.PROVIDED, null, null, "p2pmd.xmrvsbeast.com", 18080, 2, "@xmrvsbeast"),
new XmrNode(MoneroNodesOption.PUBLIC, null, null, "nodex.monerujo.io", 18081, 2, "@monerujo.io"), 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, "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: default:
throw new IllegalStateException("Unexpected base currency network: " + Config.baseCurrencyNetwork()); throw new IllegalStateException("Unexpected base currency network: " + Config.baseCurrencyNetwork());
@ -150,7 +152,7 @@ public class XmrNodes {
if (parts[0].contains("[") && parts[0].contains(":")) { if (parts[0].contains("[") && parts[0].contains(":")) {
// IPv6 address and optional port number // IPv6 address and optional port number
// address part delimited by square brackets e.g. [2a01:123:456:789::2]:8333 // 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) if (parts.length == 2)
port = Integer.parseInt(parts[1].replace(":", "")); port = Integer.parseInt(parts[1].replace(":", ""));
} else if (parts[0].contains(":") && !parts[0].contains(".")) { } else if (parts[0].contains(":") && !parts[0].contains(".")) {

View File

@ -32,6 +32,8 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import haveno.core.trade.HavenoUtils;
/** /**
* Poll for changes to the spent status of key images. * Poll for changes to the spent status of key images.
* *
@ -47,6 +49,7 @@ public class XmrKeyImagePoller {
private TaskLooper looper; private TaskLooper looper;
private Map<String, MoneroKeyImageSpentStatus> lastStatuses = new HashMap<String, MoneroKeyImageSpentStatus>(); private Map<String, MoneroKeyImageSpentStatus> lastStatuses = new HashMap<String, MoneroKeyImageSpentStatus>();
private boolean isPolling = false; private boolean isPolling = false;
private Long lastLogPollErrorTimestamp;
/** /**
* Construct the listener. * 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 spentStatuses = daemon.getKeyImageSpentStatuses(keyImages); // TODO monero-java: if order of getKeyImageSpentStatuses is guaranteed, then it should take list parameter
} }
} catch (Exception e) { } 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; return;
} }

View File

@ -24,6 +24,7 @@ import com.google.inject.name.Named;
import common.utils.JsonUtils; import common.utils.JsonUtils;
import haveno.common.ThreadUtils; import haveno.common.ThreadUtils;
import haveno.common.Timer;
import haveno.common.UserThread; import haveno.common.UserThread;
import haveno.common.config.Config; import haveno.common.config.Config;
import haveno.common.file.FileUtil; import haveno.common.file.FileUtil;
@ -67,6 +68,7 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import javafx.beans.property.LongProperty; import javafx.beans.property.LongProperty;
@ -132,8 +134,6 @@ public class XmrWalletService {
private static final String THREAD_ID = XmrWalletService.class.getSimpleName(); private static final String THREAD_ID = XmrWalletService.class.getSimpleName();
private static final long SHUTDOWN_TIMEOUT_MS = 60000; private static final long SHUTDOWN_TIMEOUT_MS = 60000;
private static final long NUM_BLOCKS_BEHIND_TOLERANCE = 5; 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 User user;
private final Preferences preferences; private final Preferences preferences;
@ -155,19 +155,22 @@ public class XmrWalletService {
private TradeManager tradeManager; private TradeManager tradeManager;
private MoneroWallet wallet; private MoneroWallet wallet;
public static final Object WALLET_LOCK = new Object(); public static final Object WALLET_LOCK = new Object();
private boolean wasWalletSynced = false; private boolean wasWalletSynced;
private final Map<String, Optional<MoneroTx>> txCache = new HashMap<String, Optional<MoneroTx>>(); private final Map<String, Optional<MoneroTx>> txCache = new HashMap<String, Optional<MoneroTx>>();
private boolean isClosingWallet = false; private boolean isClosingWallet;
private boolean isShutDownStarted = false; private boolean isShutDownStarted;
private ExecutorService syncWalletThreadPool = Executors.newFixedThreadPool(10); // TODO: adjust based on connection type private ExecutorService syncWalletThreadPool = Executors.newFixedThreadPool(10); // TODO: adjust based on connection type
private Long syncStartHeight = null; private Long syncStartHeight;
private TaskLooper syncWithProgressLooper = null; private TaskLooper syncProgressLooper;
CountDownLatch syncWithProgressLatch; private CountDownLatch syncProgressLatch;
private Timer syncProgressTimeout;
private static final int SYNC_PROGRESS_TIMEOUT_SECONDS = 45;
// wallet polling and cache // wallet polling and cache
private TaskLooper pollLooper; private TaskLooper pollLooper;
private boolean pollInProgress; private boolean pollInProgress;
private Long pollPeriodMs; private Long pollPeriodMs;
private Long lastLogPollErrorTimestamp;
private final Object pollLock = new Object(); private final Object pollLock = new Object();
private Long cachedHeight; private Long cachedHeight;
private BigInteger cachedBalance; private BigInteger cachedBalance;
@ -689,11 +692,12 @@ public class XmrWalletService {
try { try {
return createTradeTxFromSubaddress(feeAmount, feeAddress, sendAmount, sendAddress, subaddressIndices.get(i)); return createTradeTxFromSubaddress(feeAmount, feeAddress, sendAmount, sendAddress, subaddressIndices.get(i));
} catch (Exception e) { } 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 // 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); return createTradeTxFromSubaddress(feeAmount, feeAddress, sendAmount, sendAddress, null);
} }
} }
@ -933,7 +937,7 @@ public class XmrWalletService {
e.printStackTrace(); e.printStackTrace();
// force close wallet // force close wallet
forceCloseWallet(wallet, getWalletPath(MONERO_WALLET_NAME)); forceCloseMainWallet();
} }
log.info("Done shutting down {}", getClass().getSimpleName()); log.info("Done shutting down {}", getClass().getSimpleName());
@ -1281,22 +1285,9 @@ public class XmrWalletService {
else log.info(appliedMsg); else log.info(appliedMsg);
// listen for connection changes // listen for connection changes
xmrConnectionService.addConnectionListener(connection -> { xmrConnectionService.addConnectionListener(connection -> ThreadUtils.execute(() -> {
onConnectionChanged(connection);
// force restart main wallet if connection changed before synced }, THREAD_ID));
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);
}
});
// initialize main wallet when daemon synced // initialize main wallet when daemon synced
walletInitListener = (obs, oldVal, newVal) -> initMainWalletIfConnected(); walletInitListener = (obs, oldVal, newVal) -> initMainWalletIfConnected();
@ -1305,28 +1296,20 @@ public class XmrWalletService {
} }
private void initMainWalletIfConnected() { private void initMainWalletIfConnected() {
ThreadUtils.execute(() -> { if (wallet == null && xmrConnectionService.downloadPercentageProperty().get() == 1 && !isShutDownStarted) {
synchronized (WALLET_LOCK) { maybeInitMainWallet(true);
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;
} }
} }
private void maybeInitMainWallet(boolean sync) {
maybeInitMainWallet(sync, MAX_SYNC_ATTEMPTS);
}
private void maybeInitMainWallet(boolean sync, int numAttempts) { private void maybeInitMainWallet(boolean sync, int numAttempts) {
ThreadUtils.execute(() -> doMaybeInitMainWallet(sync, numAttempts), THREAD_ID);
}
private void doMaybeInitMainWallet(boolean sync, int numAttempts) {
synchronized (WALLET_LOCK) { synchronized (WALLET_LOCK) {
if (isShutDownStarted) return; if (isShutDownStarted) return;
@ -1355,12 +1338,21 @@ public class XmrWalletService {
if (sync && numAttempts > 0) { if (sync && numAttempts > 0) {
try { 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 // sync main wallet
log.info("Syncing main wallet"); log.info("Syncing main wallet");
long time = System.currentTimeMillis(); long time = System.currentTimeMillis();
syncWithProgress(); // blocking syncWithProgress(); // blocking
log.info("Done syncing main wallet in " + (System.currentTimeMillis() - time) + " ms"); log.info("Done syncing main wallet in " + (System.currentTimeMillis() - time) + " ms");
// poll wallet
doPollWallet(true); doPollWallet(true);
if (walletInitListener != null) xmrConnectionService.downloadPercentageProperty().removeListener(walletInitListener);
// log wallet balances // log wallet balances
if (getMoneroNetworkType() != MoneroNetworkType.MAINNET) { 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); log.info("Monero wallet unlocked balance={}, pending balance={}, total balance={}", unlockedBalance, balance.subtract(unlockedBalance), balance);
} }
// reapply connection after wallet synced // reapply connection after wallet synced (might reinitialize wallet on new thread)
onConnectionChanged(xmrConnectionService.getConnection()); ThreadUtils.execute(() -> onConnectionChanged(xmrConnectionService.getConnection()), THREAD_ID);
// reset internal state if main wallet was swapped // reset internal state if main wallet was swapped
resetIfWalletChanged(); resetIfWalletChanged();
@ -1395,12 +1387,12 @@ public class XmrWalletService {
// reschedule to init main wallet // reschedule to init main wallet
UserThread.runAfter(() -> { UserThread.runAfter(() -> {
ThreadUtils.execute(() -> maybeInitMainWallet(true, MAX_SYNC_ATTEMPTS), THREAD_ID); maybeInitMainWallet(true, MAX_SYNC_ATTEMPTS);
}, xmrConnectionService.getRefreshPeriodMs() / 1000); }, xmrConnectionService.getRefreshPeriodMs() / 1000);
} else { } else {
log.warn("Trying again in {} seconds", xmrConnectionService.getRefreshPeriodMs() / 1000); log.warn("Trying again in {} seconds", xmrConnectionService.getRefreshPeriodMs() / 1000);
UserThread.runAfter(() -> { UserThread.runAfter(() -> {
ThreadUtils.execute(() -> maybeInitMainWallet(true, numAttempts - 1), THREAD_ID); maybeInitMainWallet(true, numAttempts - 1);
}, xmrConnectionService.getRefreshPeriodMs() / 1000); }, xmrConnectionService.getRefreshPeriodMs() / 1000);
} }
} }
@ -1431,6 +1423,9 @@ public class XmrWalletService {
private void syncWithProgress() { private void syncWithProgress() {
// start sync progress timeout
resetSyncProgressTimeout();
// show sync progress // show sync progress
updateSyncProgress(wallet.getHeight()); updateSyncProgress(wallet.getHeight());
@ -1458,41 +1453,34 @@ public class XmrWalletService {
// poll wallet for progress // poll wallet for progress
wallet.startSyncing(xmrConnectionService.getRefreshPeriodMs()); wallet.startSyncing(xmrConnectionService.getRefreshPeriodMs());
syncWithProgressLatch = new CountDownLatch(1); syncProgressLatch = new CountDownLatch(1);
syncWithProgressLooper = new TaskLooper(() -> { syncProgressLooper = new TaskLooper(() -> {
if (wallet == null) return; if (wallet == null) return;
long height = 0; long height = 0;
try { try {
height = wallet.getHeight(); // can get read timeout while syncing height = wallet.getHeight(); // can get read timeout while syncing
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); if (!isShutDownStarted) e.printStackTrace();
return; return;
} }
if (height < xmrConnectionService.getTargetHeight()) updateSyncProgress(height); if (height < xmrConnectionService.getTargetHeight()) updateSyncProgress(height);
else { else {
syncWithProgressLooper.stop(); syncProgressLooper.stop();
wasWalletSynced = true; wasWalletSynced = true;
updateSyncProgress(height); updateSyncProgress(height);
syncWithProgressLatch.countDown(); syncProgressLatch.countDown();
} }
}); });
syncWithProgressLooper.start(1000); syncProgressLooper.start(1000);
HavenoUtils.awaitLatch(syncWithProgressLatch); HavenoUtils.awaitLatch(syncProgressLatch);
wallet.stopSyncing(); wallet.stopSyncing();
if (!wasWalletSynced) throw new IllegalStateException("Failed to sync wallet with progress"); 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) { private void updateSyncProgress(long height) {
UserThread.execute(() -> { UserThread.execute(() -> {
walletHeight.set(height); walletHeight.set(height);
resetSyncProgressTimeout();
// new wallet reports height 1 before synced // new wallet reports height 1 before synced
if (height == 1) { 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) { private MoneroWalletFull createWalletFull(MoneroWalletConfig config) {
// must be connected to daemon // must be connected to daemon
@ -1545,7 +1545,7 @@ public class XmrWalletService {
// open wallet // open wallet
config.setNetworkType(getMoneroNetworkType()); config.setNetworkType(getMoneroNetworkType());
config.setServer(connection); 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); walletFull = MoneroWalletFull.openWallet(config);
if (walletFull.getDaemonConnection() != null) walletFull.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE); if (walletFull.getDaemonConnection() != null) walletFull.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE);
log.info("Done opening full wallet " + config.getPath()); log.info("Done opening full wallet " + config.getPath());
@ -1605,7 +1605,7 @@ public class XmrWalletService {
if (!applyProxyUri) connection.setProxyUri(null); if (!applyProxyUri) connection.setProxyUri(null);
// open wallet // 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); config.setServer(connection);
walletRpc.openWallet(config); walletRpc.openWallet(config);
if (walletRpc.getDaemonConnection() != null) walletRpc.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE); if (walletRpc.getDaemonConnection() != null) walletRpc.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE);
@ -1662,31 +1662,53 @@ public class XmrWalletService {
private void onConnectionChanged(MoneroRpcConnection connection) { private void onConnectionChanged(MoneroRpcConnection connection) {
synchronized (WALLET_LOCK) { synchronized (WALLET_LOCK) {
// use current connection
connection = xmrConnectionService.getConnection();
// check if ignored
if (wallet == null || isShutDownStarted) return; if (wallet == null || isShutDownStarted) return;
if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) return; if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) return;
String oldProxyUri = wallet == null || wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getProxyUri(); String oldProxyUri = wallet == null || wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getProxyUri();
String newProxyUri = connection == null ? null : connection.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 (wallet instanceof MoneroWalletRpc) {
if (StringUtils.equals(oldProxyUri, newProxyUri)) { if (StringUtils.equals(oldProxyUri, newProxyUri)) {
wallet.setDaemonConnection(connection); wallet.setDaemonConnection(connection);
} else { } 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); closeMainWallet(true);
maybeInitMainWallet(false); doMaybeInitMainWallet(false, MAX_SYNC_ATTEMPTS);
return; // wallet is re-initialized
} }
} else { } else {
wallet.setDaemonConnection(connection); wallet.setDaemonConnection(connection);
wallet.setProxyUri(connection.getProxyUri()); 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) { if (connection != null && !isShutDownStarted) {
wallet.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE); wallet.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE);
updatePollPeriod(); 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() { private void forceCloseMainWallet() {
stopPolling();
isClosingWallet = true; isClosingWallet = true;
forceCloseWallet(wallet, getWalletPath(MONERO_WALLET_NAME)); forceCloseWallet(wallet, getWalletPath(MONERO_WALLET_NAME));
stopPolling();
stopSyncWithProgress();
wallet = null; wallet = null;
} }
private void forceRestartMainWallet() { private void forceRestartMainWallet() {
log.warn("Force restarting main wallet"); log.warn("Force restarting main wallet");
forceCloseMainWallet(); forceCloseMainWallet();
synchronized (WALLET_LOCK) { maybeInitMainWallet(true);
maybeInitMainWallet(true);
}
} }
private void startPolling() { private void startPolling() {
synchronized (WALLET_LOCK) { synchronized (WALLET_LOCK) {
if (isShutDownStarted || isPollInProgress()) return; if (isShutDownStarted || isPolling()) return;
log.info("Starting to poll main wallet");
updatePollPeriod(); updatePollPeriod();
pollLooper = new TaskLooper(() -> pollWallet()); pollLooper = new TaskLooper(() -> pollWallet());
pollLooper.start(pollPeriodMs); pollLooper.start(pollPeriodMs);
@ -1761,13 +1779,13 @@ public class XmrWalletService {
} }
private void stopPolling() { private void stopPolling() {
if (isPollInProgress()) { if (isPolling()) {
pollLooper.stop(); pollLooper.stop();
pollLooper = null; pollLooper = null;
} }
} }
private boolean isPollInProgress() { private boolean isPolling() {
return pollLooper != null; return pollLooper != null;
} }
@ -1785,7 +1803,7 @@ public class XmrWalletService {
if (this.isShutDownStarted) return; if (this.isShutDownStarted) return;
if (this.pollPeriodMs != null && this.pollPeriodMs == pollPeriodMs) return; if (this.pollPeriodMs != null && this.pollPeriodMs == pollPeriodMs) return;
this.pollPeriodMs = pollPeriodMs; this.pollPeriodMs = pollPeriodMs;
if (isPollInProgress()) { if (isPolling()) {
stopPolling(); stopPolling();
startPolling(); startPolling();
} }
@ -1793,64 +1811,83 @@ public class XmrWalletService {
} }
private void pollWallet() { private void pollWallet() {
if (pollInProgress) return; synchronized (pollLock) {
if (pollInProgress) return;
}
doPollWallet(true); doPollWallet(true);
} }
private void doPollWallet(boolean updateTxs) { private void doPollWallet(boolean updateTxs) {
synchronized (pollLock) { synchronized (pollLock) {
if (isShutDownStarted) return;
pollInProgress = true; pollInProgress = true;
try { }
if (isShutDownStarted) return;
try {
// switch to best connection if daemon is too far behind // skip if daemon not synced
MoneroDaemonInfo lastInfo = xmrConnectionService.getLastInfo(); MoneroDaemonInfo lastInfo = xmrConnectionService.getLastInfo();
if (lastInfo == null) { if (lastInfo == null) {
log.warn("Last daemon info is null"); log.warn("Last daemon info is null");
return; return;
} }
if (wasWalletSynced && walletHeight.get() < xmrConnectionService.getTargetHeight() - NUM_BLOCKS_BEHIND_TOLERANCE && !Config.baseCurrencyNetwork().isTestnet()) { if (!xmrConnectionService.isSyncedWithinTolerance()) {
log.warn("Updating connection because main wallet is {} blocks behind monerod, wallet height={}, monerod height={}", xmrConnectionService.getTargetHeight() - walletHeight.get(), walletHeight.get(), lastInfo.getHeight()); log.warn("Monero daemon is not synced within tolerance, height={}, targetHeight={}", xmrConnectionService.chainHeightProperty().get(), xmrConnectionService.getTargetHeight());
xmrConnectionService.switchToBestConnection(); return;
} }
// sync wallet if behind daemon // switch to best connection if wallet is too far behind
if (walletHeight.get() < xmrConnectionService.getTargetHeight()) { if (wasWalletSynced && walletHeight.get() < xmrConnectionService.getTargetHeight() - NUM_BLOCKS_BEHIND_TOLERANCE && !Config.baseCurrencyNetwork().isTestnet()) {
synchronized (WALLET_LOCK) { // avoid long sync from blocking other operations log.warn("Updating connection because main wallet is {} blocks behind monerod, wallet height={}, monerod height={}", xmrConnectionService.getTargetHeight() - walletHeight.get(), walletHeight.get(), lastInfo.getHeight());
syncMainWallet(); if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
} }
}
// fetch transactions from pool and store to cache // sync wallet if behind daemon
// TODO: ideally wallet should sync every poll and then avoid updating from pool on fetching txs? if (walletHeight.get() < xmrConnectionService.getTargetHeight()) {
if (updateTxs) { synchronized (WALLET_LOCK) { // avoid long sync from blocking other operations
synchronized (WALLET_LOCK) { // avoid long fetch from blocking other operations syncMainWallet();
synchronized (HavenoUtils.getDaemonLock()) { }
try { }
cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true));
lastPollSuccessTimestamp = System.currentTimeMillis(); // fetch transactions from pool and store to cache
} catch (Exception e) { // fetch from pool can fail // TODO: ideally wallet should sync every poll and then avoid updating from pool on fetching txs?
if (!isShutDownStarted) { if (updateTxs) {
if (lastPollSuccessTimestamp == null || System.currentTimeMillis() - lastPollSuccessTimestamp > LOG_POLL_ERROR_AFTER_MS) { // only log if not recently successful synchronized (WALLET_LOCK) { // avoid long fetch from blocking other operations
log.warn("Error polling main wallet's transactions from the pool: {}", e.getMessage()); 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 // cache wallet info last
cacheWalletInfo(); synchronized (WALLET_LOCK) {
} catch (Exception e) { if (wallet != null && !isShutDownStarted) {
if (wallet == null || isShutDownStarted) return; try {
boolean isConnectionRefused = e.getMessage() != null && e.getMessage().contains("Connection refused"); cacheWalletInfo();
if (isConnectionRefused) forceRestartMainWallet(); } catch (Exception e) {
else if (isWalletConnectedToDaemon()) { e.printStackTrace();
log.warn("Error polling main wallet, errorMessage={}. Monerod={}", e.getMessage(), getConnectionService().getConnection()); }
//e.printStackTrace();
} }
} finally { }
synchronized (pollLock) {
pollInProgress = false; pollInProgress = false;
} }
} }
@ -1875,6 +1912,10 @@ public class XmrWalletService {
} }
} }
public boolean requestSwitchToNextBestConnection() {
return xmrConnectionService.requestSwitchToNextBestConnection();
}
private void onNewBlock(long height) { private void onNewBlock(long height) {
UserThread.execute(() -> { UserThread.execute(() -> {
walletHeight.set(height); walletHeight.set(height);

View File

@ -311,6 +311,9 @@ market.tabs.spreadCurrency=Offers by Currency
market.tabs.spreadPayment=Offers by Payment Method market.tabs.spreadPayment=Offers by Payment Method
market.tabs.trades=Trades market.tabs.trades=Trades
# OfferBookView
market.offerBook.filterPrompt=Filter
# OfferBookChartView # OfferBookChartView
market.offerBook.sellOffersHeaderLabel=Sell {0} to market.offerBook.sellOffersHeaderLabel=Sell {0} to
market.offerBook.buyOffersHeaderLabel=Buy {0} from market.offerBook.buyOffersHeaderLabel=Buy {0} from
@ -1040,11 +1043,13 @@ funds.withdrawal.inputs=Inputs selection
funds.withdrawal.useAllInputs=Use all available inputs funds.withdrawal.useAllInputs=Use all available inputs
funds.withdrawal.useCustomInputs=Use custom inputs funds.withdrawal.useCustomInputs=Use custom inputs
funds.withdrawal.receiverAmount=Receiver's amount funds.withdrawal.receiverAmount=Receiver's amount
funds.withdrawal.sendMax=Send max available
funds.withdrawal.senderAmount=Sender's amount funds.withdrawal.senderAmount=Sender's amount
funds.withdrawal.feeExcluded=Amount excludes mining fee funds.withdrawal.feeExcluded=Amount excludes mining fee
funds.withdrawal.feeIncluded=Amount includes mining fee funds.withdrawal.feeIncluded=Amount includes mining fee
funds.withdrawal.fromLabel=Withdraw from address funds.withdrawal.fromLabel=Withdraw from address
funds.withdrawal.toLabel=Withdraw to address funds.withdrawal.toLabel=Withdraw to address
funds.withdrawal.maximum=MAX
funds.withdrawal.memoLabel=Withdrawal memo funds.withdrawal.memoLabel=Withdrawal memo
funds.withdrawal.memo=Optionally fill memo funds.withdrawal.memo=Optionally fill memo
funds.withdrawal.withdrawButton=Withdraw selected funds.withdrawal.withdrawButton=Withdraw selected
@ -2140,7 +2145,7 @@ popup.warning.seed=seed
popup.warning.mandatoryUpdate.trading=Please update to the latest Haveno version. \ popup.warning.mandatoryUpdate.trading=Please update to the latest Haveno version. \
A mandatory update was released which disables trading for old versions. \ A mandatory update was released which disables trading for old versions. \
Please check out the Haveno Forum for more information. 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}. \ 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. Please wait until the mining fees are low again or until you''ve accumulated more XMR to transfer.

View File

@ -848,6 +848,7 @@ funds.withdrawal.inputs=Selección de entradas
funds.withdrawal.useAllInputs=Usar todos los entradas disponibles funds.withdrawal.useAllInputs=Usar todos los entradas disponibles
funds.withdrawal.useCustomInputs=Usar entradas personalizados funds.withdrawal.useCustomInputs=Usar entradas personalizados
funds.withdrawal.receiverAmount=Cantidad del receptor funds.withdrawal.receiverAmount=Cantidad del receptor
funds.withdrawal.sendMax=Enviar máximo disponible
funds.withdrawal.senderAmount=Cantidad del emisor funds.withdrawal.senderAmount=Cantidad del emisor
funds.withdrawal.feeExcluded=La cantidad no incluye comisión de minado funds.withdrawal.feeExcluded=La cantidad no incluye comisión de minado
funds.withdrawal.feeIncluded=La cantidad incluye comisión de minado funds.withdrawal.feeIncluded=La cantidad incluye comisión de minado

View File

@ -849,6 +849,7 @@ funds.withdrawal.inputs=Sélection de la valeur à saisir
funds.withdrawal.useAllInputs=Utiliser toutes les valeurs disponibles funds.withdrawal.useAllInputs=Utiliser toutes les valeurs disponibles
funds.withdrawal.useCustomInputs=Utiliser une valeur de saisie personnalisée funds.withdrawal.useCustomInputs=Utiliser une valeur de saisie personnalisée
funds.withdrawal.receiverAmount=Montant du destinataire funds.withdrawal.receiverAmount=Montant du destinataire
funds.withdrawal.sendMax=Envoyer max disponible
funds.withdrawal.senderAmount=Montant de l'expéditeur funds.withdrawal.senderAmount=Montant de l'expéditeur
funds.withdrawal.feeExcluded=Montant excluant les frais de minage funds.withdrawal.feeExcluded=Montant excluant les frais de minage
funds.withdrawal.feeIncluded=Montant incluant frais de minage funds.withdrawal.feeIncluded=Montant incluant frais de minage

View File

@ -847,6 +847,7 @@ funds.withdrawal.inputs=Selezione input
funds.withdrawal.useAllInputs=Utilizza tutti gli input disponibili funds.withdrawal.useAllInputs=Utilizza tutti gli input disponibili
funds.withdrawal.useCustomInputs=Utilizza input personalizzati funds.withdrawal.useCustomInputs=Utilizza input personalizzati
funds.withdrawal.receiverAmount=Importo del destinatario funds.withdrawal.receiverAmount=Importo del destinatario
funds.withdrawal.sendMax=Inviare massimo disponibile
funds.withdrawal.senderAmount=Importo del mittente funds.withdrawal.senderAmount=Importo del mittente
funds.withdrawal.feeExcluded=L'importo esclude la commissione di mining funds.withdrawal.feeExcluded=L'importo esclude la commissione di mining
funds.withdrawal.feeIncluded=L'importo include la commissione di mining funds.withdrawal.feeIncluded=L'importo include la commissione di mining

View File

@ -34,6 +34,8 @@ import haveno.proto.grpc.CreateCryptoCurrencyPaymentAccountReply;
import haveno.proto.grpc.CreateCryptoCurrencyPaymentAccountRequest; import haveno.proto.grpc.CreateCryptoCurrencyPaymentAccountRequest;
import haveno.proto.grpc.CreatePaymentAccountReply; import haveno.proto.grpc.CreatePaymentAccountReply;
import haveno.proto.grpc.CreatePaymentAccountRequest; import haveno.proto.grpc.CreatePaymentAccountRequest;
import haveno.proto.grpc.DeletePaymentAccountReply;
import haveno.proto.grpc.DeletePaymentAccountRequest;
import haveno.proto.grpc.GetCryptoCurrencyPaymentMethodsReply; import haveno.proto.grpc.GetCryptoCurrencyPaymentMethodsReply;
import haveno.proto.grpc.GetCryptoCurrencyPaymentMethodsRequest; import haveno.proto.grpc.GetCryptoCurrencyPaymentMethodsRequest;
import haveno.proto.grpc.GetPaymentAccountFormReply; import haveno.proto.grpc.GetPaymentAccountFormReply;
@ -160,6 +162,19 @@ class GrpcPaymentAccountsService extends PaymentAccountsImplBase {
} }
} }
@Override
public void deletePaymentAccount(DeletePaymentAccountRequest req,
StreamObserver<DeletePaymentAccountReply> 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 @Override
public void getCryptoCurrencyPaymentMethods(GetCryptoCurrencyPaymentMethodsRequest req, public void getCryptoCurrencyPaymentMethods(GetCryptoCurrencyPaymentMethodsRequest req,
StreamObserver<GetCryptoCurrencyPaymentMethodsReply> responseObserver) { StreamObserver<GetCryptoCurrencyPaymentMethodsReply> responseObserver) {

View File

@ -96,9 +96,8 @@ class GrpcTradesService extends TradesImplBase {
StreamObserver<GetTradeReply> responseObserver) { StreamObserver<GetTradeReply> responseObserver) {
try { try {
Trade trade = coreApi.getTrade(req.getTradeId()); Trade trade = coreApi.getTrade(req.getTradeId());
String role = coreApi.getTradeRole(req.getTradeId());
var reply = GetTradeReply.newBuilder() var reply = GetTradeReply.newBuilder()
.setTrade(toTradeInfo(trade, role).toProtoMessage()) .setTrade(toTradeInfo(trade).toProtoMessage())
.build(); .build();
responseObserver.onNext(reply); responseObserver.onNext(reply);
responseObserver.onCompleted(); 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(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(getCompleteTradeMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 3, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES));
put(getWithdrawFundsMethod().getFullMethodName(), new GrpcCallRateMeter(3, MINUTES)); put(getWithdrawFundsMethod().getFullMethodName(), new GrpcCallRateMeter(3, MINUTES));
put(getGetChatMessagesMethod().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 : 1, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES)); put(getSendChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(Config.baseCurrencyNetwork().isTestnet() ? 10 : 4, Config.baseCurrencyNetwork().isTestnet() ? SECONDS : MINUTES));
}} }}
))); )));
} }

View File

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

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}
}

View File

@ -48,11 +48,11 @@ public class AustraliaPayidForm extends PaymentMethodForm {
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"),
((AustraliaPayidAccountPayload) paymentAccountPayload).getBankAccountName()); ((AustraliaPayidAccountPayload) paymentAccountPayload).getBankAccountName());
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.payid"), addCompactTopLabelTextField(gridPane, gridRow, 1, Res.get("payment.payid"),
((AustraliaPayidAccountPayload) paymentAccountPayload).getPayid()); ((AustraliaPayidAccountPayload) paymentAccountPayload).getPayid());
AustraliaPayidAccountPayload payId = (AustraliaPayidAccountPayload) paymentAccountPayload; 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.setMinHeight(70);
textExtraInfo.setEditable(false); textExtraInfo.setEditable(false);
textExtraInfo.setText(payId.getExtraInfo()); textExtraInfo.setText(payId.getExtraInfo());

View File

@ -17,6 +17,7 @@
package haveno.desktop.components.paymentmethods; package haveno.desktop.components.paymentmethods;
import com.jfoenix.controls.JFXTextArea;
import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.locale.Res; import haveno.core.locale.Res;
import haveno.core.payment.CashAppAccount; import haveno.core.payment.CashAppAccount;
@ -29,6 +30,7 @@ import haveno.core.util.validation.InputValidator;
import haveno.desktop.components.InputTextField; import haveno.desktop.components.InputTextField;
import haveno.desktop.util.FormBuilder; import haveno.desktop.util.FormBuilder;
import haveno.desktop.util.Layout; import haveno.desktop.util.Layout;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import javafx.scene.layout.FlowPane; import javafx.scene.layout.FlowPane;
import javafx.scene.layout.GridPane; 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.addCompactTopLabelTextField;
import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon;
import static haveno.desktop.util.FormBuilder.addTopLabelFlowPane; 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 { public class CashAppForm extends PaymentMethodForm {
private final CashAppAccount cashAppAccount; private final CashAppAccount cashAppAccount;
@ -43,6 +47,13 @@ public class CashAppForm extends PaymentMethodForm {
public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) {
addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.email.mobile.cashtag"), ((CashAppAccountPayload) paymentAccountPayload).getEmailOrMobileNrOrCashtag()); 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; return gridRow;
} }
@ -66,6 +77,16 @@ public class CashAppForm extends PaymentMethodForm {
cashAppAccount.setEmailOrMobileNrOrCashtag(newValue.trim()); cashAppAccount.setEmailOrMobileNrOrCashtag(newValue.trim());
updateFromInputs(); 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); addCurrenciesGrid(true);
addLimitations(false); addLimitations(false);
addAccountNameTextFieldWithAutoFillToggleButton(); addAccountNameTextFieldWithAutoFillToggleButton();
@ -96,6 +117,12 @@ public class CashAppForm extends PaymentMethodForm {
addAccountNameTextFieldWithAutoFillToggleButton(); addAccountNameTextFieldWithAutoFillToggleButton();
TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email.mobile.cashtag"), TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email.mobile.cashtag"),
cashAppAccount.getEmailOrMobileNrOrCashtag()).second; 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); field.setMouseTransparent(false);
addLimitations(true); addLimitations(true);
addCurrenciesGrid(false); addCurrenciesGrid(false);

View File

@ -17,6 +17,7 @@
package haveno.desktop.components.paymentmethods; package haveno.desktop.components.paymentmethods;
import com.jfoenix.controls.JFXTextArea;
import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.locale.Res; import haveno.core.locale.Res;
import haveno.core.payment.PayPalAccount; import haveno.core.payment.PayPalAccount;
@ -29,13 +30,16 @@ import haveno.core.util.validation.InputValidator;
import haveno.desktop.components.InputTextField; import haveno.desktop.components.InputTextField;
import haveno.desktop.util.FormBuilder; import haveno.desktop.util.FormBuilder;
import haveno.desktop.util.Layout; import haveno.desktop.util.Layout;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
import javafx.scene.layout.FlowPane; import javafx.scene.layout.FlowPane;
import javafx.scene.layout.GridPane; 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.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.addTopLabelFlowPane;
import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField;
public class PayPalForm extends PaymentMethodForm { public class PayPalForm extends PaymentMethodForm {
private final PayPalAccount paypalAccount; private final PayPalAccount paypalAccount;
@ -43,6 +47,13 @@ public class PayPalForm extends PaymentMethodForm {
public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) {
addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.email.mobile.username"), ((PayPalAccountPayload) paymentAccountPayload).getEmailOrMobileNrOrUsername()); 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; return gridRow;
} }
@ -66,6 +77,16 @@ public class PayPalForm extends PaymentMethodForm {
paypalAccount.setEmailOrMobileNrOrUsername(newValue.trim()); paypalAccount.setEmailOrMobileNrOrUsername(newValue.trim());
updateFromInputs(); 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); addCurrenciesGrid(true);
addLimitations(false); addLimitations(false);
addAccountNameTextFieldWithAutoFillToggleButton(); addAccountNameTextFieldWithAutoFillToggleButton();
@ -99,6 +120,12 @@ public class PayPalForm extends PaymentMethodForm {
TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, TextField field = addCompactTopLabelTextField(gridPane, ++gridRow,
Res.get("payment.email.mobile.username"), Res.get("payment.email.mobile.username"),
paypalAccount.getEmailOrMobileNrOrUsername()).second; 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); field.setMouseTransparent(false);
addLimitations(true); addLimitations(true);
addCurrenciesGrid(false); addCurrenciesGrid(false);

View File

@ -112,7 +112,7 @@ public class UpholdForm extends PaymentMethodForm {
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"),
Res.get(upholdAccount.getPaymentMethod().getId())); Res.get(upholdAccount.getPaymentMethod().getId()));
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"),
Res.get(upholdAccount.getAccountOwner())); upholdAccount.getAccountOwner());
TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.uphold.accountId"), TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.uphold.accountId"),
upholdAccount.getAccountId()).second; upholdAccount.getAccountId()).second;
field.setMouseTransparent(false); field.setMouseTransparent(false);

View File

@ -108,6 +108,7 @@ class TraditionalAccountsDataModel extends ActivatableDataModel {
TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency(); TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency();
List<TradeCurrency> tradeCurrencies = paymentAccount.getTradeCurrencies(); List<TradeCurrency> tradeCurrencies = paymentAccount.getTradeCurrencies();
if (singleTradeCurrency != null) { if (singleTradeCurrency != null) {
paymentAccount.setSelectedTradeCurrency(singleTradeCurrency);
if (singleTradeCurrency instanceof TraditionalCurrency) if (singleTradeCurrency instanceof TraditionalCurrency)
preferences.addTraditionalCurrency((TraditionalCurrency) singleTradeCurrency); preferences.addTraditionalCurrency((TraditionalCurrency) singleTradeCurrency);
else else

View File

@ -35,7 +35,7 @@
package haveno.desktop.main.funds.withdrawal; package haveno.desktop.main.funds.withdrawal;
import com.google.inject.Inject; import com.google.inject.Inject;
import haveno.common.util.Tuple4; import haveno.common.util.Tuple3;
import haveno.core.locale.Res; import haveno.core.locale.Res;
import haveno.core.trade.HavenoUtils; import haveno.core.trade.HavenoUtils;
import haveno.core.trade.TradeManager; 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.ActivatableView;
import haveno.desktop.common.view.FxmlView; import haveno.desktop.common.view.FxmlView;
import haveno.desktop.components.BusyAnimation; import haveno.desktop.components.BusyAnimation;
import haveno.desktop.components.HyperlinkWithIcon;
import haveno.desktop.components.TitledGroupBg; import haveno.desktop.components.TitledGroupBg;
import haveno.desktop.main.overlays.popups.Popup; import haveno.desktop.main.overlays.popups.Popup;
import haveno.desktop.main.overlays.windows.TxDetails; import haveno.desktop.main.overlays.windows.TxDetails;
@ -60,13 +61,11 @@ import javafx.beans.value.ChangeListener;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.TextField; 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.control.Button;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import monero.common.MoneroUtils;
import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroTxConfig;
import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroTxWallet;
@ -90,7 +89,6 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
private Label amountLabel; private Label amountLabel;
private TextField amountTextField, withdrawToTextField, withdrawMemoTextField; private TextField amountTextField, withdrawToTextField, withdrawMemoTextField;
private RadioButton feeExcludedRadioButton, feeIncludedRadioButton;
private final XmrWalletService xmrWalletService; private final XmrWalletService xmrWalletService;
private final TradeManager tradeManager; private final TradeManager tradeManager;
@ -100,11 +98,8 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
private BigInteger amount = BigInteger.ZERO; private BigInteger amount = BigInteger.ZERO;
private ChangeListener<String> amountListener; private ChangeListener<String> amountListener;
private ChangeListener<Boolean> amountFocusListener; private ChangeListener<Boolean> amountFocusListener;
private ChangeListener<Toggle> feeToggleGroupListener;
private ToggleGroup feeToggleGroup;
private boolean feeExcluded;
private int rowIndex = 0; private int rowIndex = 0;
private final static int MAX_ATTEMPTS = 3; boolean sendMax = false;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle // Constructor, lifecycle
@ -141,20 +136,15 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
withdrawToTextField = addTopLabelInputTextField(gridPane, ++rowIndex, withdrawToTextField = addTopLabelInputTextField(gridPane, ++rowIndex,
Res.get("funds.withdrawal.toLabel", Res.getBaseCurrencyCode())).second; Res.get("funds.withdrawal.toLabel", Res.getBaseCurrencyCode())).second;
feeToggleGroup = new ToggleGroup(); final Tuple3<Label, TextField, HyperlinkWithIcon> feeTuple3 = FormBuilder.addTopLabelTextFieldHyperLink(gridPane, ++rowIndex, "",
final Tuple4<Label, TextField, RadioButton, RadioButton> feeTuple3 = FormBuilder.addTopLabelTextFieldRadioButtonRadioButton(gridPane, ++rowIndex, feeToggleGroup,
Res.get("funds.withdrawal.receiverAmount", Res.getBaseCurrencyCode()), Res.get("funds.withdrawal.receiverAmount", Res.getBaseCurrencyCode()),
"", Res.get("funds.withdrawal.sendMax"),
Res.get("funds.withdrawal.feeExcluded"),
Res.get("funds.withdrawal.feeIncluded"),
0); 0);
amountLabel = feeTuple3.first; amountLabel = feeTuple3.first;
amountTextField = feeTuple3.second; amountTextField = feeTuple3.second;
amountTextField.setMinWidth(180); amountTextField.setMinWidth(180);
feeExcludedRadioButton = feeTuple3.third; HyperlinkWithIcon sendMaxLink = feeTuple3.third;
feeIncludedRadioButton = feeTuple3.fourth;
withdrawMemoTextField = addTopLabelInputTextField(gridPane, ++rowIndex, withdrawMemoTextField = addTopLabelInputTextField(gridPane, ++rowIndex,
Res.get("funds.withdrawal.memoLabel", Res.getBaseCurrencyCode())).second; Res.get("funds.withdrawal.memoLabel", Res.getBaseCurrencyCode())).second;
@ -175,6 +165,12 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
}).start(); }).start();
}); });
sendMaxLink.setOnAction(event -> {
sendMax = true;
amount = null; // set amount when tx created
amountTextField.setText(Res.get("funds.withdrawal.maximum"));
});
balanceListener = new XmrBalanceListener() { balanceListener = new XmrBalanceListener() {
@Override @Override
public void onBalanceChanged(BigInteger balance) { public void onBalanceChanged(BigInteger balance) {
@ -183,6 +179,7 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
}; };
amountListener = (observable, oldValue, newValue) -> { amountListener = (observable, oldValue, newValue) -> {
if (amountTextField.focusedProperty().get()) { if (amountTextField.focusedProperty().get()) {
sendMax = false; // disable max if amount changed while focused
try { try {
amount = HavenoUtils.parseXmr(amountTextField.getText()); amount = HavenoUtils.parseXmr(amountTextField.getText());
} catch (Throwable t) { } catch (Throwable t) {
@ -191,7 +188,9 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
} }
}; };
amountFocusListener = (observable, oldValue, newValue) -> { 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) if (amount.compareTo(BigInteger.ZERO) > 0)
amountTextField.setText(HavenoUtils.formatXmr(amount)); amountTextField.setText(HavenoUtils.formatXmr(amount));
else else
@ -199,14 +198,6 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
} }
}; };
amountLabel.setText(Res.get("funds.withdrawal.receiverAmount")); 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<VBox, Void> {
amountTextField.textProperty().addListener(amountListener); amountTextField.textProperty().addListener(amountListener);
amountTextField.focusedProperty().addListener(amountFocusListener); amountTextField.focusedProperty().addListener(amountFocusListener);
xmrWalletService.addBalanceListener(balanceListener); xmrWalletService.addBalanceListener(balanceListener);
feeToggleGroup.selectedToggleProperty().addListener(feeToggleGroupListener);
if (feeToggleGroup.getSelectedToggle() == null) feeToggleGroup.selectToggle(feeExcludedRadioButton);
GUIUtil.requestFocus(withdrawToTextField); GUIUtil.requestFocus(withdrawToTextField);
} }
@ -242,7 +230,6 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
xmrWalletService.removeBalanceListener(balanceListener); xmrWalletService.removeBalanceListener(balanceListener);
amountTextField.textProperty().removeListener(amountListener); amountTextField.textProperty().removeListener(amountListener);
amountTextField.focusedProperty().removeListener(amountFocusListener); amountTextField.focusedProperty().removeListener(amountFocusListener);
feeToggleGroup.selectedToggleProperty().removeListener(feeToggleGroupListener);
} }
@ -254,15 +241,21 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
if (GUIUtil.isReadyForTxBroadcastOrShowPopup(xmrWalletService)) { if (GUIUtil.isReadyForTxBroadcastOrShowPopup(xmrWalletService)) {
try { try {
// get withdraw address // validate address
final String withdrawToAddress = withdrawToTextField.getText(); 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 // check sufficient available balance
if (amount.compareTo(BigInteger.ZERO) <= 0) throw new RuntimeException(Res.get("portfolio.pending.step5_buyer.amountTooLow")); if (amount.compareTo(BigInteger.ZERO) <= 0) throw new RuntimeException(Res.get("portfolio.pending.step5_buyer.amountTooLow"));
// create tx // create tx
MoneroTxWallet tx = null; MoneroTxWallet tx = null;
for (int i = 0; i < MAX_ATTEMPTS; i++) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
try { try {
log.info("Creating withdraw tx"); log.info("Creating withdraw tx");
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
@ -270,12 +263,14 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
.setAccountIndex(0) .setAccountIndex(0)
.setAmount(amount) .setAmount(amount)
.setAddress(withdrawToAddress) .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); log.info("Done creating withdraw tx in {} ms", System.currentTimeMillis() - startTime);
break; break;
} catch (Exception e) { } catch (Exception e) {
log.warn("Error creating creating withdraw tx, attempt={}/{}, error={}", i + 1, MAX_ATTEMPTS, e.getMessage()); if (isNotEnoughMoney(e.getMessage())) throw e;
if (i == MAX_ATTEMPTS - 1) 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 HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} }
} }
@ -283,15 +278,17 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
// popup confirmation message // popup confirmation message
popupConfirmationMessage(tx); popupConfirmationMessage(tx);
} catch (Throwable e) { } catch (Throwable e) {
if (e.getMessage().contains("enough")) new Popup().warning(Res.get("funds.withdrawal.warn.amountExceeds")).show(); e.printStackTrace();
else { if (isNotEnoughMoney(e.getMessage())) new Popup().warning(Res.get("funds.withdrawal.notEnoughFunds")).show();
e.printStackTrace(); else new Popup().warning(e.getMessage()).show();
new Popup().warning(e.getMessage()).show();
}
} }
} }
} }
private static boolean isNotEnoughMoney(String errorMsg) {
return errorMsg.contains("not enough");
}
private void popupConfirmationMessage(MoneroTxWallet tx) { private void popupConfirmationMessage(MoneroTxWallet tx) {
// create confirmation message // create confirmation message
@ -347,6 +344,7 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
private void reset() { private void reset() {
sendMax = false;
amount = BigInteger.ZERO; amount = BigInteger.ZERO;
amountTextField.setText(""); amountTextField.setText("");
amountTextField.setPromptText(Res.get("funds.withdrawal.setAmount")); amountTextField.setPromptText(Res.get("funds.withdrawal.setAmount"));

View File

@ -466,11 +466,6 @@ public abstract class MutableOfferDataModel extends OfferDataModel {
} }
} }
public boolean hasAvailableSplitOutput() {
BigInteger reserveAmount = totalToPay.get();
return openOfferManager.hasAvailableOutput(reserveAmount);
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Utils // Utils
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -690,9 +690,11 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
}; };
errorMessageListener = (o, oldValue, newValue) -> { 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())) 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(); paymentAccountsComboBoxSelectionHandler = e -> onPaymentAccountsComboBoxSelected();

View File

@ -109,6 +109,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
private String addressAsString; private String addressAsString;
private final String paymentLabel; private final String paymentLabel;
private boolean createOfferRequested; private boolean createOfferRequested;
public boolean createOfferCanceled;
public final StringProperty amount = new SimpleStringProperty(); public final StringProperty amount = new SimpleStringProperty();
public final StringProperty minAmount = new SimpleStringProperty(); public final StringProperty minAmount = new SimpleStringProperty();
@ -608,6 +609,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
void onPlaceOffer(Offer offer, Runnable resultHandler) { void onPlaceOffer(Offer offer, Runnable resultHandler) {
errorMessage.set(null); errorMessage.set(null);
createOfferRequested = true; createOfferRequested = true;
createOfferCanceled = false;
dataModel.onPlaceOffer(offer, transaction -> { dataModel.onPlaceOffer(offer, transaction -> {
resultHandler.run(); resultHandler.run();
@ -631,6 +633,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
public void onCancelOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { public void onCancelOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
createOfferRequested = false; createOfferRequested = false;
createOfferCanceled = true;
OpenOfferManager openOfferManager = HavenoUtils.openOfferManager; OpenOfferManager openOfferManager = HavenoUtils.openOfferManager;
Optional<OpenOffer> openOffer = openOfferManager.getOpenOfferById(offer.getId()); Optional<OpenOffer> openOffer = openOfferManager.getOpenOfferById(offer.getId());
if (openOffer.isPresent()) { if (openOffer.isPresent()) {

View File

@ -46,6 +46,7 @@ import haveno.desktop.components.AutoTooltipButton;
import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.AutoTooltipLabel;
import haveno.desktop.components.AutoTooltipSlideToggleButton; import haveno.desktop.components.AutoTooltipSlideToggleButton;
import haveno.desktop.components.AutoTooltipTableColumn; import haveno.desktop.components.AutoTooltipTableColumn;
import haveno.desktop.components.AutoTooltipTextField;
import haveno.desktop.components.AutocompleteComboBox; import haveno.desktop.components.AutocompleteComboBox;
import haveno.desktop.components.ColoredDecimalPlacesWithZerosText; import haveno.desktop.components.ColoredDecimalPlacesWithZerosText;
import haveno.desktop.components.HyperlinkWithIcon; import haveno.desktop.components.HyperlinkWithIcon;
@ -107,6 +108,7 @@ import java.util.Map;
import java.util.Optional; import java.util.Optional;
import static haveno.desktop.util.FormBuilder.addTitledGroupBg; import static haveno.desktop.util.FormBuilder.addTitledGroupBg;
import static haveno.desktop.util.FormBuilder.addTopLabelAutoToolTipTextField;
abstract public class OfferBookView<R extends GridPane, M extends OfferBookViewModel> extends ActivatableViewAndModel<R, M> { abstract public class OfferBookView<R extends GridPane, M extends OfferBookViewModel> extends ActivatableViewAndModel<R, M> {
@ -122,6 +124,7 @@ abstract public class OfferBookView<R extends GridPane, M extends OfferBookViewM
protected AutocompleteComboBox<TradeCurrency> currencyComboBox; protected AutocompleteComboBox<TradeCurrency> currencyComboBox;
private AutocompleteComboBox<PaymentMethod> paymentMethodComboBox; private AutocompleteComboBox<PaymentMethod> paymentMethodComboBox;
private AutoTooltipButton createOfferButton; private AutoTooltipButton createOfferButton;
private AutoTooltipTextField filterInputField;
private AutoTooltipSlideToggleButton matchingOffersToggle; private AutoTooltipSlideToggleButton matchingOffersToggle;
private AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> amountColumn; private AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> amountColumn;
private AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> volumeColumn; private AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> volumeColumn;
@ -185,13 +188,13 @@ abstract public class OfferBookView<R extends GridPane, M extends OfferBookViewM
Res.get("offerbook.filterByCurrency")); Res.get("offerbook.filterByCurrency"));
currencyComboBoxContainer = currencyBoxTuple.first; currencyComboBoxContainer = currencyBoxTuple.first;
currencyComboBox = currencyBoxTuple.third; currencyComboBox = currencyBoxTuple.third;
currencyComboBox.setPrefWidth(270); currencyComboBox.setPrefWidth(250);
Tuple3<VBox, Label, AutocompleteComboBox<PaymentMethod>> paymentBoxTuple = FormBuilder.addTopLabelAutocompleteComboBox( Tuple3<VBox, Label, AutocompleteComboBox<PaymentMethod>> paymentBoxTuple = FormBuilder.addTopLabelAutocompleteComboBox(
Res.get("offerbook.filterByPaymentMethod")); Res.get("offerbook.filterByPaymentMethod"));
paymentMethodComboBox = paymentBoxTuple.third; paymentMethodComboBox = paymentBoxTuple.third;
paymentMethodComboBox.setCellFactory(GUIUtil.getPaymentMethodCellFactory()); paymentMethodComboBox.setCellFactory(GUIUtil.getPaymentMethodCellFactory());
paymentMethodComboBox.setPrefWidth(270); paymentMethodComboBox.setPrefWidth(250);
matchingOffersToggle = new AutoTooltipSlideToggleButton(); matchingOffersToggle = new AutoTooltipSlideToggleButton();
matchingOffersToggle.setText(Res.get("offerbook.matchingOffers")); matchingOffersToggle.setText(Res.get("offerbook.matchingOffers"));
@ -212,8 +215,13 @@ abstract public class OfferBookView<R extends GridPane, M extends OfferBookViewM
var createOfferButtonStack = new StackPane(createOfferButton, disabledCreateOfferButtonTooltip); var createOfferButtonStack = new StackPane(createOfferButton, disabledCreateOfferButtonTooltip);
Tuple3<VBox, Label, AutoTooltipTextField> autoToolTipTextField = addTopLabelAutoToolTipTextField("");
VBox filterBox = autoToolTipTextField.first;
filterInputField = autoToolTipTextField.third;
filterInputField.setPromptText(Res.get("market.offerBook.filterPrompt"));
offerToolsBox.getChildren().addAll(currencyBoxTuple.first, paymentBoxTuple.first, offerToolsBox.getChildren().addAll(currencyBoxTuple.first, paymentBoxTuple.first,
matchingOffersToggle, getSpacer(), createOfferButtonStack); filterBox, matchingOffersToggle, getSpacer(), createOfferButtonStack);
GridPane.setHgrow(offerToolsBox, Priority.ALWAYS); GridPane.setHgrow(offerToolsBox, Priority.ALWAYS);
GridPane.setRowIndex(offerToolsBox, gridRow); GridPane.setRowIndex(offerToolsBox, gridRow);
@ -427,6 +435,10 @@ abstract public class OfferBookView<R extends GridPane, M extends OfferBookViewM
nrOfOffersLabel.setText(Res.get("offerbook.nrOffers", model.getOfferList().size())); nrOfOffersLabel.setText(Res.get("offerbook.nrOffers", model.getOfferList().size()));
model.priceFeedService.updateCounterProperty().addListener(priceFeedUpdateCounterListener); model.priceFeedService.updateCounterProperty().addListener(priceFeedUpdateCounterListener);
filterInputField.setOnKeyTyped(event -> {
model.onFilterKeyTyped(filterInputField.getText());
});
} }
private void updatePaymentMethodComboBoxEditor() { private void updatePaymentMethodComboBoxEditor() {

View File

@ -121,6 +121,7 @@ abstract class OfferBookViewModel extends ActivatableViewModel {
PaymentMethod selectedPaymentMethod = getShowAllEntryForPaymentMethod(); PaymentMethod selectedPaymentMethod = getShowAllEntryForPaymentMethod();
boolean isTabSelected; boolean isTabSelected;
String filterText = "";
final BooleanProperty showAllTradeCurrenciesProperty = new SimpleBooleanProperty(true); final BooleanProperty showAllTradeCurrenciesProperty = new SimpleBooleanProperty(true);
final BooleanProperty disableMatchToggle = new SimpleBooleanProperty(); final BooleanProperty disableMatchToggle = new SimpleBooleanProperty();
final IntegerProperty maxPlacesForAmount = new SimpleIntegerProperty(); 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); abstract void saveSelectedCurrencyCodeInPreferences(OfferDirection direction, String code);
protected void onSetPaymentMethod(PaymentMethod paymentMethod) { protected void onSetPaymentMethod(PaymentMethod paymentMethod) {
@ -566,7 +572,25 @@ abstract class OfferBookViewModel extends ActivatableViewModel {
Predicate<OfferBookListItem> predicate = useOffersMatchingMyAccountsFilter ? Predicate<OfferBookListItem> predicate = useOffersMatchingMyAccountsFilter ?
getCurrencyAndMethodPredicate(direction, selectedTradeCurrency).and(getOffersMatchingMyAccountsPredicate()) : getCurrencyAndMethodPredicate(direction, selectedTradeCurrency).and(getOffersMatchingMyAccountsPredicate()) :
getCurrencyAndMethodPredicate(direction, selectedTradeCurrency); getCurrencyAndMethodPredicate(direction, selectedTradeCurrency);
filteredItems.setPredicate(predicate);
if (!filterText.isEmpty()) {
// filter node address
Predicate<OfferBookListItem> 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<OfferBookListItem> getCurrencyAndMethodPredicate(OfferDirection direction, abstract Predicate<OfferBookListItem> getCurrencyAndMethodPredicate(OfferDirection direction,

View File

@ -158,7 +158,8 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
private int gridRow = 0; private int gridRow = 0;
private final HashMap<String, Boolean> paymentAccountWarningDisplayed = new HashMap<>(); private final HashMap<String, Boolean> paymentAccountWarningDisplayed = new HashMap<>();
private boolean offerDetailsWindowDisplayed, zelleWarningDisplayed, fasterPaymentsWarningDisplayed, private boolean offerDetailsWindowDisplayed, zelleWarningDisplayed, fasterPaymentsWarningDisplayed,
takeOfferFromUnsignedAccountWarningDisplayed, payByMailWarningDisplayed, cashAtAtmWarningDisplayed, australiaPayidWarningDisplayed; takeOfferFromUnsignedAccountWarningDisplayed, payByMailWarningDisplayed, cashAtAtmWarningDisplayed,
australiaPayidWarningDisplayed, paypalWarningDisplayed, cashAppWarningDisplayed;
private SimpleBooleanProperty errorPopupDisplayed; private SimpleBooleanProperty errorPopupDisplayed;
private ChangeListener<Boolean> amountFocusedListener, getShowWalletFundedNotificationListener; private ChangeListener<Boolean> amountFocusedListener, getShowWalletFundedNotificationListener;
@ -268,6 +269,8 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
maybeShowPayByMailWarning(lastPaymentAccount, model.dataModel.getOffer()); maybeShowPayByMailWarning(lastPaymentAccount, model.dataModel.getOffer());
maybeShowCashAtAtmWarning(lastPaymentAccount, model.dataModel.getOffer()); maybeShowCashAtAtmWarning(lastPaymentAccount, model.dataModel.getOffer());
maybeShowAustraliaPayidWarning(lastPaymentAccount, model.dataModel.getOffer()); maybeShowAustraliaPayidWarning(lastPaymentAccount, model.dataModel.getOffer());
maybeShowPayPalWarning(lastPaymentAccount, model.dataModel.getOffer());
maybeShowCashAppWarning(lastPaymentAccount, model.dataModel.getOffer());
if (!model.isRange()) { if (!model.isRange()) {
nextButton.setVisible(false); nextButton.setVisible(false);
@ -1157,6 +1160,40 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
} }
} }
private void maybeShowPayPalWarning(PaymentAccount paymentAccount, Offer offer) {
if (paymentAccount.getPaymentMethod().getId().equals(PaymentMethod.PAYPAL_ID) &&
!paypalWarningDisplayed && !offer.getExtraInfo().isEmpty()) {
paypalWarningDisplayed = 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 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<Label, VBox> getTradeInputBox(HBox amountValueBox, String promptText) { private Tuple2<Label, VBox> getTradeInputBox(HBox amountValueBox, String promptText) {
Label descriptionLabel = new AutoTooltipLabel(promptText); Label descriptionLabel = new AutoTooltipLabel(promptText);
descriptionLabel.setId("input-description-label"); descriptionLabel.setId("input-description-label");

View File

@ -174,7 +174,11 @@ public class OfferDetailsWindow extends Overlay<OfferDetailsWindow> {
List<String> acceptedCountryCodes = offer.getAcceptedCountryCodes(); List<String> acceptedCountryCodes = offer.getAcceptedCountryCodes();
boolean showAcceptedCountryCodes = acceptedCountryCodes != null && !acceptedCountryCodes.isEmpty(); boolean showAcceptedCountryCodes = acceptedCountryCodes != null && !acceptedCountryCodes.isEmpty();
boolean isF2F = offer.getPaymentMethod().equals(PaymentMethod.F2F); 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()) if (!takeOfferHandlerOptional.isPresent())
rows++; rows++;
if (showAcceptedBanks) if (showAcceptedBanks)

View File

@ -296,7 +296,7 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
private void updateSelectToggleButtonState() { private void updateSelectToggleButtonState() {
List<OpenOfferListItem> availableItems = sortedList.stream() List<OpenOfferListItem> availableItems = sortedList.stream()
.filter(openOfferListItem -> !openOfferListItem.getOpenOffer().isScheduled()) .filter(openOfferListItem -> !openOfferListItem.getOpenOffer().isPending())
.collect(Collectors.toList()); .collect(Collectors.toList());
if (availableItems.size() == 0) { if (availableItems.size() == 0) {
selectToggleButton.setDisable(true); selectToggleButton.setDisable(true);
@ -710,7 +710,7 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
offerStateChangeListeners.put(openOffer.getId(), listener); offerStateChangeListeners.put(openOffer.getId(), listener);
openOffer.stateProperty().addListener(listener); openOffer.stateProperty().addListener(listener);
if (openOffer.getState() == OpenOffer.State.SCHEDULED) { if (openOffer.getState() == OpenOffer.State.PENDING) {
setGraphic(new AutoTooltipLabel(Res.get("shared.pending"))); setGraphic(new AutoTooltipLabel(Res.get("shared.pending")));
return; return;
} }

View File

@ -210,6 +210,8 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
sellerState.set(UNDEFINED); sellerState.set(UNDEFINED);
buyerState.set(BuyerState.UNDEFINED); buyerState.set(BuyerState.UNDEFINED);
onTradeStateChanged(trade.getState()); onTradeStateChanged(trade.getState());
if (trade.isPayoutPublished()) onPayoutStateChanged(trade.getPayoutState()); // TODO: payout state takes precedence in case PaymentReceivedMessage not processed
else onTradeStateChanged(trade.getState());
}); });
} }

View File

@ -450,7 +450,7 @@ public class SellerStep3View extends TradeStepView {
model.dataModel.onPaymentReceived(() -> { model.dataModel.onPaymentReceived(() -> {
}, errorMessage -> { }, errorMessage -> {
busyAnimation.stop(); 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()); confirmButton.setDisable(!confirmPaymentReceivedPermitted());
UserThread.execute(() -> statusLabel.setText("Error confirming payment received.")); UserThread.execute(() -> statusLabel.setText("Error confirming payment received."));
}); });

View File

@ -107,7 +107,7 @@ public class NetworkSettingsView extends ActivatableView<GridPane, Void> {
private final ClockWatcher clockWatcher; private final ClockWatcher clockWatcher;
private final WalletsSetup walletsSetup; private final WalletsSetup walletsSetup;
private final P2PService p2PService; private final P2PService p2PService;
private final XmrConnectionService connectionManager; private final XmrConnectionService connectionService;
private final ObservableList<P2pNetworkListItem> p2pNetworkListItems = FXCollections.observableArrayList(); private final ObservableList<P2pNetworkListItem> p2pNetworkListItems = FXCollections.observableArrayList();
private final SortedList<P2pNetworkListItem> p2pSortedList = new SortedList<>(p2pNetworkListItems); private final SortedList<P2pNetworkListItem> p2pSortedList = new SortedList<>(p2pNetworkListItems);
@ -131,7 +131,7 @@ public class NetworkSettingsView extends ActivatableView<GridPane, Void> {
@Inject @Inject
public NetworkSettingsView(WalletsSetup walletsSetup, public NetworkSettingsView(WalletsSetup walletsSetup,
P2PService p2PService, P2PService p2PService,
XmrConnectionService connectionManager, XmrConnectionService connectionService,
Preferences preferences, Preferences preferences,
XmrNodes xmrNodes, XmrNodes xmrNodes,
FilterManager filterManager, FilterManager filterManager,
@ -141,7 +141,7 @@ public class NetworkSettingsView extends ActivatableView<GridPane, Void> {
super(); super();
this.walletsSetup = walletsSetup; this.walletsSetup = walletsSetup;
this.p2PService = p2PService; this.p2PService = p2PService;
this.connectionManager = connectionManager; this.connectionService = connectionService;
this.preferences = preferences; this.preferences = preferences;
this.xmrNodes = xmrNodes; this.xmrNodes = xmrNodes;
this.filterManager = filterManager; this.filterManager = filterManager;
@ -303,10 +303,10 @@ public class NetworkSettingsView extends ActivatableView<GridPane, Void> {
rescanOutputsButton.setOnAction(event -> GUIUtil.rescanOutputs(preferences)); rescanOutputsButton.setOnAction(event -> GUIUtil.rescanOutputs(preferences));
moneroPeersSubscription = EasyBind.subscribe(connectionManager.peerConnectionsProperty(), moneroPeersSubscription = EasyBind.subscribe(connectionService.peerConnectionsProperty(),
this::updateMoneroPeersTable); this::updateMoneroPeersTable);
moneroBlockHeightSubscription = EasyBind.subscribe(connectionManager.chainHeightProperty(), moneroBlockHeightSubscription = EasyBind.subscribe(connectionService.chainHeightProperty(),
this::updateChainHeightTextField); this::updateChainHeightTextField);
nodeAddressSubscription = EasyBind.subscribe(p2PService.getNetworkNode().nodeAddressProperty(), nodeAddressSubscription = EasyBind.subscribe(p2PService.getNetworkNode().nodeAddressProperty(),
@ -503,6 +503,7 @@ public class NetworkSettingsView extends ActivatableView<GridPane, Void> {
} }
private void updateP2PTable() { private void updateP2PTable() {
if (connectionService.isShutDownStarted()) return; // ignore if shutting down
p2pPeersTableView.getItems().forEach(P2pNetworkListItem::cleanup); p2pPeersTableView.getItems().forEach(P2pNetworkListItem::cleanup);
p2pNetworkListItems.clear(); p2pNetworkListItems.clear();
p2pNetworkListItems.setAll(p2PService.getNetworkNode().getAllConnections().stream() p2pNetworkListItems.setAll(p2PService.getNetworkNode().getAllConnections().stream()

View File

@ -36,6 +36,7 @@ import haveno.desktop.components.AutoTooltipCheckBox;
import haveno.desktop.components.AutoTooltipLabel; import haveno.desktop.components.AutoTooltipLabel;
import haveno.desktop.components.AutoTooltipRadioButton; import haveno.desktop.components.AutoTooltipRadioButton;
import haveno.desktop.components.AutoTooltipSlideToggleButton; import haveno.desktop.components.AutoTooltipSlideToggleButton;
import haveno.desktop.components.AutoTooltipTextField;
import haveno.desktop.components.AutocompleteComboBox; import haveno.desktop.components.AutocompleteComboBox;
import haveno.desktop.components.BalanceTextField; import haveno.desktop.components.BalanceTextField;
import haveno.desktop.components.BusyAnimation; import haveno.desktop.components.BusyAnimation;
@ -1159,35 +1160,28 @@ public class FormBuilder {
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Label + TextField + RadioButton + RadioButton // Label + TextField + HyperlinkWithIcon
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
public static Tuple4<Label, TextField, RadioButton, RadioButton> addTopLabelTextFieldRadioButtonRadioButton(GridPane gridPane, public static Tuple3<Label, TextField, HyperlinkWithIcon> addTopLabelTextFieldHyperLink(GridPane gridPane,
int rowIndex, int rowIndex,
ToggleGroup toggleGroup, String title,
String title, String textFieldTitle,
String textFieldTitle, String maxButtonTitle,
String radioButtonTitle1, double top) {
String radioButtonTitle2,
double top) {
TextField textField = new HavenoTextField(); TextField textField = new HavenoTextField();
textField.setPromptText(textFieldTitle); textField.setPromptText(textFieldTitle);
RadioButton radioButton1 = new AutoTooltipRadioButton(radioButtonTitle1); HyperlinkWithIcon maxLink = new ExternalHyperlink(maxButtonTitle);
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));
HBox hBox = new HBox(); HBox hBox = new HBox();
hBox.setSpacing(10); hBox.setSpacing(10);
hBox.getChildren().addAll(textField, radioButton1, radioButton2); hBox.getChildren().addAll(textField, maxLink);
hBox.setAlignment(Pos.CENTER_LEFT);
final Tuple2<Label, VBox> labelVBoxTuple2 = addTopLabelWithVBox(gridPane, rowIndex, title, hBox, top); final Tuple2<Label, VBox> 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); return new Tuple3<>(vBox, label, comboBox);
} }
public static Tuple3<VBox, Label, AutoTooltipTextField> addTopLabelAutoToolTipTextField(String title) {
return addTopLabelAutoToolTipTextField(title, 0);
}
public static Tuple3<VBox, Label, AutoTooltipTextField> 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 @NotNull
private static VBox getTopLabelVBox(int top) { private static VBox getTopLabelVBox(int top) {
VBox vBox = new VBox(); VBox vBox = new VBox();

View File

@ -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). 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 ## 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 <your fork url> git clone <your fork url>
@ -57,9 +83,12 @@ For each seed node:
1. [Build the Haveno repository](#fork-and-build-haveno). 1. [Build the Haveno repository](#fork-and-build-haveno).
2. [Start a local Monero node](#start-a-local-monero-node). 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. 3. Modify `./scripts/deployment/haveno-seednode.service` and `./scripts/deployment/haveno-seednode2.service` as needed.
4. The node will print its onion address to the console. Record the onion address in `core/src/main/resources/xmr_<network>.seednodes`. Be careful to record full addresses correctly. 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. Update all seed nodes, arbitrators, and user applications for the change to take effect. 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_<network>.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. 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 ## Other tips
* If a dispute does not open properly, try manually reopening the dispute with a keyboard shortcut: `ctrl + o`. * 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. * 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.

View File

@ -170,6 +170,11 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
private final Capabilities capabilities = new Capabilities(); 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 // Constructor
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -606,35 +611,55 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
*/ */
public boolean reportInvalidRequest(RuleViolation ruleViolation) { 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; int numRuleViolations;
numRuleViolations = ruleViolations.getOrDefault(ruleViolation, 0); numRuleViolations = connection.ruleViolations.getOrDefault(ruleViolation, 0);
numRuleViolations++; numRuleViolations++;
ruleViolations.put(ruleViolation, numRuleViolations); connection.ruleViolations.put(ruleViolation, numRuleViolations);
if (numRuleViolations >= ruleViolation.maxTolerance) { 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={} " + "ruleViolations={} " +
"connection with address{} and uid {}", ruleViolations, peersNodeAddressProperty, uid); "connection with address {} and uid {}", connection.ruleViolations, connection.peersNodeAddressProperty, connection.uid);
this.ruleViolation = ruleViolation; connection.ruleViolation = ruleViolation;
if (ruleViolation == RuleViolation.PEER_BANNED) { if (ruleViolation == RuleViolation.PEER_BANNED) {
log.debug("We close connection due RuleViolation.PEER_BANNED. peersNodeAddress={}", getPeersNodeAddressOptional()); if (logReport) log.debug("We close connection due RuleViolation.PEER_BANNED. peersNodeAddress={}", connection.getPeersNodeAddressOptional());
shutDown(CloseConnectionReason.PEER_BANNED); connection.shutDown(CloseConnectionReason.PEER_BANNED);
} else if (ruleViolation == RuleViolation.INVALID_CLASS) { } else if (ruleViolation == RuleViolation.INVALID_CLASS) {
log.warn("We close connection due RuleViolation.INVALID_CLASS"); if (logReport) log.warn("We close connection due RuleViolation.INVALID_CLASS");
shutDown(CloseConnectionReason.INVALID_CLASS_RECEIVED); connection.shutDown(CloseConnectionReason.INVALID_CLASS_RECEIVED);
} else { } else {
log.warn("We close connection due RuleViolation.RULE_VIOLATION"); if (logReport) log.warn("We close connection due RuleViolation.RULE_VIOLATION");
shutDown(CloseConnectionReason.RULE_VIOLATION); connection.shutDown(CloseConnectionReason.RULE_VIOLATION);
} }
resetReportedInvalidRequestsThrottle(logReport);
return true; return true;
} else { } else {
resetReportedInvalidRequestsThrottle(logReport);
return false; 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) { private void handleException(Throwable e) {
CloseConnectionReason closeConnectionReason; CloseConnectionReason closeConnectionReason;

View File

@ -38,6 +38,7 @@ import com.google.common.util.concurrent.SettableFuture;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import lombok.Getter;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
@ -82,7 +83,8 @@ public abstract class NetworkNode implements MessageListener {
private final ListeningExecutorService sendMessageExecutor; private final ListeningExecutorService sendMessageExecutor;
private Server server; private Server server;
private volatile boolean shutDownInProgress; @Getter
private volatile boolean isShutDownStarted;
// accessed from different threads // accessed from different threads
private final CopyOnWriteArraySet<OutboundConnection> outBoundConnections = new CopyOnWriteArraySet<>(); private final CopyOnWriteArraySet<OutboundConnection> outBoundConnections = new CopyOnWriteArraySet<>();
protected final ObjectProperty<NodeAddress> nodeAddressProperty = new SimpleObjectProperty<>(); protected final ObjectProperty<NodeAddress> nodeAddressProperty = new SimpleObjectProperty<>();
@ -181,7 +183,7 @@ public abstract class NetworkNode implements MessageListener {
try { try {
socket.close(); socket.close();
} catch (Throwable throwable) { } catch (Throwable throwable) {
if (!shutDownInProgress) { if (!isShutDownStarted) {
log.error("Error at closing socket " + throwable); log.error("Error at closing socket " + throwable);
} }
} }
@ -362,8 +364,8 @@ public abstract class NetworkNode implements MessageListener {
public void shutDown(Runnable shutDownCompleteHandler) { public void shutDown(Runnable shutDownCompleteHandler) {
log.info("NetworkNode shutdown started"); log.info("NetworkNode shutdown started");
if (!shutDownInProgress) { if (!isShutDownStarted) {
shutDownInProgress = true; isShutDownStarted = true;
if (server != null) { if (server != null) {
server.shutDown(); server.shutDown();
server = null; server = null;

View File

@ -352,7 +352,13 @@ public class BroadcastHandler implements PeerManager.Listener {
sendMessageFutures.stream() sendMessageFutures.stream()
.filter(future -> !future.isCancelled() && !future.isDone()) .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(); sendMessageFutures.clear();
peerManager.removeListener(this); peerManager.removeListener(this);

View File

@ -140,33 +140,38 @@ class RequestDataHandler implements MessageListener {
getDataRequestType = getDataRequest.getClass().getSimpleName(); getDataRequestType = getDataRequest.getClass().getSimpleName();
log.info("\n\n>> We send a {} to peer {}\n", getDataRequestType, nodeAddress); log.info("\n\n>> We send a {} to peer {}\n", getDataRequestType, nodeAddress);
networkNode.addMessageListener(this); networkNode.addMessageListener(this);
SettableFuture<Connection> 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 try {
public void onFailure(@NotNull Throwable throwable) { SettableFuture<Connection> future = networkNode.sendMessage(nodeAddress, getDataRequest);
if (!stopped) { //noinspection UnstableApiUsage
String errorMessage = "Sending getDataRequest to " + nodeAddress + Futures.addCallback(future, new FutureCallback<>() {
" failed. That is expected if the peer is offline.\n\t" + @Override
"getDataRequest=" + getDataRequest + "." + public void onSuccess(Connection connection) {
"\n\tException=" + throwable.getMessage(); if (!stopped) {
handleFault(errorMessage, nodeAddress, CloseConnectionReason.SEND_MSG_FAILURE); log.trace("Send {} to {} succeeded.", getDataRequest, nodeAddress);
} else { } else {
log.trace("We have stopped already. We ignore that networkNode.sendMessage.onFailure call. " + log.trace("We have stopped already. We ignore that networkNode.sendMessage.onSuccess call." +
"Might be caused by a previous timeout."); "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 { } else {
log.warn("We have stopped already. We ignore that requestData call."); log.warn("We have stopped already. We ignore that requestData call.");
} }

View File

@ -115,35 +115,39 @@ class PeerExchangeHandler implements MessageListener {
TIMEOUT, TimeUnit.SECONDS); TIMEOUT, TimeUnit.SECONDS);
} }
SettableFuture<Connection> future = networkNode.sendMessage(nodeAddress, getPeersRequest); try {
Futures.addCallback(future, new FutureCallback<Connection>() { SettableFuture<Connection> future = networkNode.sendMessage(nodeAddress, getPeersRequest);
@Override Futures.addCallback(future, new FutureCallback<Connection>() {
public void onSuccess(Connection connection) { @Override
if (!stopped) { public void onSuccess(Connection connection) {
//TODO if (!stopped) {
/*if (!connection.getPeersNodeAddressOptional().isPresent()) { //TODO
connection.setPeersNodeAddress(nodeAddress); /*if (!connection.getPeersNodeAddressOptional().isPresent()) {
log.warn("sendGetPeersRequest: !connection.getPeersNodeAddressOptional().isPresent()"); connection.setPeersNodeAddress(nodeAddress);
}*/ log.warn("sendGetPeersRequest: !connection.getPeersNodeAddressOptional().isPresent()");
}*/
PeerExchangeHandler.this.connection = connection;
connection.addMessageListener(PeerExchangeHandler.this); PeerExchangeHandler.this.connection = connection;
} else { connection.addMessageListener(PeerExchangeHandler.this);
log.trace("We have stopped that handler already. We ignore that sendGetPeersRequest.onSuccess call."); } else {
log.trace("We have stopped that handler already. We ignore that sendGetPeersRequest.onSuccess call.");
}
} }
}
@Override
@Override public void onFailure(@NotNull Throwable throwable) {
public void onFailure(@NotNull Throwable throwable) { if (!stopped) {
if (!stopped) { String errorMessage = "Sending getPeersRequest to " + nodeAddress +
String errorMessage = "Sending getPeersRequest to " + nodeAddress + " failed. That is expected if the peer is offline. Exception=" + throwable.getMessage();
" failed. That is expected if the peer is offline. Exception=" + throwable.getMessage(); handleFault(errorMessage, CloseConnectionReason.SEND_MSG_FAILURE, nodeAddress);
handleFault(errorMessage, CloseConnectionReason.SEND_MSG_FAILURE, nodeAddress); } else {
} else { log.trace("We have stopped that handler already. We ignore that sendGetPeersRequest.onFailure call.");
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 { } else {
log.debug("My node address is still null at sendGetPeersRequest. We ignore that call."); log.debug("My node address is still null at sendGetPeersRequest. We ignore that call.");
} }

View File

@ -586,6 +586,8 @@ service PaymentAccounts {
} }
rpc CreateCryptoCurrencyPaymentAccount (CreateCryptoCurrencyPaymentAccountRequest) returns (CreateCryptoCurrencyPaymentAccountReply) { rpc CreateCryptoCurrencyPaymentAccount (CreateCryptoCurrencyPaymentAccountRequest) returns (CreateCryptoCurrencyPaymentAccountReply) {
} }
rpc DeletePaymentAccount (DeletePaymentAccountRequest) returns (DeletePaymentAccountReply) {
}
rpc GetCryptoCurrencyPaymentMethods (GetCryptoCurrencyPaymentMethodsRequest) returns (GetCryptoCurrencyPaymentMethodsReply) { rpc GetCryptoCurrencyPaymentMethods (GetCryptoCurrencyPaymentMethodsRequest) returns (GetCryptoCurrencyPaymentMethodsReply) {
} }
rpc ValidateFormField (ValidateFormFieldRequest) returns (ValidateFormFieldReply) { rpc ValidateFormField (ValidateFormFieldRequest) returns (ValidateFormFieldReply) {
@ -639,6 +641,13 @@ message CreateCryptoCurrencyPaymentAccountRequest {
bool trade_instant = 4; bool trade_instant = 4;
} }
message DeletePaymentAccountRequest {
string payment_account_id = 1;
}
message DeletePaymentAccountReply {
}
message CreateCryptoCurrencyPaymentAccountReply { message CreateCryptoCurrencyPaymentAccountReply {
PaymentAccount payment_account = 1; PaymentAccount payment_account = 1;
} }

View File

@ -1061,6 +1061,7 @@ message UpholdAccountPayload {
message CashAppAccountPayload { message CashAppAccountPayload {
string email_or_mobile_nr_or_cashtag = 1; string email_or_mobile_nr_or_cashtag = 1;
string extra_info = 2;
} }
message MoneyBeamAccountPayload { message MoneyBeamAccountPayload {
@ -1072,6 +1073,7 @@ message VenmoAccountPayload {
} }
message PayPalAccountPayload { message PayPalAccountPayload {
string email_or_mobile_nr_or_username = 1; string email_or_mobile_nr_or_username = 1;
string extra_info = 2;
} }
message PopmoneyAccountPayload { message PopmoneyAccountPayload {
@ -1391,7 +1393,7 @@ message SignedOffer {
message OpenOffer { message OpenOffer {
enum State { enum State {
PB_ERROR = 0; PB_ERROR = 0;
SCHEDULED = 1; PENDING = 1;
AVAILABLE = 2; AVAILABLE = 2;
RESERVED = 3; RESERVED = 3;
CLOSED = 4; CLOSED = 4;

View File

@ -1,3 +1,2 @@
HiddenServiceDir build/tor-hidden-service HiddenServiceDir build/tor-hidden-service
HiddenServicePort 80 127.0.0.1:8080 HiddenServicePort 80 127.0.0.1:8080
HiddenServicePoWDefensesEnabled 1

View File

@ -13,7 +13,7 @@ ExecStart=/bin/sh /home/haveno/haveno/haveno-seednode --baseCurrencyNetwork=XMR_
--nodePort=2002\ --nodePort=2002\
--appName=haveno-XMR_STAGENET_Seed_2002\ --appName=haveno-XMR_STAGENET_Seed_2002\
# --logLevel=trace\ # --logLevel=trace\
--xmrNode=http://127.0.0.1:38088\ --xmrNode=http://[::1]:38088\
--xmrNodeUsername=admin\ --xmrNodeUsername=admin\
--xmrNodePassword=password --xmrNodePassword=password
@ -33,4 +33,4 @@ RestrictSUIDSGID=true
LimitRSS=2000000000 LimitRSS=2000000000
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@ -13,7 +13,7 @@ ExecStart=/bin/sh /home/haveno/haveno/haveno-seednode --baseCurrencyNetwork=XMR_
--nodePort=3003\ --nodePort=3003\
--appName=haveno-XMR_STAGENET_Seed_3003\ --appName=haveno-XMR_STAGENET_Seed_3003\
# --logLevel=trace\ # --logLevel=trace\
--xmrNode=http://127.0.0.1:38088\ --xmrNode=http://[::1]:38088\
--xmrNodeUsername=admin\ --xmrNodeUsername=admin\
--xmrNodePassword=password --xmrNodePassword=password
@ -33,4 +33,4 @@ RestrictSUIDSGID=true
LimitRSS=2000000000 LimitRSS=2000000000
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@ -13,8 +13,9 @@ ExecStart=/bin/sh $PATH/haveno-seednode --baseCurrencyNetwork=XMR_STAGENET\
--useDevPrivilegeKeys=false\ --useDevPrivilegeKeys=false\
--nodePort=2002\ --nodePort=2002\
--appName=haveno-XMR_STAGENET_Seed_2002 --appName=haveno-XMR_STAGENET_Seed_2002
--xmrNode=[::1]:38088
ExecStop=/bin/kill ${MAINPID} ExecStop=/bin/kill ${MAINPID} ; sleep 5
Restart=always Restart=always
# Hardening # Hardening

View File

@ -5,7 +5,7 @@
JAVA_HOME=/usr/lib/jvm/openjdk-21 JAVA_HOME=/usr/lib/jvm/openjdk-21
# java memory and remote management options # 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) # use external tor (change to -1 for internal tor binary)
HAVENO_EXTERNAL_TOR_PORT=9051 HAVENO_EXTERNAL_TOR_PORT=9051

View File

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