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,24 +1068,26 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
MoneroTxWallet splitOutputTx = null;
synchronized (XmrWalletService.WALLET_LOCK) {
XmrAddressEntry entry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING);
long startTime = System.currentTimeMillis();
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
try {
log.info("Creating split output tx to fund offer {} at subaddress {}", openOffer.getShortId(), entry.getSubaddressIndex());
splitOutputTx = xmrWalletService.createTx(new MoneroTxConfig()
.setAccountIndex(0)
.setAddress(entry.getAddressString())
.setAmount(reserveAmount)
.setRelay(true)
.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 (stopped || i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
synchronized (HavenoUtils.getWalletFunctionLock()) {
long startTime = System.currentTimeMillis();
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
try {
log.info("Creating split output tx to fund offer {} at subaddress {}", openOffer.getShortId(), entry.getSubaddressIndex());
splitOutputTx = xmrWalletService.createTx(new MoneroTxConfig()
.setAccountIndex(0)
.setAddress(entry.getAddressString())
.setAmount(reserveAmount)
.setRelay(true)
.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 (stopped || i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}
}
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

View file

@ -79,7 +79,7 @@ public class HavenoUtils {
// 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_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();
public static Object getDaemonLock() {
return SYNC_DAEMON_REQUESTS ? DAEMON_LOCK : new Object();

View file

@ -94,7 +94,6 @@ import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroRpcConnection;
import monero.common.MoneroUtils;
import monero.common.TaskLooper;
import monero.daemon.MoneroDaemon;
import monero.daemon.model.MoneroKeyImage;
@ -964,6 +963,7 @@ public abstract class Trade implements Tradable, Model {
private void forceCloseWallet() {
if (wallet != null) {
xmrWalletService.forceCloseWallet(wallet, wallet.getPath());
stopPolling();
wallet = null;
}
}
@ -1099,36 +1099,18 @@ public abstract class Trade implements Tradable, Model {
// check if multisig import needed
if (wallet.isMultisigImportNeeded()) throw new RuntimeException("Cannot create payout tx because multisig import is needed");
// gather info
String sellerPayoutAddress = this.getSeller().getPayoutAddressString();
String buyerPayoutAddress = this.getBuyer().getPayoutAddressString();
Preconditions.checkNotNull(sellerPayoutAddress, "Seller payout address must not be null");
Preconditions.checkNotNull(buyerPayoutAddress, "Buyer payout address must not be null");
// 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);
// 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();
}
// 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 buyerDepositAmount = getBuyer().getDepositTx().getIncomingAmount();
BigInteger tradeAmount = getAmount();
@ -1163,7 +1145,7 @@ public abstract class Trade implements Tradable, Model {
return createTx(txConfig);
} 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 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;
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) {
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
MoneroWallet wallet = getWallet();
Contract contract = getContract();
@ -2514,12 +2512,13 @@ public abstract class Trade implements Tradable, Model {
}
private void forceRestartTradeWallet() {
log.warn("Force restarting trade wallet for {} {}", getClass().getSimpleName(), getId());
if (isShutDownStarted || restartInProgress) return;
log.warn("Force restarting trade wallet for {} {}", getClass().getSimpleName(), getId());
restartInProgress = true;
forceCloseWallet();
if (!isShutDownStarted) wallet = getWallet();
restartInProgress = false;
doPollWallet();
if (!isShutDownStarted) ThreadUtils.execute(() -> tryInitPolling(), getId());
}
@ -2609,15 +2608,15 @@ public abstract class Trade implements Tradable, Model {
// skip if arbitrator
if (this instanceof ArbitratorTrade) return;
// freeze outputs until spent
xmrWalletService.freezeOutputs(getSelf().getReserveTxKeyImages());
// close open offer or reset address entries
if (this instanceof MakerTrade) {
processModel.getOpenOfferManager().closeOpenOffer(getOffer());
} else {
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;
@Nullable
@Setter
private byte[] preparedDepositTx;
private byte[] preparedDepositTx; // TODO: remove this unused field
@Setter
private boolean useSavingsWallet;
@Setter

View file

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