diff --git a/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java b/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java index 560b21816d..7235efce7b 100644 --- a/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java +++ b/core/src/main/java/haveno/core/app/HavenoHeadlessApp.java @@ -77,7 +77,7 @@ public class HavenoHeadlessApp implements HeadlessApp { }); havenoSetup.setDisplayTorNetworkSettingsHandler(show -> log.info("onDisplayTorNetworkSettingsHandler: show={}", show)); havenoSetup.setChainFileLockedExceptionHandler(msg -> log.error("onChainFileLockedExceptionHandler: msg={}", msg)); - havenoSetup.setLockedUpFundsHandler(msg -> log.info("onLockedUpFundsHandler: msg={}", msg)); + tradeManager.setLockedUpFundsHandler(msg -> log.info("onLockedUpFundsHandler: msg={}", msg)); havenoSetup.setShowFirstPopupIfResyncSPVRequestedHandler(() -> log.info("onShowFirstPopupIfResyncSPVRequestedHandler")); havenoSetup.setDisplayUpdateHandler((alert, key) -> log.info("onDisplayUpdateHandler")); havenoSetup.setDisplayAlertHandler(alert -> log.info("onDisplayAlertHandler. alert={}", alert)); diff --git a/core/src/main/java/haveno/core/app/HavenoSetup.java b/core/src/main/java/haveno/core/app/HavenoSetup.java index 0b408a0faf..e90d25df79 100644 --- a/core/src/main/java/haveno/core/app/HavenoSetup.java +++ b/core/src/main/java/haveno/core/app/HavenoSetup.java @@ -66,13 +66,11 @@ import haveno.core.support.dispute.mediation.MediationManager; import haveno.core.support.dispute.refund.RefundManager; import haveno.core.trade.HavenoUtils; import haveno.core.trade.TradeManager; -import haveno.core.trade.TradeTxException; import haveno.core.user.Preferences; import haveno.core.user.Preferences.UseTorForXmr; import haveno.core.user.User; import haveno.core.util.FormattingUtils; import haveno.core.util.coin.CoinFormatter; -import haveno.core.xmr.model.AddressEntry; import haveno.core.xmr.setup.WalletsSetup; import haveno.core.xmr.wallet.BtcWalletService; import haveno.core.xmr.wallet.WalletsManager; @@ -92,7 +90,6 @@ import java.util.List; import java.util.Objects; import java.util.Random; import java.util.Scanner; -import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -107,7 +104,6 @@ import javax.annotation.Nullable; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import org.bitcoinj.core.Coin; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.monadic.MonadicBinding; @@ -449,11 +445,7 @@ public class HavenoSetup { walletAppSetup.init(chainFileLockedExceptionHandler, showFirstPopupIfResyncSPVRequestedHandler, showPopupIfInvalidBtcConfigHandler, - () -> { - if (allBasicServicesInitialized) { - checkForLockedUpFunds(); - } - }, + () -> {}, () -> {}); } @@ -466,10 +458,6 @@ public class HavenoSetup { revolutAccountsUpdateHandler, amazonGiftCardAccountsUpdateHandler); - if (xmrWalletService.downloadPercentageProperty().get() == 1) { - checkForLockedUpFunds(); - } - alertManager.alertMessageProperty().addListener((observable, oldValue, newValue) -> displayAlertIfPresent(newValue, false)); displayAlertIfPresent(alertManager.alertMessageProperty().get(), false); @@ -484,32 +472,6 @@ public class HavenoSetup { // Utils /////////////////////////////////////////////////////////////////////////////////////////// - private void checkForLockedUpFunds() { - // We check if there are locked up funds in failed or closed trades - try { - Set setOfAllTradeIds = tradeManager.getSetOfFailedOrClosedTradeIdsFromLockedInFunds(); - btcWalletService.getAddressEntriesForTrade().stream() - .filter(e -> setOfAllTradeIds.contains(e.getOfferId()) && - e.getContext() == AddressEntry.Context.MULTI_SIG) - .forEach(e -> { - Coin balance = e.getCoinLockedInMultiSigAsCoin(); - if (balance.isPositive()) { - String message = Res.get("popup.warning.lockedUpFunds", - formatter.formatCoinWithCode(balance), e.getAddressString(), e.getOfferId()); - log.warn(message); - if (lockedUpFundsHandler != null) { - lockedUpFundsHandler.accept(message); - } - } - }); - } catch (TradeTxException e) { - log.warn(e.getMessage()); - if (lockedUpFundsHandler != null) { - lockedUpFundsHandler.accept(e.getMessage()); - } - } - } - @Nullable public static String getLastHavenoVersion() { File versionFile = getVersionFile(); diff --git a/core/src/main/java/haveno/core/trade/ClosedTradableManager.java b/core/src/main/java/haveno/core/trade/ClosedTradableManager.java index 41eddbddae..0a48fc5188 100644 --- a/core/src/main/java/haveno/core/trade/ClosedTradableManager.java +++ b/core/src/main/java/haveno/core/trade/ClosedTradableManager.java @@ -227,4 +227,12 @@ public class ClosedTradableManager implements PersistedDataHost { private void requestPersistence() { persistenceManager.requestPersistence(); } + + public void removeTrade(Trade trade) { + synchronized (closedTradables) { + if (closedTradables.remove(trade)) { + requestPersistence(); + } + } + } } diff --git a/core/src/main/java/haveno/core/trade/Trade.java b/core/src/main/java/haveno/core/trade/Trade.java index e5670c4500..cc04b78175 100644 --- a/core/src/main/java/haveno/core/trade/Trade.java +++ b/core/src/main/java/haveno/core/trade/Trade.java @@ -1408,7 +1408,7 @@ public abstract class Trade implements Tradable, Model { // check if deposit published if (isDepositsPublished()) { - restorePublishedTrade(); + restoreDepositsPublishedTrade(); return; } @@ -1446,7 +1446,7 @@ public abstract class Trade implements Tradable, Model { // listen for deposits published to restore trade protocolErrorStateSubscription = EasyBind.subscribe(stateProperty(), state -> { if (isDepositsPublished()) { - restorePublishedTrade(); + restoreDepositsPublishedTrade(); if (protocolErrorStateSubscription != null) { // unsubscribe protocolErrorStateSubscription.unsubscribe(); protocolErrorStateSubscription = null; @@ -1496,7 +1496,7 @@ public abstract class Trade implements Tradable, Model { }); } - private void restorePublishedTrade() { + private void restoreDepositsPublishedTrade() { // close open offer if (this instanceof MakerTrade && processModel.getOpenOfferManager().getOpenOfferById(getId()).isPresent()) { @@ -2370,15 +2370,23 @@ public abstract class Trade implements Tradable, Model { setDepositTxs(txs); // check if any outputs spent (observed on payout published) + boolean hasSpentOutput = false; + boolean hasFailedTx = false; for (MoneroTxWallet tx : txs) { + if (tx.isFailed()) hasFailedTx = true; for (MoneroOutputWallet output : tx.getOutputsWallet()) { - if (Boolean.TRUE.equals(output.isSpent())) setPayoutStatePublished(); + if (Boolean.TRUE.equals(output.isSpent())) hasSpentOutput = true; } } + if (hasSpentOutput) setPayoutStatePublished(); + else if (hasFailedTx && isPayoutPublished()) { + log.warn("{} {} is in payout published state but has failed tx and no spent outputs, resetting payout state to unpublished", getClass().getSimpleName(), getShortId()); + setPayoutState(PayoutState.PAYOUT_UNPUBLISHED); + } // check for outgoing txs (appears after wallet submits payout tx or on payout confirmed) for (MoneroTxWallet tx : txs) { - if (tx.isOutgoing()) { + if (tx.isOutgoing() && !tx.isFailed()) { setPayoutTx(tx); setPayoutStatePublished(); if (tx.isConfirmed()) setPayoutStateConfirmed(); @@ -2460,6 +2468,10 @@ public abstract class Trade implements Tradable, Model { if (!isPayoutUnlocked()) setPayoutState(PayoutState.PAYOUT_UNLOCKED); } + private Trade getTrade() { + return this; + } + /** * Listen to block notifications from the main wallet in order to sync * idling trade wallets awaiting the payout to confirm or unlock. @@ -2485,9 +2497,10 @@ public abstract class Trade implements Tradable, Model { try { // get payout height if unknown - if (payoutHeight == null && getPayoutTxId() != null) { + if (payoutHeight == null && getPayoutTxId() != null && isPayoutPublished()) { MoneroTx tx = xmrWalletService.getDaemon().getTx(getPayoutTxId()); - if (tx.isConfirmed()) payoutHeight = tx.getHeight(); + if (tx == null) log.warn("Payout tx not found for {} {}, txId={}", getTrade().getClass().getSimpleName(), getId(), getPayoutTxId()); + else if (tx.isConfirmed()) payoutHeight = tx.getHeight(); } // sync wallet if confirm or unlock expected diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index 7430ccaa67..525ef2e94e 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -119,6 +119,7 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; import javafx.beans.property.BooleanProperty; @@ -129,6 +130,7 @@ import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javax.annotation.Nullable; import lombok.Getter; +import lombok.Setter; import monero.daemon.model.MoneroTx; import org.bitcoinj.core.Coin; import org.bouncycastle.crypto.params.KeyParameter; @@ -174,6 +176,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi private final LongProperty numPendingTrades = new SimpleLongProperty(); private final ReferralIdService referralIdService; + @Setter + @Nullable + private Consumer lockedUpFundsHandler; // TODO: this is unused + // set comparator for processing mailbox messages static { MailboxMessageService.setMailboxMessageComparator(new MailboxMessageComparator()); @@ -492,6 +498,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi log.warn("Swapping pending {} entries at startup. offerId={}", addressEntry.getContext(), addressEntry.getOfferId()); xmrWalletService.swapAddressEntryToAvailable(addressEntry.getOfferId(), addressEntry.getContext()); }); + + checkForLockedUpFunds(); } // notify that persisted trades initialized @@ -1040,15 +1048,21 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } public void onMoveFailedTradeToPendingTrades(Trade trade) { - addFailedTradeToPendingTrades(trade); + addTradeToPendingTrades(trade); failedTradesManager.removeTrade(trade); } - public void removeFailedTrade(Trade trade) { + public void onMoveClosedTradeToPendingTrades(Trade trade) { + trade.setCompleted(false); + addTradeToPendingTrades(trade); + closedTradableManager.removeTrade(trade); + } + + private void removeFailedTrade(Trade trade) { failedTradesManager.removeTrade(trade); } - public void addFailedTradeToPendingTrades(Trade trade) { + private void addTradeToPendingTrades(Trade trade) { if (!trade.isInitialized()) { initPersistedTrade(trade); } @@ -1061,6 +1075,14 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi } } + private void checkForLockedUpFunds() { + try { + getSetOfFailedOrClosedTradeIdsFromLockedInFunds(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + public Set getSetOfFailedOrClosedTradeIdsFromLockedInFunds() throws TradeTxException { AtomicReference tradeTxException = new AtomicReference<>(); synchronized (tradableList) { diff --git a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java index 135c7bb79f..e5cabc4c2e 100644 --- a/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java @@ -285,7 +285,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D synchronized (trade) { // skip if no need to reprocess - if (trade.isSeller() || trade.getSeller().getPaymentReceivedMessage() == null || trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal()) { + if (trade.isSeller() || trade.getSeller().getPaymentReceivedMessage() == null || (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && trade.isPayoutPublished())) { return; } diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java index 75614863ee..1a35ec8cb7 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java @@ -75,7 +75,7 @@ public class ProcessPaymentReceivedMessage extends TradeTask { if (trade.getSeller().getNodeAddress().equals(trade.getBuyer().getNodeAddress())) trade.getBuyer().setNodeAddress(null); // tests can reuse addresses // ack and complete if already processed - if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_RECEIVED.ordinal()) { + if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_RECEIVED.ordinal() && trade.isPayoutPublished()) { log.warn("Received another PaymentReceivedMessage which was already processed, ACKing"); complete(); return; diff --git a/desktop/src/main/java/haveno/desktop/main/MainViewModel.java b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java index bd37e66b14..6d2496fac3 100644 --- a/desktop/src/main/java/haveno/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/haveno/desktop/main/MainViewModel.java @@ -345,7 +345,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener havenoSetup.setChainFileLockedExceptionHandler(msg -> new Popup().warning(msg) .useShutDownButton() .show()); - havenoSetup.setLockedUpFundsHandler(msg -> new Popup().width(850).warning(msg).show()); + tradeManager.setLockedUpFundsHandler(msg -> new Popup().width(850).warning(msg).show()); havenoSetup.setDisplayUpdateHandler((alert, key) -> new DisplayUpdateDownloadWindow(alert, config) .actionButtonText(Res.get("displayUpdateDownloadWindow.button.downloadLater")) diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesDataModel.java index b7479d24b8..910b649145 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesDataModel.java @@ -27,6 +27,8 @@ import haveno.core.trade.ClosedTradableFormatter; import haveno.core.trade.ClosedTradableManager; import haveno.core.trade.ClosedTradableUtil; import haveno.core.trade.Tradable; +import haveno.core.trade.Trade; +import haveno.core.trade.TradeManager; import haveno.core.user.Preferences; import haveno.core.util.PriceUtil; import haveno.core.util.VolumeUtil; @@ -49,18 +51,21 @@ class ClosedTradesDataModel extends ActivatableDataModel { final AccountAgeWitnessService accountAgeWitnessService; private final ObservableList list = FXCollections.observableArrayList(); private final ListChangeListener tradesListChangeListener; + private final TradeManager tradeManager; @Inject public ClosedTradesDataModel(ClosedTradableManager closedTradableManager, ClosedTradableFormatter closedTradableFormatter, Preferences preferences, PriceFeedService priceFeedService, - AccountAgeWitnessService accountAgeWitnessService) { + AccountAgeWitnessService accountAgeWitnessService, + TradeManager tradeManager) { this.closedTradableManager = closedTradableManager; this.closedTradableFormatter = closedTradableFormatter; this.preferences = preferences; this.priceFeedService = priceFeedService; this.accountAgeWitnessService = accountAgeWitnessService; + this.tradeManager = tradeManager; tradesListChangeListener = change -> applyList(); } @@ -124,4 +129,8 @@ class ClosedTradesDataModel extends ActivatableDataModel { // We sort by date, the earliest first list.sort((o1, o2) -> o2.getTradable().getDate().compareTo(o1.getTradable().getDate())); } + + public void onMoveTradeToPendingTrades(Trade trade) { + tradeManager.onMoveClosedTradeToPendingTrades(trade); + } } diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesView.fxml b/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesView.fxml index d4b9e90cb6..230557d4db 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesView.fxml +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesView.fxml @@ -50,6 +50,7 @@ + diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesView.java b/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesView.java index 056da477f2..3257d21059 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesView.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/closedtrades/ClosedTradesView.java @@ -20,6 +20,8 @@ package haveno.desktop.main.portfolio.closedtrades; import com.google.inject.Inject; import com.google.inject.name.Named; import com.googlecode.jcsv.writer.CSVEntryConverter; +import com.jfoenix.controls.JFXButton; +import de.jensd.fx.fontawesome.AwesomeIcon; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import haveno.common.config.Config; import haveno.common.crypto.KeyRing; @@ -45,7 +47,7 @@ import haveno.desktop.main.overlays.windows.ClosedTradesSummaryWindow; import haveno.desktop.main.overlays.windows.OfferDetailsWindow; import haveno.desktop.main.overlays.windows.TradeDetailsWindow; import haveno.desktop.main.portfolio.presentation.PortfolioUtil; -import static haveno.desktop.util.FormBuilder.getRegularIconButton; +import haveno.desktop.util.FormBuilder; import haveno.desktop.util.GUIUtil; import haveno.network.p2p.NodeAddress; import java.util.Comparator; @@ -112,7 +114,7 @@ public class ClosedTradesView extends ActivatableViewAndModel priceColumn, deviationColumn, amountColumn, volumeColumn, tradeFeeColumn, buyerSecurityDepositColumn, sellerSecurityDepositColumn, - marketColumn, directionColumn, dateColumn, tradeIdColumn, stateColumn, + marketColumn, directionColumn, dateColumn, tradeIdColumn, stateColumn, removeTradeColumn, duplicateColumn, avatarColumn; @FXML FilterBox filterBox; @@ -186,6 +188,7 @@ public class ClosedTradesView extends ActivatableViewAndModel setRemoveTradeColumnCellFactory() { + removeTradeColumn.setCellValueFactory((trade) -> new ReadOnlyObjectWrapper<>(trade.getValue())); + removeTradeColumn.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + + @Override + public void updateItem(ClosedTradesListItem newItem, boolean empty) { + if (newItem == null || !(newItem.getTradable() instanceof Trade)) { + setGraphic(null); + return; + } + + Trade trade = (Trade) newItem.getTradable(); + super.updateItem(newItem, empty); + if (!empty && newItem != null && !trade.isPayoutConfirmed()) { + Label icon = FormBuilder.getIcon(AwesomeIcon.UNDO); + JFXButton iconButton = new JFXButton("", icon); + iconButton.setStyle("-fx-cursor: hand;"); + iconButton.getStyleClass().add("hidden-icon-button"); + iconButton.setTooltip(new Tooltip(Res.get("portfolio.failed.revertToPending"))); + iconButton.setOnAction(e -> onRevertTrade(trade)); + setGraphic(iconButton); + } else { + setGraphic(null); + } + } + }; + } + }); + return removeTradeColumn; + } + + private void onRevertTrade(Trade trade) { + new Popup().attention(Res.get("portfolio.failed.revertToPending.popup")) + .onAction(() -> onMoveTradeToPendingTrades(trade)) + .actionButtonText(Res.get("shared.yes")) + .closeButtonText(Res.get("shared.no")) + .show(); + } + + private void onMoveTradeToPendingTrades(Trade trade) { + model.dataModel.onMoveTradeToPendingTrades(trade); + } + private void onDuplicateOffer(Offer offer) { try { OfferPayload offerPayload = offer.getOfferPayload(); diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesDataModel.java index 0283acc249..dbbfa956ba 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/failedtrades/FailedTradesDataModel.java @@ -75,8 +75,7 @@ class FailedTradesDataModel extends ActivatableDataModel { } public void onMoveTradeToPendingTrades(Trade trade) { - failedTradesManager.removeTrade(trade); - tradeManager.addFailedTradeToPendingTrades(trade); + tradeManager.onMoveFailedTradeToPendingTrades(trade); } public void unfailTrade(Trade trade) {