diff --git a/common/src/main/java/bisq/common/config/Config.java b/common/src/main/java/bisq/common/config/Config.java index d7b0e0e4192..b959d4ec1c4 100644 --- a/common/src/main/java/bisq/common/config/Config.java +++ b/common/src/main/java/bisq/common/config/Config.java @@ -115,6 +115,7 @@ public class Config { public static final String GENESIS_TOTAL_SUPPLY = "genesisTotalSupply"; public static final String DAO_ACTIVATED = "daoActivated"; public static final String DUMP_DELAYED_PAYOUT_TXS = "dumpDelayedPayoutTxs"; + public static final String ALLOW_FAULTY_DELAYED_TXS = "allowFaultyDelayedTxs"; // Default values for certain options public static final int UNSPECIFIED_PORT = -1; @@ -197,6 +198,7 @@ public class Config { public final int genesisBlockHeight; public final long genesisTotalSupply; public final boolean dumpDelayedPayoutTxs; + public final boolean allowFaultyDelayedTxs; // Properties derived from options but not exposed as options themselves public final File torDir; @@ -606,6 +608,13 @@ public Config(String defaultAppName, File defaultUserDataDir, String... args) { .ofType(boolean.class) .defaultsTo(false); + ArgumentAcceptingOptionSpec allowFaultyDelayedTxsOpt = + parser.accepts(ALLOW_FAULTY_DELAYED_TXS, "Allow completion of trades with faulty delayed " + + "payout transactions") + .withRequiredArg() + .ofType(boolean.class) + .defaultsTo(false); + try { CompositeOptionSet options = new CompositeOptionSet(); @@ -717,6 +726,7 @@ public Config(String defaultAppName, File defaultUserDataDir, String... args) { this.genesisTotalSupply = options.valueOf(genesisTotalSupplyOpt); this.daoActivated = options.valueOf(daoActivatedOpt); this.dumpDelayedPayoutTxs = options.valueOf(dumpDelayedPayoutTxsOpt); + this.allowFaultyDelayedTxs = options.valueOf(allowFaultyDelayedTxsOpt); } catch (OptionException ex) { throw new ConfigException("problem parsing option '%s': %s", ex.options().get(0), diff --git a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java index ce5d2433ab4..3bb4721b17a 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java @@ -598,6 +598,13 @@ public AddressEntry getNewAddressEntry(String offerId, AddressEntry.Context cont return entry; } + public AddressEntry recoverAddressEntry(String offerId, String address, AddressEntry.Context context) { + var available = findAddressEntry(address, AddressEntry.Context.AVAILABLE); + if (!available.isPresent()) + return null; + return addressEntryList.swapAvailableToAddressEntryWithOfferId(available.get(), context, offerId); + } + private AddressEntry getOrCreateAddressEntry(AddressEntry.Context context, Optional addressEntry) { if (addressEntry.isPresent()) { return addressEntry.get(); diff --git a/core/src/main/java/bisq/core/trade/DelayedPayoutTxValidation.java b/core/src/main/java/bisq/core/trade/DelayedPayoutTxValidation.java index aaaf026e19f..0db6e9e50f8 100644 --- a/core/src/main/java/bisq/core/trade/DelayedPayoutTxValidation.java +++ b/core/src/main/java/bisq/core/trade/DelayedPayoutTxValidation.java @@ -58,6 +58,12 @@ public static class InvalidTxException extends Exception { } } + public static class AmountMismatchException extends Exception { + AmountMismatchException(String msg) { + super(msg); + } + } + public static class InvalidLockTimeException extends Exception { InvalidLockTimeException(String msg) { super(msg); @@ -69,7 +75,7 @@ public static void validatePayoutTx(Trade trade, DaoFacade daoFacade, BtcWalletService btcWalletService) throws DonationAddressException, MissingDelayedPayoutTxException, - InvalidTxException, InvalidLockTimeException { + InvalidTxException, InvalidLockTimeException, AmountMismatchException { String errorMsg; if (delayedPayoutTx == null) { errorMsg = "DelayedPayoutTx must not be null"; @@ -122,7 +128,7 @@ public static void validatePayoutTx(Trade trade, errorMsg = "Output value of deposit tx and delayed payout tx is not matching. Output: " + output + " / msOutputAmount: " + msOutputAmount; log.error(errorMsg); log.error(delayedPayoutTx.toString()); - throw new InvalidTxException(errorMsg); + throw new AmountMismatchException(errorMsg); } diff --git a/core/src/main/java/bisq/core/trade/TradeManager.java b/core/src/main/java/bisq/core/trade/TradeManager.java index 5aad36e4fff..e755789b312 100644 --- a/core/src/main/java/bisq/core/trade/TradeManager.java +++ b/core/src/main/java/bisq/core/trade/TradeManager.java @@ -57,6 +57,7 @@ import bisq.network.p2p.SendMailboxMessageListener; import bisq.common.ClockWatcher; +import bisq.common.config.Config; import bisq.common.crypto.KeyRing; import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.FaultHandler; @@ -64,6 +65,8 @@ import bisq.common.proto.network.NetworkEnvelope; import bisq.common.proto.persistable.PersistedDataHost; import bisq.common.storage.Storage; +import bisq.common.util.Tuple2; +import bisq.common.util.Utilities; import org.bitcoinj.core.AddressFormatException; import org.bitcoinj.core.Coin; @@ -72,6 +75,7 @@ import org.bitcoinj.core.TransactionConfidence; import javax.inject.Inject; +import javax.inject.Named; import com.google.common.util.concurrent.FutureCallback; @@ -89,6 +93,7 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -142,6 +147,8 @@ public class TradeManager implements PersistedDataHost { @Getter private final ObservableList tradesWithoutDepositTx = FXCollections.observableArrayList(); private final DumpDelayedPayoutTx dumpDelayedPayoutTx; + @Getter + private final boolean allowFaultyDelayedTxs; /////////////////////////////////////////////////////////////////////////////////////////// @@ -169,7 +176,8 @@ public TradeManager(User user, DaoFacade daoFacade, ClockWatcher clockWatcher, Storage> storage, - DumpDelayedPayoutTx dumpDelayedPayoutTx) { + DumpDelayedPayoutTx dumpDelayedPayoutTx, + @Named(Config.ALLOW_FAULTY_DELAYED_TXS) boolean allowFaultyDelayedTxs) { this.user = user; this.keyRing = keyRing; this.btcWalletService = btcWalletService; @@ -190,6 +198,7 @@ public TradeManager(User user, this.daoFacade = daoFacade; this.clockWatcher = clockWatcher; this.dumpDelayedPayoutTx = dumpDelayedPayoutTx; + this.allowFaultyDelayedTxs = allowFaultyDelayedTxs; tradableListStorage = storage; @@ -225,6 +234,7 @@ public TradeManager(User user, } } }); + failedTradesManager.setUnfailTradeCallback(this::unfailTrade); } @Override @@ -279,10 +289,7 @@ private void initPendingTrades() { tradableList.forEach(trade -> { if (trade.isDepositPublished() || (trade.isTakerFeePublished() && !trade.hasFailed())) { - initTrade(trade, trade.getProcessModel().isUseSavingsWallet(), - trade.getProcessModel().getFundsNeededForTradeAsLong()); - trade.updateDepositTxFromWallet(); - tradesForStatistics.add(trade); + initPendingTrade(trade); } else if (trade.isTakerFeePublished() && !trade.isFundsLockedIn()) { addTradeToFailedTradesList.add(trade); trade.appendErrorMessage("Invalid state: trade.isTakerFeePublished() && !trade.isFundsLockedIn()"); @@ -298,20 +305,23 @@ private void initPendingTrades() { tradesWithoutDepositTx.add(trade); } - try { - DelayedPayoutTxValidation.validatePayoutTx(trade, - trade.getDelayedPayoutTx(), - daoFacade, - btcWalletService); - } catch (DelayedPayoutTxValidation.DonationAddressException | - DelayedPayoutTxValidation.InvalidTxException | - DelayedPayoutTxValidation.InvalidLockTimeException | - DelayedPayoutTxValidation.MissingDelayedPayoutTxException e) { - // We move it to failed trades so it cannot be continued. - log.warn("We move the trade with ID '{}' to failed trades because of exception {}", - trade.getId(), e.getMessage()); - addTradeToFailedTradesList.add(trade); - } + try { + DelayedPayoutTxValidation.validatePayoutTx(trade, + trade.getDelayedPayoutTx(), + daoFacade, + btcWalletService); + } catch (DelayedPayoutTxValidation.DonationAddressException | + DelayedPayoutTxValidation.InvalidTxException | + DelayedPayoutTxValidation.InvalidLockTimeException | + DelayedPayoutTxValidation.MissingDelayedPayoutTxException | + DelayedPayoutTxValidation.AmountMismatchException e) { + log.warn("Delayed payout tx exception, trade {}, exception {}", trade.getId(), e.getMessage()); + if (!allowFaultyDelayedTxs) { + // We move it to failed trades so it cannot be continued. + log.warn("We move the trade with ID '{}' to failed trades", trade.getId()); + addTradeToFailedTradesList.add(trade); + } + } } ); @@ -336,6 +346,13 @@ private void initPendingTrades() { pendingTradesInitialized.set(true); } + private void initPendingTrade(Trade trade) { + initTrade(trade, trade.getProcessModel().isUseSavingsWallet(), + trade.getProcessModel().getFundsNeededForTradeAsLong()); + trade.updateDepositTxFromWallet(); + tradesForStatistics.add(trade); + } + private void onTradesChanged() { this.numPendingTrades.set(tradableList.getList().size()); } @@ -602,6 +619,38 @@ public void addTradeToFailedTrades(Trade trade) { cleanUpAddressEntries(); } + // If trade still has funds locked up it might come back from failed trades + // Aborts unfailing if the address entries needed are not available + private boolean unfailTrade(Trade trade) { + if (!recoverAddresses(trade)) { + log.warn("Failed to recover address during unfail trade"); + return false; + } + + initPendingTrade(trade); + + if (!tradableList.contains(trade)) { + tradableList.add(trade); + } + return true; + } + + // The trade is added to pending trades if the associated address entries are AVAILABLE and + // the relevant entries are changed, otherwise it's not added and no address entries are changed + private boolean recoverAddresses(Trade trade) { + // Find addresses associated with this trade. + var entries = TradeUtils.getAvailableAddresses(trade, btcWalletService, keyRing); + if (entries == null) + return false; + + btcWalletService.recoverAddressEntry(trade.getId(), entries.first, + AddressEntry.Context.MULTI_SIG); + btcWalletService.recoverAddressEntry(trade.getId(), entries.second, + AddressEntry.Context.TRADE_PAYOUT); + return true; + } + + // If trade is in preparation (if taker role: before taker fee is paid; both roles: before deposit published) // we just remove the trade from our list. We don't store those trades. public void removePreparedTrade(Trade trade) { diff --git a/core/src/main/java/bisq/core/trade/TradeModule.java b/core/src/main/java/bisq/core/trade/TradeModule.java index b421b9d149b..bdf5ea78e1f 100644 --- a/core/src/main/java/bisq/core/trade/TradeModule.java +++ b/core/src/main/java/bisq/core/trade/TradeModule.java @@ -33,6 +33,7 @@ import com.google.inject.Singleton; +import static bisq.common.config.Config.ALLOW_FAULTY_DELAYED_TXS; import static bisq.common.config.Config.DUMP_DELAYED_PAYOUT_TXS; import static bisq.common.config.Config.DUMP_STATISTICS; import static com.google.inject.name.Names.named; @@ -58,5 +59,6 @@ protected void configure() { bind(AssetTradeActivityCheck.class).in(Singleton.class); bindConstant().annotatedWith(named(DUMP_STATISTICS)).to(config.dumpStatistics); bindConstant().annotatedWith(named(DUMP_DELAYED_PAYOUT_TXS)).to(config.dumpDelayedPayoutTxs); + bindConstant().annotatedWith(named(ALLOW_FAULTY_DELAYED_TXS)).to(config.allowFaultyDelayedTxs); } } diff --git a/core/src/main/java/bisq/core/trade/TradeUtils.java b/core/src/main/java/bisq/core/trade/TradeUtils.java new file mode 100644 index 00000000000..10a5da8fdf8 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/TradeUtils.java @@ -0,0 +1,79 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.trade; + +import bisq.core.btc.wallet.BtcWalletService; + +import bisq.common.crypto.KeyRing; +import bisq.common.util.Tuple2; +import bisq.common.util.Utilities; + +import java.util.Objects; + +public class TradeUtils { + + // Returns if both are AVAILABLE, otherwise null + static Tuple2 getAvailableAddresses(Trade trade, BtcWalletService btcWalletService, + KeyRing keyRing) { + var addresses = getTradeAddresses(trade, btcWalletService, keyRing); + if (addresses == null) + return null; + + if (btcWalletService.getAvailableAddressEntries().stream() + .noneMatch(e -> Objects.equals(e.getAddressString(), addresses.first))) + return null; + if (btcWalletService.getAvailableAddressEntries().stream() + .noneMatch(e -> Objects.equals(e.getAddressString(), addresses.second))) + return null; + + return new Tuple2<>(addresses.first, addresses.second); + } + + // Returns addresses as strings if they're known by the wallet + public static Tuple2 getTradeAddresses(Trade trade, BtcWalletService btcWalletService, + KeyRing keyRing) { + var contract = trade.getContract(); + if (contract == null) + return null; + + // Get multisig address + var isMyRoleBuyer = contract.isMyRoleBuyer(keyRing.getPubKeyRing()); + var multiSigPubKey = isMyRoleBuyer ? contract.getBuyerMultiSigPubKey() : contract.getSellerMultiSigPubKey(); + if (multiSigPubKey == null) + return null; + var multiSigPubKeyString = Utilities.bytesAsHexString(multiSigPubKey); + var multiSigAddress = btcWalletService.getAddressEntryListAsImmutableList().stream() + .filter(e -> e.getKeyPair().getPublicKeyAsHex().equals(multiSigPubKeyString)) + .findAny() + .orElse(null); + if (multiSigAddress == null) + return null; + + // Get payout address + var payoutAddress = isMyRoleBuyer ? + contract.getBuyerPayoutAddressString() : contract.getSellerPayoutAddressString(); + var payoutAddressEntry = btcWalletService.getAddressEntryListAsImmutableList().stream() + .filter(e -> Objects.equals(e.getAddressString(), payoutAddress)) + .findAny() + .orElse(null); + if (payoutAddressEntry == null) + return null; + + return new Tuple2<>(multiSigAddress.getAddressString(), payoutAddress); + } +} diff --git a/core/src/main/java/bisq/core/trade/failed/FailedTradesManager.java b/core/src/main/java/bisq/core/trade/failed/FailedTradesManager.java index 7733de0ff31..ed7b633847e 100644 --- a/core/src/main/java/bisq/core/trade/failed/FailedTradesManager.java +++ b/core/src/main/java/bisq/core/trade/failed/FailedTradesManager.java @@ -17,27 +17,35 @@ package bisq.core.trade.failed; +import bisq.core.btc.model.AddressEntry; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.offer.Offer; import bisq.core.provider.price.PriceFeedService; import bisq.core.trade.DumpDelayedPayoutTx; import bisq.core.trade.TradableList; import bisq.core.trade.Trade; +import bisq.core.trade.TradeUtils; +import bisq.common.config.Config; import bisq.common.crypto.KeyRing; import bisq.common.proto.persistable.PersistedDataHost; import bisq.common.storage.Storage; import com.google.inject.Inject; +import javax.inject.Named; + import javafx.collections.ObservableList; import java.util.Optional; +import java.util.function.Function; import java.util.stream.Stream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import lombok.Setter; + public class FailedTradesManager implements PersistedDataHost { private static final Logger log = LoggerFactory.getLogger(FailedTradesManager.class); private TradableList failedTrades; @@ -46,6 +54,8 @@ public class FailedTradesManager implements PersistedDataHost { private final BtcWalletService btcWalletService; private final Storage> tradableListStorage; private final DumpDelayedPayoutTx dumpDelayedPayoutTx; + @Setter + private Function unfailTradeCallback; @Inject public FailedTradesManager(KeyRing keyRing, @@ -96,4 +106,32 @@ public Stream getTradesStreamWithFundsLockedIn() { return failedTrades.stream() .filter(Trade::isFundsLockedIn); } + + public void unfailTrade(Trade trade) { + if (unfailTradeCallback == null) + return; + + if (unfailTradeCallback.apply(trade)) { + log.info("Unfailing trade {}", trade.getId()); + failedTrades.remove(trade); + } + } + + public String checkUnfail(Trade trade) { + var addresses = TradeUtils.getTradeAddresses(trade, btcWalletService, keyRing); + if (addresses == null) { + return "Addresses not found"; + } + StringBuilder blockingTrades = new StringBuilder(); + for (var entry : btcWalletService.getAddressEntryListAsImmutableList()) { + if (entry.getContext() == AddressEntry.Context.AVAILABLE) + continue; + if (entry.getAddressString() != null && + (entry.getAddressString().equals(addresses.first) || + entry.getAddressString().equals(addresses.second))) { + blockingTrades.append(entry.getOfferId()).append(", "); + } + } + return blockingTrades.toString(); + } } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java index d0ebb8a509b..468ec7dca81 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java @@ -50,7 +50,8 @@ protected void run() { } catch (DelayedPayoutTxValidation.DonationAddressException | DelayedPayoutTxValidation.MissingDelayedPayoutTxException | DelayedPayoutTxValidation.InvalidTxException | - DelayedPayoutTxValidation.InvalidLockTimeException e) { + DelayedPayoutTxValidation.InvalidLockTimeException | + DelayedPayoutTxValidation.AmountMismatchException e) { failed(e.getMessage()); } catch (Throwable t) { failed(t); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java index ea9c2a7cbf4..3242ba8cf6a 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java @@ -46,7 +46,8 @@ protected void run() { } catch (DelayedPayoutTxValidation.DonationAddressException | DelayedPayoutTxValidation.MissingDelayedPayoutTxException | DelayedPayoutTxValidation.InvalidTxException | - DelayedPayoutTxValidation.InvalidLockTimeException e) { + DelayedPayoutTxValidation.InvalidLockTimeException | + DelayedPayoutTxValidation.AmountMismatchException e) { failed(e.getMessage()); } catch (Throwable t) { failed(t); diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index dcfaed095f1..1c2e7febef5 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -557,9 +557,8 @@ portfolio.tab.history=History portfolio.tab.failed=Failed portfolio.tab.editOpenOffer=Edit offer -portfolio.pending.invalidDelayedPayoutTx=The donation address in the delayed payout transaction is invalid.\n\n\ - Please do NOT send the Altcoin or Fiat payment but contact the Bisq developers at 'https://bisq.community' or \ - the Keybase channel.\n\n\ +portfolio.pending.invalidDelayedPayoutTx=Please do NOT send the Altcoin or Fiat payment but contact the Bisq \ + developers at 'https://bisq.community' or the Keybase channel.\n\n\ {0} portfolio.pending.step1.waitForConf=Wait for blockchain confirmation @@ -791,6 +790,8 @@ portfolio.pending.error.depositTxNull=The deposit transaction is null. You canno For further help please contact the Bisq support channel at the Bisq Keybase team. portfolio.pending.mediationResult.error.depositTxNull=The deposit transaction is null. The trade gets moved to the \ failed trades section. +portfolio.pending.mediationResult.error.delayedPayoutTxNull=The delayed payout transaction is null. The trade gets \ + moved to the failed trades section. portfolio.pending.error.depositTxNotConfirmed=The deposit transaction is not confirmed. You can not open an arbitration dispute \ with an unconfirmed deposit transaction. Please wait until it is confirmed or go to \"Settings/Network info\" and do a SPV resync.\n\n\ For further help please contact the Bisq support channel at the Bisq Keybase team. @@ -859,6 +860,13 @@ portfolio.closed.ticketClosed=Arbitrated portfolio.closed.mediationTicketClosed=Mediated portfolio.closed.canceled=Canceled portfolio.failed.Failed=Failed +portfolio.failed.unfail=Before proceeding, make sure you have a backup of your data directory!\n\ + Do you want to move this trade back to open trades?\n\ + This is a way to unlock funds stuck in a failed trade. +portfolio.failed.cantUnfail=This trade cannot be moved back to open trades at the moment. \n\ + Try again after completion of trade(s) {0} +portfolio.failed.depositTxNull=The trade cannot be completed. Deposit transaction is null +portfolio.failed.delayedPayoutTxNull=The trade cannot be completed. Delayed payout transaction is null #################################################################### diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/failedtrades/FailedTradesDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/failedtrades/FailedTradesDataModel.java index ca971fe061f..2b7dd31f7e5 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/failedtrades/FailedTradesDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/failedtrades/FailedTradesDataModel.java @@ -74,4 +74,11 @@ private void applyList() { list.sort((o1, o2) -> o2.getTrade().getDate().compareTo(o1.getTrade().getDate())); } + public void unfailTrade(Trade trade) { + failedTradesManager.unfailTrade(trade); + } + + public String checkUnfail(Trade trade) { + return failedTradesManager.checkUnfail(trade); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/failedtrades/FailedTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/failedtrades/FailedTradesView.java index 940feda4c63..2f22f370633 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/failedtrades/FailedTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/failedtrades/FailedTradesView.java @@ -21,22 +21,33 @@ import bisq.desktop.common.view.FxmlView; import bisq.desktop.components.AutoTooltipLabel; import bisq.desktop.components.HyperlinkWithIcon; +import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.windows.TradeDetailsWindow; import bisq.core.locale.Res; +import bisq.core.trade.Trade; + +import bisq.common.config.Config; +import bisq.common.util.Utilities; import javax.inject.Inject; +import javax.inject.Named; import javafx.fxml.FXML; +import javafx.scene.Scene; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.Tooltip; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; import javafx.scene.layout.VBox; import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.event.EventHandler; + import javafx.collections.transformation.SortedList; import javafx.util.Callback; @@ -53,11 +64,17 @@ public class FailedTradesView extends ActivatableViewAndModel sortedList; + private EventHandler keyEventEventHandler; + private Scene scene; + private final boolean allowFaultyDelayedTxs; @Inject - public FailedTradesView(FailedTradesViewModel model, TradeDetailsWindow tradeDetailsWindow) { + public FailedTradesView(FailedTradesViewModel model, + TradeDetailsWindow tradeDetailsWindow, + @Named(Config.ALLOW_FAULTY_DELAYED_TXS) boolean allowFaultyDelayedTxs) { super(model); this.tradeDetailsWindow = tradeDetailsWindow; + this.allowFaultyDelayedTxs = allowFaultyDelayedTxs; } @Override @@ -95,10 +112,59 @@ public void initialize() { dateColumn.setSortType(TableColumn.SortType.DESCENDING); tableView.getSortOrder().add(dateColumn); + keyEventEventHandler = keyEvent -> { + if (Utilities.isAltOrCtrlPressed(KeyCode.Y, keyEvent)) { + var checkTxs = checkTxs(); + var checkUnfailString = checkUnfail(); + if (!checkTxs.isEmpty()) { + log.warn("Cannot unfail, error {}", checkTxs); + new Popup().warning(checkTxs) + .show(); + } else if (!checkUnfailString.isEmpty()) { + log.warn("Cannot unfail, error {}", checkUnfailString); + new Popup().warning(Res.get("portfolio.failed.cantUnfail", checkUnfailString)) + .show(); + } else { + new Popup().warning(Res.get("portfolio.failed.unfail")) + .onAction(this::onUnfail) + .show(); + } + } + }; + } + + private void onUnfail() { + Trade trade = sortedList.get(tableView.getSelectionModel().getFocusedIndex()).getTrade(); + model.dataModel.unfailTrade(trade); + } + + private String checkUnfail() { + Trade trade = sortedList.get(tableView.getSelectionModel().getFocusedIndex()).getTrade(); + return model.dataModel.checkUnfail(trade); + } + + private String checkTxs() { + Trade trade = sortedList.get(tableView.getSelectionModel().getFocusedIndex()).getTrade(); + log.info("Initiated unfail of trade {}", trade.getId()); + if (trade.getDepositTx() == null) { + log.info("Check unfail found no depositTx for trade {}", trade.getId()); + return Res.get("portfolio.failed.depositTxNull"); + } + if (trade.getDelayedPayoutTxBytes() == null) { + log.info("Check unfail found no delayedPayoutTxBytes for trade {}", trade.getId()); + if (!allowFaultyDelayedTxs) { + return Res.get("portfolio.failed.delayedPayoutTxNull"); + } + } + return ""; } @Override protected void activate() { + scene = root.getScene(); + if (scene != null) { + scene.addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); + } sortedList = new SortedList<>(model.getList()); sortedList.comparatorProperty().bind(tableView.comparatorProperty()); tableView.setItems(sortedList); @@ -106,6 +172,9 @@ protected void activate() { @Override protected void deactivate() { + if (scene != null) { + scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); + } sortedList.comparatorProperty().unbind(); } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java index 49d79a0874a..9f5276f9b97 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java @@ -591,6 +591,7 @@ private void doOpenDispute(boolean isSupportTicket, Transaction depositTx) { resultHandler = () -> navigation.navigateTo(MainView.class, SupportView.class, RefundClientView.class); if (trade.getDelayedPayoutTx() == null) { + log.error("Delayed payout tx is missing"); return; } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java index ef82e67448d..4945754fdb5 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java @@ -522,12 +522,17 @@ private void openMediationResultPopup(String headLine) { return; } - if (trade.getDepositTx() == null || trade.getDelayedPayoutTx() == null) { - log.error("trade.getDepositTx() or trade.getDelayedPayoutTx() was null at openMediationResultPopup. " + + if (trade.getDepositTx() == null) { + log.error("trade.getDepositTx() was null at openMediationResultPopup. " + "We add the trade to failed trades. TradeId={}", trade.getId()); model.dataModel.addTradeToFailedTrades(); new Popup().warning(Res.get("portfolio.pending.mediationResult.error.depositTxNull")).show(); return; + } else if (trade.getDelayedPayoutTx() == null) { + log.error("trade.getDelayedPayoutTx() was null at openMediationResultPopup. " + + "We add the trade to failed trades. TradeId={}", trade.getId()); + new Popup().warning(Res.get("portfolio.pending.mediationResult.error.delayedPayoutTxNull")).show(); + return; } DisputeResult disputeResult = optionalDispute.get().getDisputeResultProperty().get(); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep1View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep1View.java index 9c90991880b..184dcd038f9 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep1View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep1View.java @@ -45,8 +45,11 @@ public void activate() { model.dataModel.btcWalletService); } catch (DelayedPayoutTxValidation.DonationAddressException | DelayedPayoutTxValidation.InvalidTxException | + DelayedPayoutTxValidation.AmountMismatchException | DelayedPayoutTxValidation.InvalidLockTimeException e) { - new Popup().warning(Res.get("portfolio.pending.invalidDelayedPayoutTx", e.getMessage())).show(); + if (!model.dataModel.tradeManager.isAllowFaultyDelayedTxs()) { + new Popup().warning(Res.get("portfolio.pending.invalidDelayedPayoutTx", e.getMessage())).show(); + } } catch (DelayedPayoutTxValidation.MissingDelayedPayoutTxException ignore) { // We don't react on those errors as a failed trade might get listed initially but getting removed from the // trade manager after initPendingTrades which happens after activate might be called. diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java index fb212e170b2..80826ccbde4 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java @@ -122,8 +122,11 @@ public void activate() { model.dataModel.btcWalletService); } catch (DelayedPayoutTxValidation.DonationAddressException | DelayedPayoutTxValidation.InvalidTxException | + DelayedPayoutTxValidation.AmountMismatchException | DelayedPayoutTxValidation.InvalidLockTimeException e) { - new Popup().warning(Res.get("portfolio.pending.invalidDelayedPayoutTx", e.getMessage())).show(); + if (!model.dataModel.tradeManager.isAllowFaultyDelayedTxs()) { + new Popup().warning(Res.get("portfolio.pending.invalidDelayedPayoutTx", e.getMessage())).show(); + } } catch (DelayedPayoutTxValidation.MissingDelayedPayoutTxException ignore) { // We don't react on those errors as a failed trade might get listed initially but getting removed from the // trade manager after initPendingTrades which happens after activate might be called.