recover from failed payout tx

This commit is contained in:
woodser 2024-05-15 07:32:45 -04:00
parent 7847460f11
commit 7e3d89797e
12 changed files with 124 additions and 59 deletions

View file

@ -77,7 +77,7 @@ public class HavenoHeadlessApp implements HeadlessApp {
}); });
havenoSetup.setDisplayTorNetworkSettingsHandler(show -> log.info("onDisplayTorNetworkSettingsHandler: show={}", show)); havenoSetup.setDisplayTorNetworkSettingsHandler(show -> log.info("onDisplayTorNetworkSettingsHandler: show={}", show));
havenoSetup.setChainFileLockedExceptionHandler(msg -> log.error("onChainFileLockedExceptionHandler: msg={}", msg)); 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.setShowFirstPopupIfResyncSPVRequestedHandler(() -> log.info("onShowFirstPopupIfResyncSPVRequestedHandler"));
havenoSetup.setDisplayUpdateHandler((alert, key) -> log.info("onDisplayUpdateHandler")); havenoSetup.setDisplayUpdateHandler((alert, key) -> log.info("onDisplayUpdateHandler"));
havenoSetup.setDisplayAlertHandler(alert -> log.info("onDisplayAlertHandler. alert={}", alert)); havenoSetup.setDisplayAlertHandler(alert -> log.info("onDisplayAlertHandler. alert={}", alert));

View file

