diff --git a/core/src/main/java/haveno/core/offer/OpenOffer.java b/core/src/main/java/haveno/core/offer/OpenOffer.java index 3574cb59..6725c725 100644 --- a/core/src/main/java/haveno/core/offer/OpenOffer.java +++ b/core/src/main/java/haveno/core/offer/OpenOffer.java @@ -21,6 +21,9 @@ import haveno.common.Timer; import haveno.common.UserThread; import haveno.common.proto.ProtoUtil; import haveno.core.trade.Tradable; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Setter; @@ -78,15 +81,12 @@ public final class OpenOffer implements Tradable { @Setter @Getter private String reserveTxKey; - - - // Added in v1.5.3. - // If market price reaches that trigger price the offer gets deactivated @Getter private final long triggerPrice; @Getter @Setter transient private long mempoolStatus = -1; + transient final private ObjectProperty stateProperty = new SimpleObjectProperty<>(state); public OpenOffer(Offer offer) { this(offer, 0, false); @@ -185,6 +185,7 @@ public final class OpenOffer implements Tradable { public void setState(State state) { this.state = state; + stateProperty.set(state); // We keep it reserved for a limited time, if trade preparation fails we revert to available state if (this.state == State.RESERVED) { // TODO (woodser): remove this? @@ -194,6 +195,14 @@ public final class OpenOffer implements Tradable { } } + public ReadOnlyObjectProperty stateProperty() { + return stateProperty; + } + + public boolean isScheduled() { + return state == State.SCHEDULED; + } + public boolean isDeactivated() { return state == State.DEACTIVATED; } diff --git a/core/src/main/java/haveno/core/offer/OpenOfferManager.java b/core/src/main/java/haveno/core/offer/OpenOfferManager.java index a3fc1d83..29f0cbb7 100644 --- a/core/src/main/java/haveno/core/offer/OpenOfferManager.java +++ b/core/src/main/java/haveno/core/offer/OpenOfferManager.java @@ -526,7 +526,9 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe public void activateOpenOffer(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - if (!offersToBeEdited.containsKey(openOffer.getId())) { + if (openOffer.isScheduled()) { + resultHandler.handleResult(); // ignore if scheduled + } else if (!offersToBeEdited.containsKey(openOffer.getId())) { Offer offer = openOffer.getOffer(); offerBookService.activateOffer(offer, () -> { @@ -545,14 +547,18 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { Offer offer = openOffer.getOffer(); - offerBookService.deactivateOffer(offer.getOfferPayload(), - () -> { - openOffer.setState(OpenOffer.State.DEACTIVATED); - requestPersistence(); - log.debug("deactivateOpenOffer, offerId={}", offer.getId()); - resultHandler.handleResult(); - }, - errorMessageHandler); + if (openOffer.isScheduled()) { + resultHandler.handleResult(); // ignore if scheduled + } else { + offerBookService.deactivateOffer(offer.getOfferPayload(), + () -> { + openOffer.setState(OpenOffer.State.DEACTIVATED); + requestPersistence(); + log.debug("deactivateOpenOffer, offerId={}", offer.getId()); + resultHandler.handleResult(); + }, + errorMessageHandler); + } } public void removeOpenOffer(OpenOffer openOffer, @@ -799,6 +805,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // get offer reserve amount BigInteger offerReserveAmount = openOffer.getOffer().getReserveAmount(); + // handle split output offer if (openOffer.isSplitOutput()) { @@ -816,11 +823,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe signAndPostOffer(openOffer, true, resultHandler, errorMessageHandler); return; } else if (splitOutputTx == null) { - + // handle sufficient available balance to split output boolean sufficientAvailableBalance = xmrWalletService.getWallet().getUnlockedBalance(0).compareTo(offerReserveAmount) >= 0; if (sufficientAvailableBalance) { - + // create and relay tx to split output splitOutputTx = createAndRelaySplitOutputTx(openOffer); // TODO: confirm with user? @@ -975,7 +982,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe private MoneroTxWallet createAndRelaySplitOutputTx(OpenOffer openOffer) { BigInteger reserveAmount = openOffer.getOffer().getReserveAmount(); - String fundingSubaddress = xmrWalletService.getAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).get().getAddressString(); + xmrWalletService.swapTradeEntryToAvailableEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); // change funding subaddress in case funded with unsuitable output // TODO: unecessary with destination funding + String fundingSubaddress = xmrWalletService.getNewAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).getAddressString(); return xmrWalletService.getWallet().createTx(new MoneroTxConfig() .setAccountIndex(0) .setAddress(fundingSubaddress) 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 5b9fb4ae..ea892135 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 @@ -56,7 +56,7 @@ public class MakerReserveOfferFunds extends Task { String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); BigInteger exactOutputAmount = model.getOpenOffer().isSplitOutput() ? model.getOpenOffer().getOffer().getReserveAmount() : null; XmrAddressEntry fundingEntry = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null); - Integer preferredSubaddressIndex = model.getOpenOffer().isSplitOutput() && fundingEntry != null ? fundingEntry.getSubaddressIndex() : null; + Integer preferredSubaddressIndex = model.getOpenOffer().isSplitOutput() && fundingEntry != null ? fundingEntry.getSubaddressIndex() : null; MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(makerFee, sendAmount, securityDeposit, returnAddress, exactOutputAmount, preferredSubaddressIndex); // check for error in case creating reserve tx exceeded timeout diff --git a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java index 7080ac73..2abe6a8d 100644 --- a/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java +++ b/core/src/main/java/haveno/core/xmr/wallet/XmrWalletService.java @@ -342,6 +342,7 @@ public class XmrWalletService { log.info("Done creating reserve tx in {} ms", System.currentTimeMillis() - time); return reserveTx; } catch (Exception e) { + if (exactOutputAmount != null) return spendOutputManually(true, tradeFee, sendAmount, securityDeposit, returnAddress, exactOutputAmount); // retry creating reserve tx using funds outside subaddress if (subaddressIndex != null) return createReserveTx(tradeFee, sendAmount, securityDeposit, returnAddress, exactOutputAmount, null); @@ -349,7 +350,7 @@ public class XmrWalletService { } } - /**s + /** * Create the multisig deposit tx and freeze its inputs. * * @param trade the trade to create a deposit tx from @@ -380,15 +381,42 @@ public class XmrWalletService { log.info("Done creating deposit tx for trade {} {} in {} ms", trade.getClass().getSimpleName(), trade.getId(), System.currentTimeMillis() - time); return tradeTx; } catch (Exception e) { + if (exactOutputAmount != null) return spendOutputManually(false, tradeFee, sendAmount, securityDeposit, multisigAddress, exactOutputAmount); // retry creating deposit tx using funds outside subaddress if (subaddressIndex != null) return createDepositTx(trade, exactOutputAmount, null); else throw e; } - } } + // retry with exact outputs in other subaddresses + // TODO: this is a hack because wallet2 sometimes prefers to spend multiple inputs intead of exact output; replace with fund by destination address when available + private MoneroTxWallet spendOutputManually(boolean isReserveTx, BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String returnAddress, BigInteger exactOutputAmount) { + log.warn("Manually selecting subaddress to spend output from"); + List exactOutputs = wallet.getOutputs(new MoneroOutputQuery() + .setAmount(exactOutputAmount) + .setIsSpent(false) + .setIsFrozen(false)); + Set subaddressIndices = new HashSet(); + for (MoneroOutputWallet output : exactOutputs) { + if (!output.getTx().isLocked()) subaddressIndices.add(output.getSubaddressIndex()); + } + Exception err = null; + for (Integer idx : subaddressIndices) { + try { + long startTime = System.currentTimeMillis(); + MoneroTxWallet reserveTx = createTradeTx(tradeFee, sendAmount, securityDeposit, returnAddress, isReserveTx, exactOutputAmount, idx); + log.info("Done creating output tx in {} ms", System.currentTimeMillis() - startTime); + return reserveTx; + } catch (Exception e2) { + err = e2; + } + } + if (err != null) throw new RuntimeException(err); + throw new RuntimeException("No output available with amount " + exactOutputAmount); + } + private MoneroTxWallet createTradeTx(BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String address, boolean isReserveTx, BigInteger exactOutputAmount, Integer subaddressIndex) { MoneroWallet wallet = getWallet(); synchronized (wallet) { @@ -398,7 +426,7 @@ public class XmrWalletService { MoneroTxWallet tradeTx = null; double appliedTolerance = 0.0; // percent of tolerance to apply, thereby decreasing security deposit double searchDiff = 1.0; // difference for next binary search - int maxSearches = 5 ; + int maxSearches = 5; for (int i = 0; i < maxSearches; i++) { try { BigInteger appliedSecurityDeposit = new BigDecimal(securityDeposit).multiply(new BigDecimal(1.0 - SECURITY_DEPOSIT_TOLERANCE * appliedTolerance)).toBigInteger(); diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 46a098d8..868bb540 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -229,6 +229,7 @@ shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transac shared.numItemsLabel=Number of entries: {0} shared.filter=Filter shared.enabled=Enabled +shared.pending=Pending shared.me=Me shared.maker=Maker shared.taker=Taker diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java index 2317c73a..592255b5 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/openoffer/OpenOffersView.java @@ -68,10 +68,17 @@ import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.stage.Stage; import javafx.util.Callback; + +import org.fxmisc.easybind.EasyBind; +import org.fxmisc.easybind.Subscription; import org.jetbrains.annotations.NotNull; import javax.inject.Inject; import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import static haveno.desktop.util.FormBuilder.getRegularIconButton; @@ -109,6 +116,8 @@ public class OpenOffersView extends ActivatableViewAndModel widthListener; + private Map offerStateSubscriptions = new HashMap(); + @Inject public OpenOffersView(OpenOffersViewModel model, Navigation navigation, OfferDetailsWindow offerDetailsWindow) { super(model); @@ -285,16 +294,24 @@ public class OpenOffersView extends ActivatableViewAndModel availableItems = sortedList.stream() + .filter(openOfferListItem -> !openOfferListItem.getOpenOffer().isScheduled()) + .collect(Collectors.toList()); + if (availableItems.size() == 0) { selectToggleButton.setDisable(true); selectToggleButton.setSelected(false); } else { selectToggleButton.setDisable(false); - long numDeactivated = sortedList.stream() + long numDeactivated = availableItems.stream() .filter(openOfferListItem -> openOfferListItem.getOpenOffer().isDeactivated()) .count(); - if (numDeactivated == sortedList.size()) { + if (numDeactivated == availableItems.size()) { selectToggleButton.setSelected(false); } else if (numDeactivated == 0) { selectToggleButton.setSelected(true); @@ -683,15 +700,24 @@ public class OpenOffersView extends ActivatableViewAndModel { + refresh(); + })); + } + if (openOffer.getState() == OpenOffer.State.SCHEDULED) { + setGraphic(new AutoTooltipLabel(Res.get("shared.pending"))); + return; + } + if (checkBox == null) { checkBox = new AutoTooltipSlideToggleButton(); checkBox.setPadding(new Insets(-7, 0, -7, 0));