refactor syncing wallet with progress and switching connections

This commit is contained in:
woodser 2024-08-04 18:03:17 -04:00
parent dbb3d4f891
commit 1b5c03bce8
12 changed files with 240 additions and 141 deletions

View file

@ -178,7 +178,7 @@ class CoreWalletsService {
verifyWalletsAreAvailable(); verifyWalletsAreAvailable();
verifyEncryptedWalletIsUnlocked(); verifyEncryptedWalletIsUnlocked();
try { try {
return xmrWalletService.getWallet().relayTx(metadata); return xmrWalletService.relayTx(metadata);
} catch (Exception ex) { } catch (Exception ex) {
log.error("", ex); log.error("", ex);
throw new IllegalStateException(ex); throw new IllegalStateException(ex);

View file

@ -273,7 +273,11 @@ public final class XmrConnectionService {
} }
public synchronized boolean requestSwitchToNextBestConnection() { public synchronized boolean requestSwitchToNextBestConnection() {
log.warn("Requesting switch to next best monerod, current monerod={}", getConnection() == null ? null : getConnection().getUri()); return requestSwitchToNextBestConnection(null);
}
public synchronized boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) {
log.warn("Requesting switch to next best monerod, source monerod={}", sourceConnection == null ? getConnection() == null ? null : getConnection().getUri() : sourceConnection.getUri());
// skip if shut down started // skip if shut down started
if (isShutDownStarted) { if (isShutDownStarted) {
@ -281,9 +285,15 @@ public final class XmrConnectionService {
return false; return false;
} }
// skip if connection is already switched
if (sourceConnection != null && sourceConnection != getConnection()) {
log.warn("Skipping switch to next best Monero connection because source connection is not current connection");
return false;
}
// skip if connection is fixed // skip if connection is fixed
if (isFixedConnection() || !connectionManager.getAutoSwitch()) { if (isFixedConnection() || !connectionManager.getAutoSwitch()) {
log.info("Skipping switch to next best Monero connection because connection is fixed or auto switch is disabled"); log.warn("Skipping switch to next best Monero connection because connection is fixed or auto switch is disabled");
return false; return false;
} }

View file

@ -133,6 +133,22 @@ public class WalletAppSetup {
String result; String result;
if (exception == null && errorMsg == null) { if (exception == null && errorMsg == null) {
// update daemon sync progress
double chainDownloadPercentageD = xmrConnectionService.downloadPercentageProperty().doubleValue();
Long bestChainHeight = xmrConnectionService.chainHeightProperty().get();
String chainHeightAsString = bestChainHeight != null && bestChainHeight > 0 ? String.valueOf(bestChainHeight) : "";
if (chainDownloadPercentageD < 1) {
xmrDaemonSyncProgress.set(chainDownloadPercentageD);
if (chainDownloadPercentageD > 0.0) {
String synchronizingWith = Res.get("mainView.footer.xmrInfo.synchronizingWith", getXmrDaemonNetworkAsString(), chainHeightAsString, FormattingUtils.formatToRoundedPercentWithSymbol(chainDownloadPercentageD));
result = Res.get("mainView.footer.xmrInfo", synchronizingWith, "");
} else {
result = Res.get("mainView.footer.xmrInfo",
Res.get("mainView.footer.xmrInfo.connectingTo"),
getXmrDaemonNetworkAsString());
}
} else {
// update wallet sync progress // update wallet sync progress
double walletDownloadPercentageD = (double) walletDownloadPercentage; double walletDownloadPercentageD = (double) walletDownloadPercentage;
xmrWalletSyncProgress.set(walletDownloadPercentageD); xmrWalletSyncProgress.set(walletDownloadPercentageD);
@ -144,29 +160,15 @@ public class WalletAppSetup {
result = Res.get("mainView.footer.xmrInfo", synchronizedWith, feeInfo); result = Res.get("mainView.footer.xmrInfo", synchronizedWith, feeInfo);
getXmrSplashSyncIconId().set("image-connection-synced"); getXmrSplashSyncIconId().set("image-connection-synced");
downloadCompleteHandler.run(); downloadCompleteHandler.run();
} else if (walletDownloadPercentageD > 0) { } else if (walletDownloadPercentageD >= 0) {
String synchronizingWith = Res.get("mainView.footer.xmrInfo.synchronizingWalletWith", getXmrWalletNetworkAsString(), walletHeightAsString, FormattingUtils.formatToRoundedPercentWithSymbol(walletDownloadPercentageD)); String synchronizingWith = Res.get("mainView.footer.xmrInfo.synchronizingWalletWith", getXmrWalletNetworkAsString(), walletHeightAsString, FormattingUtils.formatToRoundedPercentWithSymbol(walletDownloadPercentageD));
result = Res.get("mainView.footer.xmrInfo", synchronizingWith, ""); result = Res.get("mainView.footer.xmrInfo", synchronizingWith, "");
getXmrSplashSyncIconId().set(""); // clear synced icon getXmrSplashSyncIconId().set(""); // clear synced icon
} else { } else {
// update daemon sync progress
double chainDownloadPercentageD = xmrConnectionService.downloadPercentageProperty().doubleValue();
xmrDaemonSyncProgress.set(chainDownloadPercentageD);
Long bestChainHeight = xmrConnectionService.chainHeightProperty().get();
String chainHeightAsString = bestChainHeight != null && bestChainHeight > 0 ? String.valueOf(bestChainHeight) : "";
if (chainDownloadPercentageD == 1) {
String synchronizedWith = Res.get("mainView.footer.xmrInfo.connectedTo", getXmrDaemonNetworkAsString(), chainHeightAsString); String synchronizedWith = Res.get("mainView.footer.xmrInfo.connectedTo", getXmrDaemonNetworkAsString(), chainHeightAsString);
String feeInfo = ""; // TODO: feeService.isFeeAvailable() returns true, disable String feeInfo = ""; // TODO: feeService.isFeeAvailable() returns true, disable
result = Res.get("mainView.footer.xmrInfo", synchronizedWith, feeInfo); result = Res.get("mainView.footer.xmrInfo", synchronizedWith, feeInfo);
getXmrSplashSyncIconId().set("image-connection-synced"); getXmrSplashSyncIconId().set("image-connection-synced");
} else if (chainDownloadPercentageD > 0.0) {
String synchronizingWith = Res.get("mainView.footer.xmrInfo.synchronizingWith", getXmrDaemonNetworkAsString(), chainHeightAsString, FormattingUtils.formatToRoundedPercentWithSymbol(chainDownloadPercentageD));
result = Res.get("mainView.footer.xmrInfo", synchronizingWith, "");
} else {
result = Res.get("mainView.footer.xmrInfo",
Res.get("mainView.footer.xmrInfo.connectingTo"),
getXmrDaemonNetworkAsString());
} }
} }
} else { } else {

View file

@ -110,6 +110,7 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import lombok.Getter; import lombok.Getter;
import monero.common.MoneroRpcConnection;
import monero.daemon.model.MoneroKeyImageSpentStatus; import monero.daemon.model.MoneroKeyImageSpentStatus;
import monero.daemon.model.MoneroTx; import monero.daemon.model.MoneroTx;
import monero.wallet.model.MoneroIncomingTransfer; import monero.wallet.model.MoneroIncomingTransfer;
@ -1089,6 +1090,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
synchronized (HavenoUtils.getWalletFunctionLock()) { synchronized (HavenoUtils.getWalletFunctionLock()) {
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try { try {
log.info("Creating split output tx to fund offer {} at subaddress {}", openOffer.getShortId(), entry.getSubaddressIndex()); log.info("Creating split output tx to fund offer {} at subaddress {}", openOffer.getShortId(), entry.getSubaddressIndex());
splitOutputTx = xmrWalletService.createTx(new MoneroTxConfig() splitOutputTx = xmrWalletService.createTx(new MoneroTxConfig()
@ -1101,8 +1103,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
} catch (Exception e) { } catch (Exception e) {
if (e.getMessage().contains("not enough")) throw e; // do not retry if not enough funds 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()); 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());
xmrWalletService.handleWalletError(e, sourceConnection);
if (stopped || i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; if (stopped || i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
if (xmrConnectionService.isConnected()) xmrWalletService.requestSwitchToNextBestConnection();
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} }
} }

View file

@ -32,6 +32,7 @@ import haveno.core.trade.protocol.TradeProtocol;
import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.model.XmrAddressEntry;
import haveno.core.xmr.wallet.XmrWalletService; import haveno.core.xmr.wallet.XmrWalletService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroRpcConnection;
import monero.daemon.model.MoneroOutput; import monero.daemon.model.MoneroOutput;
import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroTxWallet;
@ -82,14 +83,16 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
try { try {
synchronized (HavenoUtils.getWalletFunctionLock()) { synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = model.getXmrWalletService().getConnectionService().getConnection();
try { try {
//if (true) throw new RuntimeException("Pretend error"); //if (true) throw new RuntimeException("Pretend error");
reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex); reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex);
} catch (Exception e) { } catch (Exception e) {
log.warn("Error creating reserve tx, offerId={}, attempt={}/{}, 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());
model.getXmrWalletService().handleWalletError(e, sourceConnection);
verifyPending();
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
model.getProtocol().startTimeoutTimer(); // reset protocol timeout model.getProtocol().startTimeoutTimer(); // reset protocol timeout
if (model.getXmrWalletService().getConnectionService().isConnected()) model.getXmrWalletService().requestSwitchToNextBestConnection();
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} }
@ -129,7 +132,11 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
} }
} }
public void verifyPending() { private boolean isPending() {
if (!model.getOpenOffer().isPending()) throw new RuntimeException("Offer " + model.getOpenOffer().getOffer().getId() + " is canceled"); return model.getOpenOffer().isPending();
}
private void verifyPending() {
if (!isPending()) throw new RuntimeException("Offer " + model.getOpenOffer().getOffer().getId() + " is canceled");
} }
} }

View file

@ -78,6 +78,7 @@ import haveno.network.p2p.P2PService;
import haveno.network.p2p.network.Connection; import haveno.network.p2p.network.Connection;
import haveno.network.p2p.network.MessageListener; import haveno.network.p2p.network.MessageListener;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroRpcConnection;
import monero.wallet.MoneroWallet; import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroDestination; import monero.wallet.model.MoneroDestination;
import monero.wallet.model.MoneroMultisigSignResult; import monero.wallet.model.MoneroMultisigSignResult;
@ -501,6 +502,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
// submit fully signed payout tx to the network // submit fully signed payout tx to the network
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try { try {
List<String> txHashes = multisigWallet.submitMultisigTxHex(disputeTxSet.getMultisigTxHex()); List<String> txHashes = multisigWallet.submitMultisigTxHex(disputeTxSet.getMultisigTxHex());
disputeTxSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed disputeTxSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed
@ -509,7 +511,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
if (trade.isPayoutPublished()) throw new IllegalStateException("Payout tx already published for " + trade.getClass().getSimpleName() + " " + trade.getShortId()); if (trade.isPayoutPublished()) throw new IllegalStateException("Payout tx already published for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
log.warn("Failed to submit dispute payout tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); log.warn("Failed to submit dispute payout tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
if (trade.getXmrConnectionService().isConnected()) trade.requestSwitchToNextBestConnection(); if (trade.getXmrConnectionService().isConnected()) trade.requestSwitchToNextBestConnection(sourceConnection);
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} }
} }

View file

@ -506,4 +506,16 @@ public class HavenoUtils {
public static void setTopError(String msg) { public static void setTopError(String msg) {
havenoSetup.getTopErrorMsg().set(msg); havenoSetup.getTopErrorMsg().set(msg);
} }
public static boolean isConnectionRefused(Exception e) {
return e != null && e.getMessage().contains("Connection refused");
}
public static boolean isReadTimeout(Exception e) {
return e != null && e.getMessage().contains("Read timed out");
}
public static boolean isUnresponsive(Exception e) {
return isConnectionRefused(e) || isReadTimeout(e);
}
} }

View file

@ -850,8 +850,8 @@ public abstract class Trade implements Tradable, Model {
} }
} }
public boolean requestSwitchToNextBestConnection() { public boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) {
if (xmrConnectionService.requestSwitchToNextBestConnection()) { if (xmrConnectionService.requestSwitchToNextBestConnection(sourceConnection)) {
onConnectionChanged(xmrConnectionService.getConnection()); // change connection on same thread onConnectionChanged(xmrConnectionService.getConnection()); // change connection on same thread
return true; return true;
} }
@ -895,10 +895,6 @@ public abstract class Trade implements Tradable, Model {
}).start(); }).start();
} }
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? // 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?
private boolean isInvalidImportError(String errMsg) { private boolean isInvalidImportError(String errMsg) {
return errMsg.contains("Failed to parse hex") || errMsg.contains("Multisig info is for a different account"); return errMsg.contains("Failed to parse hex") || errMsg.contains("Multisig info is for a different account");
@ -1081,6 +1077,7 @@ public abstract class Trade implements Tradable, Model {
synchronized (walletLock) { synchronized (walletLock) {
synchronized (HavenoUtils.getDaemonLock()) { // lock on daemon because import calls full refresh synchronized (HavenoUtils.getDaemonLock()) { // lock on daemon because import calls full refresh
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try { try {
doImportMultisigHex(); doImportMultisigHex();
break; break;
@ -1088,9 +1085,8 @@ public abstract class Trade implements Tradable, Model {
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
log.warn("Failed to import multisig hex, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); log.warn("Failed to import multisig hex, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
handleWalletError(e, sourceConnection);
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; 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 HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} }
} }
@ -1168,6 +1164,12 @@ public abstract class Trade implements Tradable, Model {
log.info("Done importing multisig hexes for {} {} in {} ms, count={}", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime, multisigHexes.size()); log.info("Done importing multisig hexes for {} {} in {} ms, count={}", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime, multisigHexes.size());
} }
private void handleWalletError(Exception e, MoneroRpcConnection sourceConnection) {
if (HavenoUtils.isUnresponsive(e)) forceCloseWallet(); // wallet can be stuck a while
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection(sourceConnection);
getWallet(); // re-open wallet
}
private String getMultisigHexRole(String multisigHex) { private String getMultisigHexRole(String multisigHex) {
if (multisigHex.equals(getArbitrator().getUpdatedMultisigHex())) return "arbitrator"; if (multisigHex.equals(getArbitrator().getUpdatedMultisigHex())) return "arbitrator";
if (multisigHex.equals(getBuyer().getUpdatedMultisigHex())) return "buyer"; if (multisigHex.equals(getBuyer().getUpdatedMultisigHex())) return "buyer";
@ -1189,14 +1191,15 @@ public abstract class Trade implements Tradable, Model {
synchronized (walletLock) { synchronized (walletLock) {
synchronized (HavenoUtils.getWalletFunctionLock()) { synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try { try {
return doCreatePayoutTx(); return doCreatePayoutTx();
} catch (IllegalArgumentException | IllegalStateException e) { } catch (IllegalArgumentException | IllegalStateException e) {
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
log.warn("Failed to create payout tx, tradeId={}, attempt={}/{}, 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());
handleWalletError(e, sourceConnection);
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} }
} }
@ -1246,6 +1249,7 @@ public abstract class Trade implements Tradable, Model {
synchronized (walletLock) { synchronized (walletLock) {
synchronized (HavenoUtils.getWalletFunctionLock()) { synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try { try {
if (wallet.isMultisigImportNeeded()) throw new IllegalStateException("Cannot create dispute payout tx because multisig import is needed for " + getClass().getSimpleName() + " " + getShortId()); if (wallet.isMultisigImportNeeded()) throw new IllegalStateException("Cannot create dispute payout tx because multisig import is needed for " + getClass().getSimpleName() + " " + getShortId());
return createTx(txConfig); return createTx(txConfig);
@ -1254,8 +1258,8 @@ public abstract class Trade implements Tradable, Model {
} catch (Exception e) { } catch (Exception e) {
if (e.getMessage().contains("not possible")) throw new IllegalArgumentException("Loser payout is too small to cover the mining fee"); if (e.getMessage().contains("not possible")) throw new IllegalArgumentException("Loser payout is too small to cover the mining fee");
log.warn("Failed to create dispute payout tx, tradeId={}, attempt={}/{}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage()); log.warn("Failed to create dispute payout tx, tradeId={}, attempt={}/{}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage());
handleWalletError(e, sourceConnection);
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} }
} }
@ -1275,6 +1279,7 @@ public abstract class Trade implements Tradable, Model {
synchronized (walletLock) { synchronized (walletLock) {
synchronized (HavenoUtils.getWalletFunctionLock()) { synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try { try {
doProcessPayoutTx(payoutTxHex, sign, publish); doProcessPayoutTx(payoutTxHex, sign, publish);
break; break;
@ -1282,8 +1287,8 @@ public abstract class Trade implements Tradable, Model {
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
log.warn("Failed to process payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage()); log.warn("Failed to process payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage());
handleWalletError(e, sourceConnection);
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} finally { } finally {
requestSaveWallet(); requestSaveWallet();
@ -2412,6 +2417,7 @@ public abstract class Trade implements Tradable, Model {
} }
private void syncWallet(boolean pollWallet) { private void syncWallet(boolean pollWallet) {
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try { try {
if (getWallet() == null) throw new RuntimeException("Cannot sync trade wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId()); 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 (getWallet().getDaemonConnection() == null) throw new RuntimeException("Cannot sync trade wallet because it's not connected to a Monero daemon for " + getClass().getSimpleName() + ", " + getId());
@ -2432,7 +2438,7 @@ public abstract class Trade implements Tradable, Model {
if (pollWallet) pollWallet(); if (pollWallet) pollWallet();
} catch (Exception e) { } catch (Exception e) {
ThreadUtils.execute(() -> requestSwitchToNextBestConnection(), getId()); ThreadUtils.execute(() -> requestSwitchToNextBestConnection(sourceConnection), getId());
throw e; throw e;
} }
} }
@ -2561,11 +2567,12 @@ public abstract class Trade implements Tradable, Model {
// rescan spent outputs to detect unconfirmed payout tx // rescan spent outputs to detect unconfirmed payout tx
if (isPayoutExpected && wallet.getBalance().compareTo(BigInteger.ZERO) > 0) { if (isPayoutExpected && wallet.getBalance().compareTo(BigInteger.ZERO) > 0) {
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try { try {
wallet.rescanSpent(); wallet.rescanSpent();
} catch (Exception e) { } catch (Exception e) {
log.warn("Failed to rescan spent outputs for {} {}, errorMessage={}", getClass().getSimpleName(), getShortId(), e.getMessage()); log.warn("Failed to rescan spent outputs for {} {}, errorMessage={}", getClass().getSimpleName(), getShortId(), e.getMessage());
ThreadUtils.execute(() -> requestSwitchToNextBestConnection(), getId()); // do not block polling thread ThreadUtils.execute(() -> requestSwitchToNextBestConnection(sourceConnection), getId()); // do not block polling thread
} }
} }
@ -2610,12 +2617,11 @@ public abstract class Trade implements Tradable, Model {
} }
} }
} catch (Exception e) { } catch (Exception e) {
boolean isConnectionRefused = e.getMessage() != null && e.getMessage().contains("Connection refused"); if (HavenoUtils.isUnresponsive(e)) forceRestartTradeWallet();
if (isConnectionRefused) forceRestartTradeWallet();
else { else {
boolean isWalletConnected = isWalletConnectedToDaemon(); boolean isWalletConnected = isWalletConnectedToDaemon();
if (wallet != null && !isShutDownStarted && isWalletConnected) { if (wallet != null && !isShutDownStarted && isWalletConnected) {
log.warn("Error polling trade wallet for {} {}, errorMessage={}. Monerod={}", getClass().getSimpleName(), getShortId(), e.getMessage(), getXmrWalletService().getConnectionService().getConnection()); log.warn("Error polling trade wallet for {} {}, errorMessage={}. Monerod={}", getClass().getSimpleName(), getShortId(), e.getMessage(), wallet.getDaemonConnection());
//e.printStackTrace(); //e.printStackTrace();
} }
} }
@ -2675,7 +2681,8 @@ public abstract class Trade implements Tradable, Model {
log.warn("Rescanning blockchain for {} {}", getClass().getSimpleName(), getShortId()); log.warn("Rescanning blockchain for {} {}", getClass().getSimpleName(), getShortId());
wallet.rescanBlockchain(); wallet.rescanBlockchain();
} catch (Exception e) { } catch (Exception e) {
if (isReadTimeoutError(e.getMessage())) forceRestartTradeWallet(); // wallet can be stuck a while log.warn("Error rescanning blockchain for {} {}, errorMessage={}", getClass().getSimpleName(), getShortId(), e.getMessage());
if (HavenoUtils.isUnresponsive(e)) forceRestartTradeWallet(); // wallet can be stuck a while
throw e; throw e;
} finally { } finally {

View file

@ -31,6 +31,7 @@ import haveno.core.xmr.model.XmrAddressEntry;
import haveno.core.xmr.wallet.XmrWalletService; import haveno.core.xmr.wallet.XmrWalletService;
import haveno.network.p2p.SendDirectMessageListener; import haveno.network.p2p.SendDirectMessageListener;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroRpcConnection;
import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroTxWallet;
import java.math.BigInteger; import java.math.BigInteger;
@ -100,12 +101,14 @@ public class MaybeSendSignContractRequest extends TradeTask {
try { try {
synchronized (HavenoUtils.getWalletFunctionLock()) { synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection();
try { try {
depositTx = trade.getXmrWalletService().createDepositTx(trade, reserveExactAmount, subaddressIndex); depositTx = trade.getXmrWalletService().createDepositTx(trade, reserveExactAmount, subaddressIndex);
} catch (Exception e) { } catch (Exception e) {
log.warn("Error creating deposit tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage()); log.warn("Error creating deposit tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage());
trade.getXmrWalletService().handleWalletError(e, sourceConnection);
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating deposit tx, tradeId=" + trade.getShortId());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
if (trade.getXmrConnectionService().isConnected()) trade.getXmrWalletService().requestSwitchToNextBestConnection();
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} }

View file

@ -26,6 +26,7 @@ import haveno.core.trade.protocol.TradeProtocol;
import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.model.XmrAddressEntry;
import haveno.core.xmr.wallet.XmrWalletService; import haveno.core.xmr.wallet.XmrWalletService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroRpcConnection;
import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroTxWallet;
import java.math.BigInteger; import java.math.BigInteger;
@ -66,12 +67,14 @@ public class TakerReserveTradeFunds extends TradeTask {
try { try {
synchronized (HavenoUtils.getWalletFunctionLock()) { synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection();
try { try {
reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, takerFee, sendAmount, securityDeposit, returnAddress, false, null); reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, takerFee, sendAmount, securityDeposit, returnAddress, false, null);
} catch (Exception e) { } catch (Exception e) {
log.warn("Error creating reserve tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage()); log.warn("Error creating reserve tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage());
trade.getXmrWalletService().handleWalletError(e, sourceConnection);
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
if (trade.getXmrConnectionService().isConnected()) trade.getXmrWalletService().requestSwitchToNextBestConnection();
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} }

View file

@ -160,9 +160,11 @@ public class XmrWalletService {
private boolean isClosingWallet; private boolean isClosingWallet;
private boolean isShutDownStarted; private boolean isShutDownStarted;
private ExecutorService syncWalletThreadPool = Executors.newFixedThreadPool(10); // TODO: adjust based on connection type private ExecutorService syncWalletThreadPool = Executors.newFixedThreadPool(10); // TODO: adjust based on connection type
private boolean isSyncingWithProgress;
private Long syncStartHeight; private Long syncStartHeight;
private TaskLooper syncProgressLooper; private TaskLooper syncProgressLooper;
private CountDownLatch syncProgressLatch; private CountDownLatch syncProgressLatch;
private Exception syncProgressError;
private Timer syncProgressTimeout; private Timer syncProgressTimeout;
private static final int SYNC_PROGRESS_TIMEOUT_SECONDS = 60; private static final int SYNC_PROGRESS_TIMEOUT_SECONDS = 60;
@ -178,7 +180,9 @@ public class XmrWalletService {
private List<MoneroSubaddress> cachedSubaddresses; private List<MoneroSubaddress> cachedSubaddresses;
private List<MoneroOutputWallet> cachedOutputs; private List<MoneroOutputWallet> cachedOutputs;
private List<MoneroTxWallet> cachedTxs; private List<MoneroTxWallet> cachedTxs;
private boolean runReconnectTestOnStartup = false; // test reconnecting on startup while syncing so the wallet is blocked private boolean testReconnectOnStartup = false; // test reconnecting on startup while syncing so the wallet is blocked
private String testReconnectMonerod1 = "http://node.community.rino.io:18081";
private String testReconnectMonerod2 = "http://nodex.monerujo.io:18081";
@SuppressWarnings("unused") @SuppressWarnings("unused")
@Inject @Inject
@ -473,6 +477,14 @@ public class XmrWalletService {
} }
} }
public String relayTx(String metadata) {
synchronized (WALLET_LOCK) {
String txId = wallet.relayTx(metadata);
requestSaveMainWallet();
return txId;
}
}
public MoneroTxWallet createTx(List<MoneroDestination> destinations) { public MoneroTxWallet createTx(List<MoneroDestination> destinations) {
MoneroTxWallet tx = createTx(new MoneroTxConfig().setAccountIndex(0).setDestinations(destinations).setRelay(false).setCanSplit(false)); MoneroTxWallet tx = createTx(new MoneroTxConfig().setAccountIndex(0).setDestinations(destinations).setRelay(false).setCanSplit(false));
//printTxs("XmrWalletService.createTx", tx); //printTxs("XmrWalletService.createTx", tx);
@ -1289,9 +1301,19 @@ public class XmrWalletService {
else log.info(appliedMsg); else log.info(appliedMsg);
// listen for connection changes // listen for connection changes
xmrConnectionService.addConnectionListener(connection -> ThreadUtils.execute(() -> { xmrConnectionService.addConnectionListener(connection -> {
if (wasWalletSynced && !isSyncingWithProgress) {
ThreadUtils.execute(() -> {
onConnectionChanged(connection); onConnectionChanged(connection);
}, THREAD_ID)); }, THREAD_ID);
} else {
// force restart main wallet if connection changed while syncing
log.warn("Force restarting main wallet because connection changed while syncing");
forceRestartMainWallet();
return;
}
});
// initialize main wallet when daemon synced // initialize main wallet when daemon synced
walletInitListener = (obs, oldVal, newVal) -> initMainWalletIfConnected(); walletInitListener = (obs, oldVal, newVal) -> initMainWalletIfConnected();
@ -1340,6 +1362,7 @@ public class XmrWalletService {
long date = localDateTime.toEpochSecond(ZoneOffset.UTC); long date = localDateTime.toEpochSecond(ZoneOffset.UTC);
user.setWalletCreationDate(date); user.setWalletCreationDate(date);
} }
walletHeight.set(wallet.getHeight());
isClosingWallet = false; isClosingWallet = false;
} }
@ -1348,6 +1371,7 @@ public class XmrWalletService {
log.info("Monero wallet path={}", wallet.getPath()); log.info("Monero wallet path={}", wallet.getPath());
// sync main wallet if applicable // sync main wallet if applicable
// TODO: error handling and re-initialization is jenky, refactor
if (sync && numAttempts > 0) { if (sync && numAttempts > 0) {
try { try {
@ -1360,7 +1384,16 @@ public class XmrWalletService {
// sync main wallet // sync main wallet
log.info("Syncing main wallet"); log.info("Syncing main wallet");
long time = System.currentTimeMillis(); long time = System.currentTimeMillis();
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try {
syncWithProgress(); // blocking syncWithProgress(); // blocking
} catch (Exception e) {
log.warn("Error syncing wallet with progress on startup: " + e.getMessage());
forceCloseMainWallet();
requestSwitchToNextBestConnection(sourceConnection);
maybeInitMainWallet(true, numAttempts - 1); // re-initialize wallet and sync again
return;
}
log.info("Done syncing main wallet in " + (System.currentTimeMillis() - time) + " ms"); log.info("Done syncing main wallet in " + (System.currentTimeMillis() - time) + " ms");
// poll wallet // poll wallet
@ -1435,69 +1468,83 @@ public class XmrWalletService {
} }
private void syncWithProgress() { private void syncWithProgress() {
synchronized (WALLET_LOCK) {
// start sync progress timeout // set initial state
resetSyncProgressTimeout(); isSyncingWithProgress = true;
syncProgressError = null;
// show sync progress updateSyncProgress(walletHeight.get());
updateSyncProgress(wallet.getHeight());
// test connection changing on startup before wallet synced // test connection changing on startup before wallet synced
if (runReconnectTestOnStartup) { if (testReconnectOnStartup) {
UserThread.runAfter(() -> { UserThread.runAfter(() -> {
log.warn("Testing connection change on startup before wallet synced"); log.warn("Testing connection change on startup before wallet synced");
xmrConnectionService.setConnection("http://node.community.rino.io:18081"); // TODO: needs to be online if (xmrConnectionService.getConnection().getUri().equals(testReconnectMonerod1)) xmrConnectionService.setConnection(testReconnectMonerod2);
else xmrConnectionService.setConnection(testReconnectMonerod1);
}, 1); }, 1);
runReconnectTestOnStartup = false; // only run once testReconnectOnStartup = false; // only run once
} }
// get sync notifications from native wallet // native wallet provides sync notifications
if (wallet instanceof MoneroWalletFull) { if (wallet instanceof MoneroWalletFull) {
if (runReconnectTestOnStartup) HavenoUtils.waitFor(1000); // delay sync to test if (testReconnectOnStartup) HavenoUtils.waitFor(1000); // delay sync to test
wallet.sync(new MoneroWalletListener() { wallet.sync(new MoneroWalletListener() {
@Override @Override
public void onSyncProgress(long height, long startHeight, long endHeight, double percentDone, String message) { public void onSyncProgress(long height, long startHeight, long endHeight, double percentDone, String message) {
updateSyncProgress(height); updateSyncProgress(height);
} }
}); });
wasWalletSynced = true; setWalletSyncedWithProgress();
return; return;
} }
// poll wallet for progress // start polling wallet for progress
wallet.startSyncing(xmrConnectionService.getRefreshPeriodMs());
syncProgressLatch = new CountDownLatch(1); syncProgressLatch = new CountDownLatch(1);
syncProgressLooper = new TaskLooper(() -> { syncProgressLooper = new TaskLooper(() -> {
if (wallet == null) return; if (wallet == null) return;
long height = 0; long height;
try { try {
height = wallet.getHeight(); // can get read timeout while syncing height = wallet.getHeight(); // can get read timeout while syncing
} catch (Exception e) { } catch (Exception e) {
if (!isShutDownStarted) e.printStackTrace(); log.warn("Error getting wallet height while syncing with progress: " + e.getMessage());
if (wallet != null && !isShutDownStarted) e.printStackTrace();
// stop polling and release latch
syncProgressError = e;
syncProgressLatch.countDown();
return; return;
} }
if (height < xmrConnectionService.getTargetHeight()) updateSyncProgress(height);
else {
syncProgressLooper.stop();
wasWalletSynced = true;
updateSyncProgress(height); updateSyncProgress(height);
if (height >= xmrConnectionService.getTargetHeight()) {
setWalletSyncedWithProgress();
syncProgressLatch.countDown(); syncProgressLatch.countDown();
} }
}); });
wallet.startSyncing(xmrConnectionService.getRefreshPeriodMs());
syncProgressLooper.start(1000); syncProgressLooper.start(1000);
// wait for sync to complete
HavenoUtils.awaitLatch(syncProgressLatch); HavenoUtils.awaitLatch(syncProgressLatch);
wallet.stopSyncing();
if (!wasWalletSynced) throw new IllegalStateException("Failed to sync wallet with progress"); // stop polling
syncProgressLooper.stop();
syncProgressTimeout.stop();
if (wallet != null) wallet.stopSyncing(); // can become null if interrupted by force close
isSyncingWithProgress = false;
if (syncProgressError != null) throw new RuntimeException(syncProgressError);
}
} }
private void updateSyncProgress(long height) { private void updateSyncProgress(long height) {
UserThread.execute(() -> {
walletHeight.set(height);
resetSyncProgressTimeout(); resetSyncProgressTimeout();
UserThread.execute(() -> {
// set wallet height
walletHeight.set(height);
// new wallet reports height 1 before synced // new wallet reports height 1 before synced
if (height == 1) { if (height == 1) {
downloadListener.progress(.0001, xmrConnectionService.getTargetHeight() - height, null); // >0% shows progress bar downloadListener.progress(0, xmrConnectionService.getTargetHeight() - height, null);
return; return;
} }
@ -1505,7 +1552,7 @@ public class XmrWalletService {
long targetHeight = xmrConnectionService.getTargetHeight(); long targetHeight = xmrConnectionService.getTargetHeight();
long blocksLeft = targetHeight - walletHeight.get(); long blocksLeft = targetHeight - walletHeight.get();
if (syncStartHeight == null) syncStartHeight = walletHeight.get(); if (syncStartHeight == null) syncStartHeight = walletHeight.get();
double percent = Math.min(1.0, targetHeight == syncStartHeight ? 1.0 : ((double) Math.max(1, (double) walletHeight.get() - syncStartHeight) / (double) (targetHeight - syncStartHeight))); // grant at least 1 block to show progress double percent = Math.min(1.0, targetHeight == syncStartHeight ? 1.0 : ((double) walletHeight.get() - syncStartHeight) / (double) (targetHeight - syncStartHeight));
downloadListener.progress(percent, blocksLeft, null); downloadListener.progress(percent, blocksLeft, null);
}); });
} }
@ -1513,15 +1560,18 @@ public class XmrWalletService {
private synchronized void resetSyncProgressTimeout() { private synchronized void resetSyncProgressTimeout() {
if (syncProgressTimeout != null) syncProgressTimeout.stop(); if (syncProgressTimeout != null) syncProgressTimeout.stop();
syncProgressTimeout = UserThread.runAfter(() -> { syncProgressTimeout = UserThread.runAfter(() -> {
if (isShutDownStarted || wasWalletSynced) return; if (isShutDownStarted) return;
log.warn("Sync progress timeout called"); syncProgressError = new RuntimeException("Sync progress timeout called");
forceCloseMainWallet(); syncProgressLatch.countDown();
requestSwitchToNextBestConnection();
maybeInitMainWallet(true);
resetSyncProgressTimeout();
}, SYNC_PROGRESS_TIMEOUT_SECONDS, TimeUnit.SECONDS); }, SYNC_PROGRESS_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} }
private void setWalletSyncedWithProgress() {
wasWalletSynced = true;
isSyncingWithProgress = false;
syncProgressTimeout.stop();
}
private MoneroWalletFull createWalletFull(MoneroWalletConfig config) { private MoneroWalletFull createWalletFull(MoneroWalletConfig config) {
// must be connected to daemon // must be connected to daemon
@ -1686,14 +1736,6 @@ public class XmrWalletService {
String newProxyUri = connection == null ? null : connection.getProxyUri(); String newProxyUri = connection == null ? null : connection.getProxyUri();
log.info("Setting daemon connection for main wallet, monerod={}, 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 // update connection
if (wallet instanceof MoneroWalletRpc) { if (wallet instanceof MoneroWalletRpc) {
if (StringUtils.equals(oldProxyUri, newProxyUri)) { if (StringUtils.equals(oldProxyUri, newProxyUri)) {
@ -1702,7 +1744,7 @@ public class XmrWalletService {
log.info("Restarting main wallet because proxy URI has changed, old={}, new={}", oldProxyUri, newProxyUri); // TODO: set proxy without restarting wallet log.info("Restarting main wallet because proxy URI has changed, old={}, new={}", oldProxyUri, newProxyUri); // TODO: set proxy without restarting wallet
closeMainWallet(true); closeMainWallet(true);
doMaybeInitMainWallet(false, MAX_SYNC_ATTEMPTS); doMaybeInitMainWallet(false, MAX_SYNC_ATTEMPTS);
return; // wallet is re-initialized return; // wallet re-initializes off thread
} }
} else { } else {
wallet.setDaemonConnection(connection); wallet.setDaemonConnection(connection);
@ -1771,19 +1813,26 @@ public class XmrWalletService {
private void forceCloseMainWallet() { private void forceCloseMainWallet() {
stopPolling(); stopPolling();
if (wallet != null) { if (wallet != null && !isClosingWallet) {
isClosingWallet = true; isClosingWallet = true;
forceCloseWallet(wallet, getWalletPath(MONERO_WALLET_NAME)); forceCloseWallet(wallet, getWalletPath(MONERO_WALLET_NAME));
wallet = null; wallet = null;
} }
} }
private void forceRestartMainWallet() { public void forceRestartMainWallet() {
log.warn("Force restarting main wallet"); log.warn("Force restarting main wallet");
if (isClosingWallet) return;
forceCloseMainWallet(); forceCloseMainWallet();
maybeInitMainWallet(true); maybeInitMainWallet(true);
} }
public void handleWalletError(Exception e, MoneroRpcConnection sourceConnection) {
if (HavenoUtils.isUnresponsive(e)) forceCloseMainWallet(); // wallet can be stuck a while
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection(sourceConnection);
getWallet(); // re-open wallet
}
private void startPolling() { private void startPolling() {
synchronized (WALLET_LOCK) { synchronized (WALLET_LOCK) {
if (isShutDownStarted || isPolling()) return; if (isShutDownStarted || isPolling()) return;
@ -1849,17 +1898,10 @@ public class XmrWalletService {
log.warn("Monero daemon is not synced within tolerance, height={}, targetHeight={}", xmrConnectionService.chainHeightProperty().get(), xmrConnectionService.getTargetHeight()); log.warn("Monero daemon is not synced within tolerance, height={}, targetHeight={}", xmrConnectionService.chainHeightProperty().get(), xmrConnectionService.getTargetHeight());
return; return;
} }
// 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();
}
// sync wallet if behind daemon // sync wallet if behind daemon
if (walletHeight.get() < xmrConnectionService.getTargetHeight()) { if (walletHeight.get() < xmrConnectionService.getTargetHeight()) {
synchronized (WALLET_LOCK) { // avoid long sync from blocking other operations synchronized (WALLET_LOCK) { // avoid long sync from blocking other operations
syncMainWallet(); syncWithProgress();
} }
} }
@ -1868,13 +1910,17 @@ public class XmrWalletService {
if (updateTxs) { if (updateTxs) {
synchronized (WALLET_LOCK) { // avoid long fetch from blocking other operations synchronized (WALLET_LOCK) { // avoid long fetch from blocking other operations
synchronized (HavenoUtils.getDaemonLock()) { synchronized (HavenoUtils.getDaemonLock()) {
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try { try {
cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true)); cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true));
} catch (Exception e) { // fetch from pool can fail } catch (Exception e) { // fetch from pool can fail
if (!isShutDownStarted) { if (!isShutDownStarted) {
if (lastLogPollErrorTimestamp == null || System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS) { // limit error logging
// throttle error handling
if (lastLogPollErrorTimestamp == null || System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS) {
log.warn("Error polling main wallet's transactions from the pool: {}", e.getMessage()); log.warn("Error polling main wallet's transactions from the pool: {}", e.getMessage());
lastLogPollErrorTimestamp = System.currentTimeMillis(); lastLogPollErrorTimestamp = System.currentTimeMillis();
requestSwitchToNextBestConnection(sourceConnection);
} }
} }
} }
@ -1883,8 +1929,7 @@ public class XmrWalletService {
} }
} catch (Exception e) { } catch (Exception e) {
if (wallet == null || isShutDownStarted) return; if (wallet == null || isShutDownStarted) return;
boolean isConnectionRefused = e.getMessage() != null && e.getMessage().contains("Connection refused"); if (HavenoUtils.isUnresponsive(e)) forceRestartMainWallet();
if (isConnectionRefused) forceRestartMainWallet();
else if (isWalletConnectedToDaemon()) { else if (isWalletConnectedToDaemon()) {
log.warn("Error polling main wallet, errorMessage={}. Monerod={}", e.getMessage(), getConnectionService().getConnection()); log.warn("Error polling main wallet, errorMessage={}. Monerod={}", e.getMessage(), getConnectionService().getConnection());
//e.printStackTrace(); //e.printStackTrace();
@ -1927,8 +1972,12 @@ public class XmrWalletService {
} }
} }
public boolean requestSwitchToNextBestConnection() { private boolean requestSwitchToNextBestConnection() {
return xmrConnectionService.requestSwitchToNextBestConnection(); return requestSwitchToNextBestConnection(null);
}
public boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) {
return xmrConnectionService.requestSwitchToNextBestConnection(sourceConnection);
} }
private void onNewBlock(long height) { private void onNewBlock(long height) {

View file

@ -65,6 +65,7 @@ import javafx.scene.control.Button;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import monero.common.MoneroRpcConnection;
import monero.common.MoneroUtils; import monero.common.MoneroUtils;
import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroTxConfig;
import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroTxWallet;
@ -256,6 +257,7 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
// create tx // create tx
MoneroTxWallet tx = null; MoneroTxWallet tx = null;
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) { for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = xmrWalletService.getConnectionService().getConnection();
try { try {
log.info("Creating withdraw tx"); log.info("Creating withdraw tx");
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
@ -270,7 +272,7 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
if (isNotEnoughMoney(e.getMessage())) throw e; if (isNotEnoughMoney(e.getMessage())) throw e;
log.warn("Error creating creating withdraw tx, attempt={}/{}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage()); 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 (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
if (xmrWalletService.getConnectionService().isConnected()) xmrWalletService.requestSwitchToNextBestConnection(); if (xmrWalletService.getConnectionService().isConnected()) xmrWalletService.requestSwitchToNextBestConnection(sourceConnection);
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} }
} }