force restart trade wallet on confirm payment if missing txs to fix #960

This commit is contained in:
woodser 2024-05-25 12:47:38 -04:00
parent 2f0ea48a31
commit 6dfa1841f8
5 changed files with 54 additions and 53 deletions

View File

@ -1068,6 +1068,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
MoneroTxWallet splitOutputTx = null; MoneroTxWallet splitOutputTx = null;
synchronized (XmrWalletService.WALLET_LOCK) { synchronized (XmrWalletService.WALLET_LOCK) {
XmrAddressEntry entry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); XmrAddressEntry entry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING);
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++) {
try { try {
@ -1087,6 +1088,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
} }
log.info("Done creating split output tx to fund offer {} in {} ms", openOffer.getId(), System.currentTimeMillis() - startTime); log.info("Done creating split output tx to fund offer {} in {} ms", openOffer.getId(), System.currentTimeMillis() - startTime);
} }
}
// set split tx // set split tx
setSplitOutputTx(openOffer, splitOutputTx); setSplitOutputTx(openOffer, splitOutputTx);

View File

@ -79,7 +79,7 @@ public class HavenoUtils {
// synchronize requests to the daemon // synchronize requests to the daemon
private static boolean SYNC_DAEMON_REQUESTS = true; // sync long requests to daemon (e.g. refresh, update pool) private static boolean SYNC_DAEMON_REQUESTS = true; // sync long requests to daemon (e.g. refresh, update pool)
private static boolean SYNC_WALLET_REQUESTS = false; // additionally sync wallet functions to daemon (e.g. create tx, import multisig hex) private static boolean SYNC_WALLET_REQUESTS = false; // additionally sync wallet functions to daemon (e.g. create txs)
private static Object DAEMON_LOCK = new Object(); private static Object DAEMON_LOCK = new Object();
public static Object getDaemonLock() { public static Object getDaemonLock() {
return SYNC_DAEMON_REQUESTS ? DAEMON_LOCK : new Object(); return SYNC_DAEMON_REQUESTS ? DAEMON_LOCK : new Object();

View File

@ -94,7 +94,6 @@ import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroRpcConnection; import monero.common.MoneroRpcConnection;
import monero.common.MoneroUtils;
import monero.common.TaskLooper; import monero.common.TaskLooper;
import monero.daemon.MoneroDaemon; import monero.daemon.MoneroDaemon;
import monero.daemon.model.MoneroKeyImage; import monero.daemon.model.MoneroKeyImage;
@ -964,6 +963,7 @@ public abstract class Trade implements Tradable, Model {
private void forceCloseWallet() { private void forceCloseWallet() {
if (wallet != null) { if (wallet != null) {
xmrWalletService.forceCloseWallet(wallet, wallet.getPath()); xmrWalletService.forceCloseWallet(wallet, wallet.getPath());
stopPolling();
wallet = null; wallet = null;
} }
} }
@ -1099,36 +1099,18 @@ public abstract class Trade implements Tradable, Model {
// check if multisig import needed // check if multisig import needed
if (wallet.isMultisigImportNeeded()) throw new RuntimeException("Cannot create payout tx because multisig import is needed"); if (wallet.isMultisigImportNeeded()) throw new RuntimeException("Cannot create payout tx because multisig import is needed");
// gather info // TODO: wallet sometimes returns empty data, after disconnect?
String sellerPayoutAddress = this.getSeller().getPayoutAddressString(); List<MoneroTxWallet> txs = wallet.getTxs(); // TODO: this fetches from pool
String buyerPayoutAddress = this.getBuyer().getPayoutAddressString(); if (txs.isEmpty()) {
Preconditions.checkNotNull(sellerPayoutAddress, "Seller payout address must not be null"); log.warn("Restarting wallet for {} {} because deposit txs are missing to create payout tx", getClass().getSimpleName(), getId());
Preconditions.checkNotNull(buyerPayoutAddress, "Buyer payout address must not be null"); forceRestartTradeWallet();
// TODO: wallet query to get deposit txs can sometimes return null, maybe when disconnected?
if (wallet.getTx(getSeller().getDepositTxHash()) == null || wallet.getTx(getBuyer().getDepositTxHash()) == null) {
String warningMsg = "Issue detected with trade wallet " + getShortId() + ". Please send logs to Haveno developers and restart your application if you encounter further problems:";
warningMsg += "\n\nSeller deposit tx id: " + getSeller().getDepositTxHash();
warningMsg += "\nBuyer deposit tx id: " + getBuyer().getDepositTxHash();
warningMsg += "\nSeller deposit tx is initialized: " + (getSeller().getDepositTx() != null);
warningMsg += "\nBuyer deposit tx is initialized: " + (getBuyer().getDepositTx() != null);
log.warn(warningMsg);
// request with logging
int previousLogLevel = MoneroUtils.getLogLevel();
MoneroUtils.setLogLevel(3);
log.warn("Requesting seller tx with logging");
MoneroTxWallet fetchedTx = wallet.getTx(getSeller().getDepositTxHash());
log.info("Seller tx: " + fetchedTx);
log.warn("Requesting buyer tx with logging");
fetchedTx = wallet.getTx(getBuyer().getDepositTxHash());
log.info("Buyer tx: " + fetchedTx);
MoneroUtils.setLogLevel(previousLogLevel);
// set top level error message to notify user
HavenoUtils.havenoSetup.getTopErrorMsg().set(warningMsg);
} }
// gather info
String sellerPayoutAddress = getSeller().getPayoutAddressString();
String buyerPayoutAddress = getBuyer().getPayoutAddressString();
Preconditions.checkNotNull(sellerPayoutAddress, "Seller payout address must not be null");
Preconditions.checkNotNull(buyerPayoutAddress, "Buyer payout address must not be null");
BigInteger sellerDepositAmount = getSeller().getDepositTx().getIncomingAmount(); BigInteger sellerDepositAmount = getSeller().getDepositTx().getIncomingAmount();
BigInteger buyerDepositAmount = getBuyer().getDepositTx().getIncomingAmount(); BigInteger buyerDepositAmount = getBuyer().getDepositTx().getIncomingAmount();
BigInteger tradeAmount = getAmount(); BigInteger tradeAmount = getAmount();
@ -1163,7 +1145,7 @@ public abstract class Trade implements Tradable, Model {
return createTx(txConfig); return createTx(txConfig);
} catch (Exception e) { } catch (Exception e) {
if (e.getMessage().contains("not possible")) throw new RuntimeException("Loser payout is too small to cover the mining fee"); if (e.getMessage().contains("not possible")) throw new RuntimeException("Loser payout is too small to cover the mining fee");
log.warn("Failed to create payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage()); log.warn("Failed to create dispute payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e; if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} }
@ -1183,6 +1165,22 @@ public abstract class Trade implements Tradable, Model {
public void processPayoutTx(String payoutTxHex, boolean sign, boolean publish) { public void processPayoutTx(String payoutTxHex, boolean sign, boolean publish) {
log.info("Processing payout tx for {} {}", getClass().getSimpleName(), getId()); 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();
}
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
}
}
// gather relevant info // gather relevant info
MoneroWallet wallet = getWallet(); MoneroWallet wallet = getWallet();
Contract contract = getContract(); Contract contract = getContract();
@ -2514,12 +2512,13 @@ public abstract class Trade implements Tradable, Model {
} }
private void forceRestartTradeWallet() { private void forceRestartTradeWallet() {
log.warn("Force restarting trade wallet for {} {}", getClass().getSimpleName(), getId());
if (isShutDownStarted || restartInProgress) return; if (isShutDownStarted || restartInProgress) return;
log.warn("Force restarting trade wallet for {} {}", getClass().getSimpleName(), getId());
restartInProgress = true; restartInProgress = true;
forceCloseWallet(); forceCloseWallet();
if (!isShutDownStarted) wallet = getWallet(); if (!isShutDownStarted) wallet = getWallet();
restartInProgress = false; restartInProgress = false;
doPollWallet();
if (!isShutDownStarted) ThreadUtils.execute(() -> tryInitPolling(), getId()); if (!isShutDownStarted) ThreadUtils.execute(() -> tryInitPolling(), getId());
} }
@ -2609,15 +2608,15 @@ public abstract class Trade implements Tradable, Model {
// skip if arbitrator // skip if arbitrator
if (this instanceof ArbitratorTrade) return; if (this instanceof ArbitratorTrade) return;
// freeze outputs until spent
xmrWalletService.freezeOutputs(getSelf().getReserveTxKeyImages());
// close open offer or reset address entries // close open offer or reset address entries
if (this instanceof MakerTrade) { if (this instanceof MakerTrade) {
processModel.getOpenOfferManager().closeOpenOffer(getOffer()); processModel.getOpenOfferManager().closeOpenOffer(getOffer());
} else { } else {
getXmrWalletService().resetAddressEntriesForOpenOffer(getId()); getXmrWalletService().resetAddressEntriesForOpenOffer(getId());
} }
// freeze outputs until spent
ThreadUtils.submitToPool(() -> xmrWalletService.freezeOutputs(getSelf().getReserveTxKeyImages()));
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -114,7 +114,7 @@ public class ProcessModel implements Model, PersistablePayload {
private byte[] payoutTxSignature; private byte[] payoutTxSignature;
@Nullable @Nullable
@Setter @Setter
private byte[] preparedDepositTx; private byte[] preparedDepositTx; // TODO: remove this unused field
@Setter @Setter
private boolean useSavingsWallet; private boolean useSavingsWallet;
@Setter @Setter

View File

@ -1735,9 +1735,9 @@ public class XmrWalletService {
private void forceCloseMainWallet() { private void forceCloseMainWallet() {
isClosingWallet = true; isClosingWallet = true;
forceCloseWallet(wallet, getWalletPath(MONERO_WALLET_NAME));
stopPolling(); stopPolling();
stopSyncWithProgress(); stopSyncWithProgress();
forceCloseWallet(wallet, getWalletPath(MONERO_WALLET_NAME));
wallet = null; wallet = null;
} }