mirror of
https://github.com/haveno-dex/haveno.git
synced 2024-10-01 01:35:48 -04:00
recover from offer funds being unexpectedly unavailable
This commit is contained in:
parent
fc3407cd50
commit
13d6eaee7d
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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()) {
|
||||
|
Loading…
Reference in New Issue
Block a user