recover from offer funds being unexpectedly unavailable

This commit is contained in:
woodser 2024-07-20 01:13:29 -04:00
parent fc3407cd50
commit 13d6eaee7d
6 changed files with 64 additions and 22 deletions

View file

@ -556,13 +556,12 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
latch.countDown();
resultHandler.handleResult(transaction);
}, (errorMessage) -> {
if (openOffer.isCanceled()) latch.countDown();
else {
if (!openOffer.isCanceled()) {
log.warn("Error processing pending offer {}: {}", openOffer.getId(), errorMessage);
doCancelOffer(openOffer);
latch.countDown();
errorMessageHandler.handleErrorMessage(errorMessage);
}
latch.countDown();
errorMessageHandler.handleErrorMessage(errorMessage);
});
HavenoUtils.awaitLatch(latch);
}
@ -943,8 +942,18 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// 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
@ -981,7 +990,23 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// return split output tx if already assigned
if (openOffer != null && openOffer.getSplitOutputTxHash() != null) {
return xmrWalletService.getTx(openOffer.getSplitOutputTxHash());
// get recorded split output tx
MoneroTxWallet splitOutputTx = xmrWalletService.getTx(openOffer.getSplitOutputTxHash());
// 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());
}
}
// get split output tx to offer's preferred subaddress
@ -996,6 +1021,15 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
return getEarliestUnscheduledTx(openOffers, openOffer, fundingTxs);
}
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>();
@ -1064,7 +1098,8 @@ 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
@ -1080,10 +1115,10 @@ 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.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);
}
@ -1139,9 +1174,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
for (OpenOffer otherOffer : openOffers) {
if (otherOffer == openOffer) continue;
if (otherOffer.getState() != OpenOffer.State.PENDING) continue;
if (otherOffer.getScheduledTxHashes() == null) continue;
for (String scheduledTxHash : otherOffer.getScheduledTxHashes()) {
if (txHash.equals(scheduledTxHash)) return true;
if (txHash.equals(otherOffer.getSplitOutputTxHash())) return true;
if (otherOffer.getScheduledTxHashes() != null) {
for (String scheduledTxHash : otherOffer.getScheduledTxHashes()) {
if (txHash.equals(scheduledTxHash)) return true;
}
}
}
return false;

View file

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

View file

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

@ -1073,7 +1073,7 @@ public abstract class Trade implements Tradable, Model {
} catch (IllegalArgumentException | IllegalStateException e) {
throw e;
} catch (Exception e) {
log.warn("Failed to import multisig hex, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage());
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
@ -1180,7 +1180,7 @@ public abstract class Trade implements Tradable, Model {
} 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
@ -1241,7 +1241,7 @@ public abstract class Trade implements Tradable, Model {
throw e;
} catch (Exception e) {
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, attempt={}/{}, tradeId={}, 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());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying

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