diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index 4a4610d8..ec8baae5 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -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 offerKeyImages = new HashSet(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages()); + for (MoneroOutputWallet output : tx.getOutputsWallet()) { + if (offerKeyImages.contains(output.getKeyImage().getHex())) return true; + } + return false; + } + private List getSplitOutputFundingTxs(BigInteger reserveAmount, Integer preferredSubaddressIndex) { List splitOutputTxs = xmrWalletService.getTxs(new MoneroTxQuery().setIsIncoming(true).setIsFailed(false)); Set removeTxs = new HashSet(); @@ -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; diff --git a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java index 27103cb6..3388368a 100644 --- a/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java +++ b/core/src/main/java/haveno/core/offer/placeoffer/tasks/MakerReserveOfferFunds.java @@ -86,7 +86,7 @@ public class MakerReserveOfferFunds extends Task { //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(); diff --git a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java index 4e3c38ae..1d8a32d3 100644 --- a/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java +++ b/core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java @@ -476,7 +476,7 @@ public final class ArbitrationManager extends DisputeManager> 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(); diff --git a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java index fa0dfbbf..33233083 100644 --- a/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/offer/MutableOfferViewModel.java @@ -109,6 +109,7 @@ public abstract class MutableOfferViewModel 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 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 ext public void onCancelOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { createOfferRequested = false; + createOfferCanceled = true; OpenOfferManager openOfferManager = HavenoUtils.openOfferManager; Optional openOffer = openOfferManager.getOpenOfferById(offer.getId()); if (openOffer.isPresent()) {