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