mirror of
https://github.com/haveno-dex/haveno.git
synced 2025-01-03 09:29:44 +00:00
recover from failed payout tx
This commit is contained in:
parent
7847460f11
commit
7e3d89797e
12 changed files with 124 additions and 59 deletions
|
@ -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));
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in a new issue