update to v1.0.9

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

View File

@ -40,24 +40,9 @@ deploy:
screen -S localnet -X screen -t $$target; \
screen -S localnet -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -37,16 +37,20 @@ import haveno.core.monetary.Volume;
import static haveno.core.offer.OfferPayload.ACCOUNT_AGE_WITNESS_HASH;
import static haveno.core.offer.OfferPayload.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());
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -141,6 +141,7 @@ public abstract class Trade implements Tradable, Model {
private static final long SHUTDOWN_TIMEOUT_MS = 60000;
private static final long 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());
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -86,11 +86,13 @@ public class XmrNodes {
new XmrNode(MoneroNodesOption.PROVIDED, null, null, "127.0.0.1", 18081, 1, "@local"),
new XmrNode(MoneroNodesOption.PROVIDED, null, null, "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(".")) {

View File

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

View File

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

View File

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

View File

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

View File

@ -849,6 +849,7 @@ funds.withdrawal.inputs=Sélection de la valeur à saisir
funds.withdrawal.useAllInputs=Utiliser toutes les valeurs disponibles
funds.withdrawal.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

View File

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

View File

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

View File

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

View File

@ -5,10 +5,10 @@
<!-- See: https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -->
<key>CFBundleVersion</key>
<string>1.0.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>

View File

@ -0,0 +1,45 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.skins.JFXTextFieldSkin;
import javafx.scene.control.Skin;
import javafx.scene.control.TextField;
public class AutoTooltipTextField extends JFXTextField {
public AutoTooltipTextField() {
super();
}
public AutoTooltipTextField(String text) {
super(text);
}
@Override
protected Skin<?> createDefaultSkin() {
return new AutoTooltipTextFieldSkin(this);
}
private class AutoTooltipTextFieldSkin extends JFXTextFieldSkin {
public AutoTooltipTextFieldSkin(TextField textField) {
super(textField);
}
}
}

View File

@ -48,11 +48,11 @@ public class AustraliaPayidForm extends PaymentMethodForm {
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"),
((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());

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,9 +22,35 @@ Arbitrators can be started in a Screen session and then detached to run in the b
Some good hints about how to secure a VPS are in [Monero's meta repository](https://github.com/monero-project/meta/blob/master/SERVER_SETUP_HARDENING.md).
## 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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,7 +13,7 @@ ExecStart=/bin/sh /home/haveno/haveno/haveno-seednode --baseCurrencyNetwork=XMR_
--nodePort=2002\
--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

View File

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

View File

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

View File

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

View File

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