/* * This file is part of Bitsquare. * * Bitsquare 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. * * Bitsquare 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 Bitsquare. If not, see <http://www.gnu.org/licenses/>. */ package io.bitsquare.gui.main.portfolio.pendingtrades; import com.google.inject.Inject; import io.bitsquare.app.Log; import io.bitsquare.arbitration.Arbitrator; import io.bitsquare.arbitration.Dispute; import io.bitsquare.arbitration.DisputeAlreadyOpenException; import io.bitsquare.arbitration.DisputeManager; import io.bitsquare.btc.FeePolicy; import io.bitsquare.btc.TradeWalletService; import io.bitsquare.btc.WalletService; import io.bitsquare.common.crypto.KeyRing; import io.bitsquare.common.handlers.ErrorMessageHandler; import io.bitsquare.common.handlers.FaultHandler; import io.bitsquare.common.handlers.ResultHandler; import io.bitsquare.gui.Navigation; import io.bitsquare.gui.common.model.ActivatableDataModel; import io.bitsquare.gui.main.MainView; import io.bitsquare.gui.main.disputes.DisputesView; import io.bitsquare.gui.main.overlays.notifications.NotificationCenter; import io.bitsquare.gui.main.overlays.popups.Popup; import io.bitsquare.gui.main.overlays.windows.SelectDepositTxWindow; import io.bitsquare.gui.main.overlays.windows.WalletPasswordWindow; import io.bitsquare.p2p.P2PService; import io.bitsquare.payment.PaymentAccountContractData; import io.bitsquare.trade.BuyerTrade; import io.bitsquare.trade.SellerTrade; import io.bitsquare.trade.Trade; import io.bitsquare.trade.TradeManager; import io.bitsquare.trade.offer.Offer; import io.bitsquare.user.Preferences; import io.bitsquare.user.User; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import org.bitcoinj.core.BlockChainListener; import org.bitcoinj.core.Coin; import org.bitcoinj.core.Transaction; import org.spongycastle.crypto.params.KeyParameter; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; public class PendingTradesDataModel extends ActivatableDataModel { public final TradeManager tradeManager; public final WalletService walletService; private final TradeWalletService tradeWalletService; private final User user; private final KeyRing keyRing; public final DisputeManager disputeManager; private P2PService p2PService; public final Navigation navigation; public final WalletPasswordWindow walletPasswordWindow; private final NotificationCenter notificationCenter; final ObservableList<PendingTradesListItem> list = FXCollections.observableArrayList(); private final ListChangeListener<Trade> tradesListChangeListener; private boolean isOfferer; final ObjectProperty<PendingTradesListItem> selectedItemProperty = new SimpleObjectProperty<>(); public final StringProperty txId = new SimpleStringProperty(); public final Preferences preferences; private boolean activated; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, initialization /////////////////////////////////////////////////////////////////////////////////////////// @Inject public PendingTradesDataModel(TradeManager tradeManager, WalletService walletService, TradeWalletService tradeWalletService, User user, KeyRing keyRing, DisputeManager disputeManager, Preferences preferences, P2PService p2PService, Navigation navigation, WalletPasswordWindow walletPasswordWindow, NotificationCenter notificationCenter) { this.tradeManager = tradeManager; this.walletService = walletService; this.tradeWalletService = tradeWalletService; this.user = user; this.keyRing = keyRing; this.disputeManager = disputeManager; this.preferences = preferences; this.p2PService = p2PService; this.navigation = navigation; this.walletPasswordWindow = walletPasswordWindow; this.notificationCenter = notificationCenter; tradesListChangeListener = change -> onListChanged(); notificationCenter.setSelectItemByTradeIdConsumer(this::selectItemByTradeId); } @Override protected void activate() { tradeManager.getTrades().addListener(tradesListChangeListener); onListChanged(); if (selectedItemProperty.get() != null) notificationCenter.setSelectedTradeId(selectedItemProperty.get().getTrade().getId()); activated = true; } @Override protected void deactivate() { tradeManager.getTrades().removeListener(tradesListChangeListener); notificationCenter.setSelectedTradeId(null); activated = false; } /////////////////////////////////////////////////////////////////////////////////////////// // UI actions /////////////////////////////////////////////////////////////////////////////////////////// void onSelectItem(PendingTradesListItem item) { doSelectItem(item); } public void onPaymentStarted(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { checkNotNull(getTrade(), "trade must not be null"); checkArgument(getTrade() instanceof BuyerTrade, "Check failed: trade instanceof BuyerTrade"); checkArgument(getTrade().getDisputeState() == Trade.DisputeState.NONE, "Check failed: trade.getDisputeState() == Trade.DisputeState.NONE"); ((BuyerTrade) getTrade()).onFiatPaymentStarted(resultHandler, errorMessageHandler); } public void onFiatPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { checkNotNull(getTrade(), "trade must not be null"); checkArgument(getTrade() instanceof SellerTrade, "Check failed: trade not instanceof SellerTrade"); if (getTrade().getDisputeState() == Trade.DisputeState.NONE) ((SellerTrade) getTrade()).onFiatPaymentReceived(resultHandler, errorMessageHandler); } public void onWithdrawRequest(String toAddress, Coin receiverAmount, KeyParameter aesKey, ResultHandler resultHandler, FaultHandler faultHandler) { checkNotNull(getTrade(), "trade must not be null"); if (toAddress != null && toAddress.length() > 0) { tradeManager.onWithdrawRequest( toAddress, receiverAmount, aesKey, getTrade(), () -> { resultHandler.handleResult(); selectBestItem(); }, (errorMessage, throwable) -> { log.error(errorMessage); faultHandler.handleFault(errorMessage, throwable); }); } else { faultHandler.handleFault("No receiver address defined", null); } } public void onOpenDispute() { tryOpenDispute(false); } public void onOpenSupportTicket() { tryOpenDispute(true); } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// @Nullable public PendingTradesListItem getSelectedItem() { return selectedItemProperty.get() != null ? selectedItemProperty.get() : null; } @Nullable public Trade getTrade() { return selectedItemProperty.get() != null ? selectedItemProperty.get().getTrade() : null; } @Nullable Offer getOffer() { return getTrade() != null ? getTrade().getOffer() : null; } boolean isBuyOffer() { return getOffer() != null && getOffer().getDirection() == Offer.Direction.BUY; } boolean isOfferer(Offer offer) { return tradeManager.isMyOffer(offer); } boolean isOfferer() { return isOfferer; } Coin getTotalFees() { return FeePolicy.getFixedTxFeeForTrades(getOffer()).add(isOfferer() ? FeePolicy.getCreateOfferFee() : FeePolicy.getTakeOfferFee()); } public String getCurrencyCode() { return getOffer() != null ? getOffer().getCurrencyCode() : ""; } public Offer.Direction getDirection(Offer offer) { isOfferer = tradeManager.isMyOffer(offer); return isOfferer ? offer.getDirection() : offer.getMirroredDirection(); } void addBlockChainListener(BlockChainListener blockChainListener) { tradeWalletService.addBlockChainListener(blockChainListener); } void removeBlockChainListener(BlockChainListener blockChainListener) { tradeWalletService.removeBlockChainListener(blockChainListener); } public long getLockTime() { return getTrade() != null ? getTrade().getLockTimeAsBlockHeight() : 0; } public int getBestChainHeight() { return tradeWalletService.getBestChainHeight(); } @Nullable public PaymentAccountContractData getSellersPaymentAccountContractData() { if (getTrade() != null && getTrade().getContract() != null) return getTrade().getContract().getSellerPaymentAccountContractData(); else return null; } public String getReference() { return getOffer() != null ? getOffer().getShortId() : ""; } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void onListChanged() { Log.traceCall(); list.clear(); list.addAll(tradeManager.getTrades().stream().map(PendingTradesListItem::new).collect(Collectors.toList())); // we sort by date, earliest first list.sort((o1, o2) -> o2.getTrade().getDate().compareTo(o1.getTrade().getDate())); selectBestItem(); } private void selectBestItem() { if (list.size() == 1) doSelectItem(list.get(0)); else if (list.size() > 1 && (selectedItemProperty.get() == null || !list.contains(selectedItemProperty.get()))) doSelectItem(list.get(0)); else if (list.size() == 0) doSelectItem(null); } private void selectItemByTradeId(String tradeId) { if (activated) list.stream().filter(e -> e.getTrade().getId().equals(tradeId)).findAny().ifPresent(this::doSelectItem); } private void doSelectItem(PendingTradesListItem item) { if (item != null) { Trade trade = item.getTrade(); isOfferer = tradeManager.isMyOffer(trade.getOffer()); if (trade.getDepositTx() != null) txId.set(trade.getDepositTx().getHashAsString()); else txId.set(""); notificationCenter.setSelectedTradeId(trade.getId()); } else { notificationCenter.setSelectedTradeId(null); } selectedItemProperty.set(item); } private void tryOpenDispute(boolean isSupportTicket) { if (getTrade() != null) { Transaction depositTx = getTrade().getDepositTx(); if (depositTx != null) { doOpenDispute(isSupportTicket, getTrade().getDepositTx()); } else { log.info("Trade.depositTx is null. We try to find the tx in our wallet."); List<Transaction> candidates = new ArrayList<>(); List<Transaction> transactions = walletService.getWallet().getRecentTransactions(100, true); transactions.stream().forEach(transaction -> { Coin valueSentFromMe = transaction.getValueSentFromMe(walletService.getWallet()); if (!valueSentFromMe.isZero()) { // spending tx // MS tx candidates.addAll(transaction.getOutputs().stream() .filter(transactionOutput -> !transactionOutput.isMine(walletService.getWallet())) .filter(transactionOutput -> transactionOutput.getScriptPubKey().isPayToScriptHash()) .map(transactionOutput -> transaction) .collect(Collectors.toList())); } }); if (candidates.size() == 1) doOpenDispute(isSupportTicket, candidates.get(0)); else if (candidates.size() > 1) new SelectDepositTxWindow().transactions(candidates) .onSelect(transaction -> doOpenDispute(isSupportTicket, transaction)) .closeButtonText("Cancel") .show(); else log.error("Trade.depositTx is null and we did not find any MultiSig transaction."); } } else { log.error("Trade is null"); } } private void doOpenDispute(boolean isSupportTicket, Transaction depositTx) { Log.traceCall("depositTx=" + depositTx); byte[] depositTxSerialized = null; byte[] payoutTxSerialized = null; String depositTxHashAsString = null; String payoutTxHashAsString = null; if (depositTx != null) { depositTxSerialized = depositTx.bitcoinSerialize(); depositTxHashAsString = depositTx.getHashAsString(); } else { log.warn("depositTx is null"); } Trade trade = getTrade(); if (trade != null) { Transaction payoutTx = trade.getPayoutTx(); if (payoutTx != null) { payoutTxSerialized = payoutTx.bitcoinSerialize(); payoutTxHashAsString = payoutTx.getHashAsString(); } else { log.debug("payoutTx is null at doOpenDispute"); } final Arbitrator acceptedArbitratorByAddress = user.getAcceptedArbitratorByAddress(trade.getArbitratorNodeAddress()); checkNotNull(acceptedArbitratorByAddress, "acceptedArbitratorByAddress must no tbe null"); Dispute dispute = new Dispute(disputeManager.getDisputeStorage(), trade.getId(), keyRing.getPubKeyRing().hashCode(), // traderId trade.getOffer().getDirection() == Offer.Direction.BUY ? isOfferer : !isOfferer, isOfferer, keyRing.getPubKeyRing(), trade.getDate(), trade.getContract(), trade.getContractHash(), depositTxSerialized, payoutTxSerialized, depositTxHashAsString, payoutTxHashAsString, trade.getContractAsJson(), trade.getOffererContractSignature(), trade.getTakerContractSignature(), acceptedArbitratorByAddress.getPubKeyRing(), isSupportTicket ); trade.setDisputeState(Trade.DisputeState.DISPUTE_REQUESTED); if (p2PService.isBootstrapped()) { sendOpenNewDisputeMessage(dispute, false); } else { new Popup().information("You need to wait until you are fully connected to the network.\n" + "That might take up to about 2 minutes at startup.").show(); } } else { log.warn("trade is null at doOpenDispute"); } } private void sendOpenNewDisputeMessage(Dispute dispute, boolean reOpen) { disputeManager.sendOpenNewDisputeMessage(dispute, reOpen, () -> navigation.navigateTo(MainView.class, DisputesView.class), (errorMessage, throwable) -> { if ((throwable instanceof DisputeAlreadyOpenException)) { errorMessage += "\n\n" + "If you are not sure that the message to the arbitrator arrived (e.g. if you did not got " + "a response after 1 day) feel free to open a dispute again."; new Popup().warning(errorMessage) .actionButtonText("Open dispute again") .onAction(() -> sendOpenNewDisputeMessage(dispute, true)) .closeButtonText("Cancel") .show(); } else { new Popup().warning(errorMessage).show(); } }); } }