@ -66,13 +66,11 @@ import haveno.core.support.dispute.mediation.MediationManager;
import haveno.core.support.dispute.refund.RefundManager; import haveno.core.support.dispute.refund.RefundManager;
import haveno.core.trade.HavenoUtils; import haveno.core.trade.HavenoUtils;
import haveno.core.trade.TradeManager; import haveno.core.trade.TradeManager;
import haveno.core.trade.TradeTxException;
import haveno.core.user.Preferences; import haveno.core.user.Preferences;
import haveno.core.user.Preferences.UseTorForXmr; import haveno.core.user.Preferences.UseTorForXmr;
import haveno.core.user.User; import haveno.core.user.User;
import haveno.core.util.FormattingUtils; import haveno.core.util.FormattingUtils;
import haveno.core.util.coin.CoinFormatter; import haveno.core.util.coin.CoinFormatter;
import haveno.core.xmr.model.AddressEntry;
import haveno.core.xmr.setup.WalletsSetup; import haveno.core.xmr.setup.WalletsSetup;
import haveno.core.xmr.wallet.BtcWalletService; import haveno.core.xmr.wallet.BtcWalletService;
import haveno.core.xmr.wallet.WalletsManager; import haveno.core.xmr.wallet.WalletsManager;
@ -92,7 +90,6 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Random; import java.util.Random;
import java.util.Scanner; import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -107,7 +104,6 @@ import javax.annotation.Nullable;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.bitcoinj.core.Coin;
import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.monadic.MonadicBinding; import org.fxmisc.easybind.monadic.MonadicBinding;
@ -449,11 +445,7 @@ public class HavenoSetup {
walletAppSetup.init(chainFileLockedExceptionHandler, walletAppSetup.init(chainFileLockedExceptionHandler,
showFirstPopupIfResyncSPVRequestedHandler, showFirstPopupIfResyncSPVRequestedHandler,
showPopupIfInvalidBtcConfigHandler, showPopupIfInvalidBtcConfigHandler,
() -> { () -> {},
if (allBasicServicesInitialized) {
checkForLockedUpFunds();
}
},
() -> {}); () -> {});
} }
@ -466,10 +458,6 @@ public class HavenoSetup {
revolutAccountsUpdateHandler, revolutAccountsUpdateHandler,
amazonGiftCardAccountsUpdateHandler); amazonGiftCardAccountsUpdateHandler);
if (xmrWalletService.downloadPercentageProperty().get() == 1) {
checkForLockedUpFunds();
}
alertManager.alertMessageProperty().addListener((observable, oldValue, newValue) -> alertManager.alertMessageProperty().addListener((observable, oldValue, newValue) ->
displayAlertIfPresent(newValue, false)); displayAlertIfPresent(newValue, false));
displayAlertIfPresent(alertManager.alertMessageProperty().get(), false); displayAlertIfPresent(alertManager.alertMessageProperty().get(), false);
@ -484,32 +472,6 @@ public class HavenoSetup {
// Utils // Utils
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
private void checkForLockedUpFunds() {
// We check if there are locked up funds in failed or closed trades
try {
Set<String> 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 @Nullable
public static String getLastHavenoVersion() { public static String getLastHavenoVersion() {
File versionFile = getVersionFile(); File versionFile = getVersionFile();

View file

@ -227,4 +227,12 @@ public class ClosedTradableManager implements PersistedDataHost {
private void requestPersistence() { private void requestPersistence() {
persistenceManager.requestPersistence(); persistenceManager.requestPersistence();
} }
public void removeTrade(Trade trade) {
synchronized (closedTradables) {
if (closedTradables.remove(trade)) {
requestPersistence();
}
}
}
} }

View file

@ -1408,7 +1408,7 @@ public abstract class Trade implements Tradable, Model {
// check if deposit published // check if deposit published
if (isDepositsPublished()) { if (isDepositsPublished()) {
restorePublishedTrade(); restoreDepositsPublishedTrade();
return; return;
} }
@ -1446,7 +1446,7 @@ public abstract class Trade implements Tradable, Model {
// listen for deposits published to restore trade // listen for deposits published to restore trade
protocolErrorStateSubscription = EasyBind.subscribe(stateProperty(), state -> { protocolErrorStateSubscription = EasyBind.subscribe(stateProperty(), state -> {
if (isDepositsPublished()) { if (isDepositsPublished()) {
restorePublishedTrade(); restoreDepositsPublishedTrade();
if (protocolErrorStateSubscription != null) { // unsubscribe if (protocolErrorStateSubscription != null) { // unsubscribe
protocolErrorStateSubscription.unsubscribe(); protocolErrorStateSubscription.unsubscribe();
protocolErrorStateSubscription = null; protocolErrorStateSubscription = null;
@ -1496,7 +1496,7 @@ public abstract class Trade implements Tradable, Model {
}); });
} }
private void restorePublishedTrade() { private void restoreDepositsPublishedTrade() {
// close open offer // close open offer
if (this instanceof MakerTrade && processModel.getOpenOfferManager().getOpenOfferById(getId()).isPresent()) { if (this instanceof MakerTrade && processModel.getOpenOfferManager().getOpenOfferById(getId()).isPresent()) {
@ -2370,15 +2370,23 @@ public abstract class Trade implements Tradable, Model {
setDepositTxs(txs); setDepositTxs(txs);
// check if any outputs spent (observed on payout published) // check if any outputs spent (observed on payout published)
boolean hasSpentOutput = false;
boolean hasFailedTx = false;
for (MoneroTxWallet tx : txs) { for (MoneroTxWallet tx : txs) {
if (tx.isFailed()) hasFailedTx = true;
for (MoneroOutputWallet output : tx.getOutputsWallet()) { 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) // check for outgoing txs (appears after wallet submits payout tx or on payout confirmed)
for (MoneroTxWallet tx : txs) { for (MoneroTxWallet tx : txs) {
if (tx.isOutgoing()) { if (tx.isOutgoing() && !tx.isFailed()) {
setPayoutTx(tx); setPayoutTx(tx);
setPayoutStatePublished(); setPayoutStatePublished();
if (tx.isConfirmed()) setPayoutStateConfirmed(); if (tx.isConfirmed()) setPayoutStateConfirmed();
@ -2460,6 +2468,10 @@ public abstract class Trade implements Tradable, Model {
if (!isPayoutUnlocked()) setPayoutState(PayoutState.PAYOUT_UNLOCKED); if (!isPayoutUnlocked()) setPayoutState(PayoutState.PAYOUT_UNLOCKED);
} }
private Trade getTrade() {
return this;
}
/** /**
* Listen to block notifications from the main wallet in order to sync * Listen to block notifications from the main wallet in order to sync
* idling trade wallets awaiting the payout to confirm or unlock. * idling trade wallets awaiting the payout to confirm or unlock.
@ -2485,9 +2497,10 @@ public abstract class Trade implements Tradable, Model {
try { try {
// get payout height if unknown // get payout height if unknown
if (payoutHeight == null && getPayoutTxId() != null) { if (payoutHeight == null && getPayoutTxId() != null && isPayoutPublished()) {
MoneroTx tx = xmrWalletService.getDaemon().getTx(getPayoutTxId()); 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 // sync wallet if confirm or unlock expected

View file

@ -119,6 +119,7 @@ import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
@ -129,6 +130,7 @@ import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import lombok.Getter; import lombok.Getter;
import lombok.Setter;
import monero.daemon.model.MoneroTx; import monero.daemon.model.MoneroTx;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
import org.bouncycastle.crypto.params.KeyParameter; import org.bouncycastle.crypto.params.KeyParameter;
@ -174,6 +176,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
private final LongProperty numPendingTrades = new SimpleLongProperty(); private final LongProperty numPendingTrades = new SimpleLongProperty();
private final ReferralIdService referralIdService; private final ReferralIdService referralIdService;
@Setter
@Nullable
private Consumer<String> lockedUpFundsHandler; // TODO: this is unused
// set comparator for processing mailbox messages // set comparator for processing mailbox messages
static { static {
MailboxMessageService.setMailboxMessageComparator(new MailboxMessageComparator()); 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()); log.warn("Swapping pending {} entries at startup. offerId={}", addressEntry.getContext(), addressEntry.getOfferId());
xmrWalletService.swapAddressEntryToAvailable(addressEntry.getOfferId(), addressEntry.getContext()); xmrWalletService.swapAddressEntryToAvailable(addressEntry.getOfferId(), addressEntry.getContext());
}); });
checkForLockedUpFunds();
} }
// notify that persisted trades initialized // notify that persisted trades initialized
@ -1040,15 +1048,21 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
} }
public void onMoveFailedTradeToPendingTrades(Trade trade) { public void onMoveFailedTradeToPendingTrades(Trade trade) {
addFailedTradeToPendingTrades(trade); addTradeToPendingTrades(trade);
failedTradesManager.removeTrade(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); failedTradesManager.removeTrade(trade);
} }
public void addFailedTradeToPendingTrades(Trade trade) { private void addTradeToPendingTrades(Trade trade) {
if (!trade.isInitialized()) { if (!trade.isInitialized()) {
initPersistedTrade(trade); 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<String> getSetOfFailedOrClosedTradeIdsFromLockedInFunds() throws TradeTxException { public Set<String> getSetOfFailedOrClosedTradeIdsFromLockedInFunds() throws TradeTxException {
AtomicReference<TradeTxException> tradeTxException = new AtomicReference<>(); AtomicReference<TradeTxException> tradeTxException = new AtomicReference<>();
synchronized (tradableList) { synchronized (tradableList) {

View file

@ -285,7 +285,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
synchronized (trade) { synchronized (trade) {
// skip if no need to reprocess // 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; return;
} }

View file

@ -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 if (trade.getSeller().getNodeAddress().equals(trade.getBuyer().getNodeAddress())) trade.getBuyer().setNodeAddress(null); // tests can reuse addresses
// ack and complete if already processed // 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"); log.warn("Received another PaymentReceivedMessage which was already processed, ACKing");
complete(); complete();
return; return;

View file

@ -345,7 +345,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener
havenoSetup.setChainFileLockedExceptionHandler(msg -> new Popup().warning(msg) havenoSetup.setChainFileLockedExceptionHandler(msg -> new Popup().warning(msg)
.useShutDownButton() .useShutDownButton()
.show()); .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) havenoSetup.setDisplayUpdateHandler((alert, key) -> new DisplayUpdateDownloadWindow(alert, config)
.actionButtonText(Res.get("displayUpdateDownloadWindow.button.downloadLater")) .actionButtonText(Res.get("displayUpdateDownloadWindow.button.downloadLater"))

View file

@ -27,6 +27,8 @@ import haveno.core.trade.ClosedTradableFormatter;
import haveno.core.trade.ClosedTradableManager; import haveno.core.trade.ClosedTradableManager;
import haveno.core.trade.ClosedTradableUtil; import haveno.core.trade.ClosedTradableUtil;
import haveno.core.trade.Tradable; import haveno.core.trade.Tradable;
import haveno.core.trade.Trade;
import haveno.core.trade.TradeManager;
import haveno.core.user.Preferences; import haveno.core.user.Preferences;
import haveno.core.util.PriceUtil; import haveno.core.util.PriceUtil;
import haveno.core.util.VolumeUtil; import haveno.core.util.VolumeUtil;
@ -49,18 +51,21 @@ class ClosedTradesDataModel extends ActivatableDataModel {
final AccountAgeWitnessService accountAgeWitnessService; final AccountAgeWitnessService accountAgeWitnessService;
private final ObservableList<ClosedTradesListItem> list = FXCollections.observableArrayList(); private final ObservableList<ClosedTradesListItem> list = FXCollections.observableArrayList();
private final ListChangeListener<Tradable> tradesListChangeListener; private final ListChangeListener<Tradable> tradesListChangeListener;
private final TradeManager tradeManager;
@Inject @Inject
public ClosedTradesDataModel(ClosedTradableManager closedTradableManager, public ClosedTradesDataModel(ClosedTradableManager closedTradableManager,
ClosedTradableFormatter closedTradableFormatter, ClosedTradableFormatter closedTradableFormatter,
Preferences preferences, Preferences preferences,
PriceFeedService priceFeedService, PriceFeedService priceFeedService,
AccountAgeWitnessService accountAgeWitnessService) { AccountAgeWitnessService accountAgeWitnessService,
TradeManager tradeManager) {
this.closedTradableManager = closedTradableManager; this.closedTradableManager = closedTradableManager;
this.closedTradableFormatter = closedTradableFormatter; this.closedTradableFormatter = closedTradableFormatter;
this.preferences = preferences; this.preferences = preferences;
this.priceFeedService = priceFeedService; this.priceFeedService = priceFeedService;
this.accountAgeWitnessService = accountAgeWitnessService; this.accountAgeWitnessService = accountAgeWitnessService;
this.tradeManager = tradeManager;
tradesListChangeListener = change -> applyList(); tradesListChangeListener = change -> applyList();
} }
@ -124,4 +129,8 @@ class ClosedTradesDataModel extends ActivatableDataModel {
// We sort by date, the earliest first // We sort by date, the earliest first
list.sort((o1, o2) -> o2.getTradable().getDate().compareTo(o1.getTradable().getDate())); list.sort((o1, o2) -> o2.getTradable().getDate().compareTo(o1.getTradable().getDate()));
} }
public void onMoveTradeToPendingTrades(Trade trade) {
tradeManager.onMoveClosedTradeToPendingTrades(trade);
}
} }

View file

@ -50,6 +50,7 @@
<TableColumn fx:id="stateColumn" minWidth="80"/> <TableColumn fx:id="stateColumn" minWidth="80"/>
<TableColumn fx:id="duplicateColumn" minWidth="30" maxWidth="30" sortable="false"/> <TableColumn fx:id="duplicateColumn" minWidth="30" maxWidth="30" sortable="false"/>
<TableColumn fx:id="avatarColumn" minWidth="40" maxWidth="40"/> <TableColumn fx:id="avatarColumn" minWidth="40" maxWidth="40"/>
<TableColumn fx:id="removeTradeColumn" minWidth="40" maxWidth="40"/>
</columns> </columns>
</TableView> </TableView>

View file

@ -20,6 +20,8 @@ package haveno.desktop.main.portfolio.closedtrades;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.name.Named; import com.google.inject.name.Named;
import com.googlecode.jcsv.writer.CSVEntryConverter; 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 de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon;
import haveno.common.config.Config; import haveno.common.config.Config;
import haveno.common.crypto.KeyRing; 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.OfferDetailsWindow;
import haveno.desktop.main.overlays.windows.TradeDetailsWindow; import haveno.desktop.main.overlays.windows.TradeDetailsWindow;
import haveno.desktop.main.portfolio.presentation.PortfolioUtil; 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.desktop.util.GUIUtil;
import haveno.network.p2p.NodeAddress; import haveno.network.p2p.NodeAddress;
import java.util.Comparator; import java.util.Comparator;
@ -112,7 +114,7 @@ public class ClosedTradesView extends ActivatableViewAndModel<VBox, ClosedTrades
@FXML @FXML
TableColumn<ClosedTradesListItem, ClosedTradesListItem> priceColumn, deviationColumn, amountColumn, volumeColumn, TableColumn<ClosedTradesListItem, ClosedTradesListItem> priceColumn, deviationColumn, amountColumn, volumeColumn,
tradeFeeColumn, buyerSecurityDepositColumn, sellerSecurityDepositColumn, tradeFeeColumn, buyerSecurityDepositColumn, sellerSecurityDepositColumn,
marketColumn, directionColumn, dateColumn, tradeIdColumn, stateColumn, marketColumn, directionColumn, dateColumn, tradeIdColumn, stateColumn, removeTradeColumn,
duplicateColumn, avatarColumn; duplicateColumn, avatarColumn;
@FXML @FXML
FilterBox filterBox; FilterBox filterBox;
@ -186,6 +188,7 @@ public class ClosedTradesView extends ActivatableViewAndModel<VBox, ClosedTrades
setDateColumnCellFactory(); setDateColumnCellFactory();
setMarketColumnCellFactory(); setMarketColumnCellFactory();
setStateColumnCellFactory(); setStateColumnCellFactory();
setRemoveTradeColumnCellFactory();
setDuplicateColumnCellFactory(); setDuplicateColumnCellFactory();
setAvatarColumnCellFactory(); setAvatarColumnCellFactory();
@ -440,7 +443,7 @@ public class ClosedTradesView extends ActivatableViewAndModel<VBox, ClosedTrades
if (item != null && !empty && isMyOfferAsMaker(item.getTradable().getOffer().getOfferPayload())) { if (item != null && !empty && isMyOfferAsMaker(item.getTradable().getOffer().getOfferPayload())) {
if (button == null) { if (button == null) {
button = getRegularIconButton(MaterialDesignIcon.CONTENT_COPY); button = FormBuilder.getRegularIconButton(MaterialDesignIcon.CONTENT_COPY);
button.setTooltip(new Tooltip(Res.get("shared.duplicateOffer"))); button.setTooltip(new Tooltip(Res.get("shared.duplicateOffer")));
setGraphic(button); setGraphic(button);
} }
@ -672,6 +675,54 @@ public class ClosedTradesView extends ActivatableViewAndModel<VBox, ClosedTrades
}); });
} }
private TableColumn<ClosedTradesListItem, ClosedTradesListItem> setRemoveTradeColumnCellFactory() {
removeTradeColumn.setCellValueFactory((trade) -> new ReadOnlyObjectWrapper<>(trade.getValue()));
removeTradeColumn.setCellFactory(
new Callback<>() {
@Override
public TableCell<ClosedTradesListItem, ClosedTradesListItem> call(TableColumn<ClosedTradesListItem,
ClosedTradesListItem> 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) { private void onDuplicateOffer(Offer offer) {
try { try {
OfferPayload offerPayload = offer.getOfferPayload(); OfferPayload offerPayload = offer.getOfferPayload();

View file

@ -75,8 +75,7 @@ class FailedTradesDataModel extends ActivatableDataModel {
} }
public void onMoveTradeToPendingTrades(Trade trade) { public void onMoveTradeToPendingTrades(Trade trade) {
failedTradesManager.removeTrade(trade); tradeManager.onMoveFailedTradeToPendingTrades(trade);
tradeManager.addFailedTradeToPendingTrades(trade);
} }
public void unfailTrade(Trade trade) { public void unfailTrade(Trade trade) {