mirror of
https://github.com/haveno-dex/haveno.git
synced 2024-12-22 11:39:29 +00:00
recover from offer funds being unexpectedly unavailable
This commit is contained in:
parent
fc3407cd50
commit
13d6eaee7d
6 changed files with 64 additions and 22 deletions
|
@ -556,13 +556,12 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
latch.countDown();
|
latch.countDown();
|
||||||
resultHandler.handleResult(transaction);
|
resultHandler.handleResult(transaction);
|
||||||
}, (errorMessage) -> {
|
}, (errorMessage) -> {
|
||||||
if (openOffer.isCanceled()) latch.countDown();
|
if (!openOffer.isCanceled()) {
|
||||||
else {
|
|
||||||
log.warn("Error processing pending offer {}: {}", openOffer.getId(), errorMessage);
|
log.warn("Error processing pending offer {}: {}", openOffer.getId(), errorMessage);
|
||||||
doCancelOffer(openOffer);
|
doCancelOffer(openOffer);
|
||||||
latch.countDown();
|
|
||||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
|
||||||
}
|
}
|
||||||
|
latch.countDown();
|
||||||
|
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||||
});
|
});
|
||||||
HavenoUtils.awaitLatch(latch);
|
HavenoUtils.awaitLatch(latch);
|
||||||
}
|
}
|
||||||
|
@ -943,8 +942,18 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
|
|
||||||
// if not found, create tx to split exact output
|
// if not found, create tx to split exact output
|
||||||
if (splitOutputTx == null) {
|
if (splitOutputTx == null) {
|
||||||
if (openOffer.getSplitOutputTxHash() != null) log.warn("Split output tx not found for offer {}", openOffer.getId());
|
if (openOffer.getSplitOutputTxHash() != null) {
|
||||||
splitOrSchedule(openOffers, openOffer, amountNeeded);
|
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()) {
|
} else if (!splitOutputTx.isLocked()) {
|
||||||
|
|
||||||
// otherwise sign and post offer if split output available
|
// 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
|
// return split output tx if already assigned
|
||||||
if (openOffer != null && openOffer.getSplitOutputTxHash() != null) {
|
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
|
// 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);
|
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) {
|
private List<MoneroTxWallet> getSplitOutputFundingTxs(BigInteger reserveAmount, Integer preferredSubaddressIndex) {
|
||||||
List<MoneroTxWallet> splitOutputTxs = xmrWalletService.getTxs(new MoneroTxQuery().setIsIncoming(true).setIsFailed(false));
|
List<MoneroTxWallet> splitOutputTxs = xmrWalletService.getTxs(new MoneroTxQuery().setIsIncoming(true).setIsFailed(false));
|
||||||
Set<MoneroTxWallet> removeTxs = new HashSet<MoneroTxWallet>();
|
Set<MoneroTxWallet> removeTxs = new HashSet<MoneroTxWallet>();
|
||||||
|
@ -1064,7 +1098,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
.setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY));
|
.setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY));
|
||||||
break;
|
break;
|
||||||
} catch (Exception e) {
|
} 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 (stopped || i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
||||||
if (xmrConnectionService.isConnected()) xmrWalletService.requestSwitchToNextBestConnection();
|
if (xmrConnectionService.isConnected()) xmrWalletService.requestSwitchToNextBestConnection();
|
||||||
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
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) {
|
private void setSplitOutputTx(OpenOffer openOffer, MoneroTxWallet splitOutputTx) {
|
||||||
openOffer.setSplitOutputTxHash(splitOutputTx.getHash());
|
openOffer.setSplitOutputTxHash(splitOutputTx == null ? null : splitOutputTx.getHash());
|
||||||
openOffer.setSplitOutputTxFee(splitOutputTx.getFee().longValueExact());
|
openOffer.setSplitOutputTxFee(splitOutputTx == null ? 0l : splitOutputTx.getFee().longValueExact());
|
||||||
openOffer.setScheduledTxHashes(Arrays.asList(splitOutputTx.getHash()));
|
openOffer.setScheduledTxHashes(splitOutputTx == null ? null : Arrays.asList(splitOutputTx.getHash()));
|
||||||
openOffer.setScheduledAmount(openOffer.getOffer().getAmountNeeded().toString());
|
openOffer.setScheduledAmount(splitOutputTx == null ? null : openOffer.getOffer().getAmountNeeded().toString());
|
||||||
if (!openOffer.isCanceled()) openOffer.setState(OpenOffer.State.PENDING);
|
if (!openOffer.isCanceled()) openOffer.setState(OpenOffer.State.PENDING);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1139,9 +1174,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||||
for (OpenOffer otherOffer : openOffers) {
|
for (OpenOffer otherOffer : openOffers) {
|
||||||
if (otherOffer == openOffer) continue;
|
if (otherOffer == openOffer) continue;
|
||||||
if (otherOffer.getState() != OpenOffer.State.PENDING) continue;
|
if (otherOffer.getState() != OpenOffer.State.PENDING) continue;
|
||||||
if (otherOffer.getScheduledTxHashes() == null) continue;
|
if (txHash.equals(otherOffer.getSplitOutputTxHash())) return true;
|
||||||
for (String scheduledTxHash : otherOffer.getScheduledTxHashes()) {
|
if (otherOffer.getScheduledTxHashes() != null) {
|
||||||
if (txHash.equals(scheduledTxHash)) return true;
|
for (String scheduledTxHash : otherOffer.getScheduledTxHashes()) {
|
||||||
|
if (txHash.equals(scheduledTxHash)) return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -86,7 +86,7 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
||||||
//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, 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;
|
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();
|
if (model.getXmrWalletService().getConnectionService().isConnected()) model.getXmrWalletService().requestSwitchToNextBestConnection();
|
||||||
|
|
|
@ -476,7 +476,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
break;
|
break;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
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, 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 (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
||||||
if (trade.getXmrConnectionService().isConnected()) trade.requestSwitchToNextBestConnection();
|
if (trade.getXmrConnectionService().isConnected()) trade.requestSwitchToNextBestConnection();
|
||||||
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
||||||
|
|
|
@ -1073,7 +1073,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
} catch (IllegalArgumentException | IllegalStateException e) {
|
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||||
throw e;
|
throw e;
|
||||||
} catch (Exception 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 (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
||||||
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
|
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
|
||||||
if (isReadTimeoutError(e.getMessage())) forceRestartTradeWallet(); // wallet can be stuck a while
|
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) {
|
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||||
throw e;
|
throw e;
|
||||||
} catch (Exception 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 (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
||||||
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
|
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
|
||||||
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
||||||
|
@ -1241,7 +1241,7 @@ public abstract class Trade implements Tradable, Model {
|
||||||
throw e;
|
throw e;
|
||||||
} 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, 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 (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
||||||
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
|
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
|
||||||
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
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) -> {
|
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()))
|
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();
|
paymentAccountsComboBoxSelectionHandler = e -> onPaymentAccountsComboBoxSelected();
|
||||||
|
|
|
@ -109,6 +109,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
|
||||||
private String addressAsString;
|
private String addressAsString;
|
||||||
private final String paymentLabel;
|
private final String paymentLabel;
|
||||||
private boolean createOfferRequested;
|
private boolean createOfferRequested;
|
||||||
|
public boolean createOfferCanceled;
|
||||||
|
|
||||||
public final StringProperty amount = new SimpleStringProperty();
|
public final StringProperty amount = new SimpleStringProperty();
|
||||||
public final StringProperty minAmount = 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) {
|
void onPlaceOffer(Offer offer, Runnable resultHandler) {
|
||||||
errorMessage.set(null);
|
errorMessage.set(null);
|
||||||
createOfferRequested = true;
|
createOfferRequested = true;
|
||||||
|
createOfferCanceled = false;
|
||||||
|
|
||||||
dataModel.onPlaceOffer(offer, transaction -> {
|
dataModel.onPlaceOffer(offer, transaction -> {
|
||||||
resultHandler.run();
|
resultHandler.run();
|
||||||
|
@ -631,6 +633,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
|
||||||
|
|
||||||
public void onCancelOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
public void onCancelOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||||
createOfferRequested = false;
|
createOfferRequested = false;
|
||||||
|
createOfferCanceled = true;
|
||||||
OpenOfferManager openOfferManager = HavenoUtils.openOfferManager;
|
OpenOfferManager openOfferManager = HavenoUtils.openOfferManager;
|
||||||
Optional<OpenOffer> openOffer = openOfferManager.getOpenOfferById(offer.getId());
|
Optional<OpenOffer> openOffer = openOfferManager.getOpenOfferById(offer.getId());
|
||||||
if (openOffer.isPresent()) {
|
if (openOffer.isPresent()) {
|
||||||
|
|
Loading…
Reference in a new issue