/* * 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.btc.FeePolicy; import io.bitsquare.common.Clock; import io.bitsquare.gui.common.model.ActivatableWithDataModel; import io.bitsquare.gui.common.model.ViewModel; import io.bitsquare.gui.util.BSFormatter; import io.bitsquare.gui.util.validation.BtcAddressValidator; import io.bitsquare.locale.BSResources; import io.bitsquare.p2p.P2PService; import io.bitsquare.payment.PaymentMethod; import io.bitsquare.trade.Contract; import io.bitsquare.trade.Trade; import io.bitsquare.trade.closed.ClosedTradableManager; import io.bitsquare.trade.offer.Offer; import io.bitsquare.user.User; import javafx.beans.property.*; import javafx.beans.value.ChangeListener; import org.bitcoinj.core.BlockChainListener; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; import java.util.Date; import java.util.stream.Collectors; import static io.bitsquare.gui.main.portfolio.pendingtrades.PendingTradesViewModel.SellerState.*; public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTradesDataModel> implements ViewModel { private Subscription tradeStateSubscription; interface State { } enum BuyerState implements State { UNDEFINED, WAIT_FOR_BLOCKCHAIN_CONFIRMATION, REQUEST_START_FIAT_PAYMENT, WAIT_FOR_FIAT_PAYMENT_RECEIPT, WAIT_FOR_BROADCAST_AFTER_UNLOCK, REQUEST_WITHDRAWAL } enum SellerState implements State { UNDEFINED, WAIT_FOR_BLOCKCHAIN_CONFIRMATION, WAIT_FOR_FIAT_PAYMENT_STARTED, REQUEST_CONFIRM_FIAT_PAYMENT_RECEIVED, WAIT_FOR_PAYOUT_TX, WAIT_FOR_BROADCAST_AFTER_UNLOCK, REQUEST_WITHDRAWAL } public final BSFormatter formatter; public final BtcAddressValidator btcAddressValidator; public final P2PService p2PService; public final User user; private ClosedTradableManager closedTradableManager; public final Clock clock; private final ObjectProperty<BuyerState> buyerState = new SimpleObjectProperty<>(); private final ObjectProperty<SellerState> sellerState = new SimpleObjectProperty<>(); private final BooleanProperty withdrawalButtonDisable = new SimpleBooleanProperty(true); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, initialization /////////////////////////////////////////////////////////////////////////////////////////// @Inject public PendingTradesViewModel(PendingTradesDataModel dataModel, BSFormatter formatter, BtcAddressValidator btcAddressValidator, P2PService p2PService, User user, ClosedTradableManager closedTradableManager, Clock clock) { super(dataModel); this.formatter = formatter; this.btcAddressValidator = btcAddressValidator; this.p2PService = p2PService; this.user = user; this.closedTradableManager = closedTradableManager; this.clock = clock; } private ChangeListener<Trade.State> tradeStateChangeListener; @Override protected void activate() { } // Dont set own listener as we need to control the order of the calls public void onSelectedItemChanged(PendingTradesListItem selectedItem) { if (tradeStateSubscription != null) tradeStateSubscription.unsubscribe(); if (selectedItem != null) tradeStateSubscription = EasyBind.subscribe(selectedItem.getTrade().stateProperty(), this::onTradeStateChanged); } @Override protected void deactivate() { if (tradeStateSubscription != null) { tradeStateSubscription.unsubscribe(); tradeStateSubscription = null; } } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// ReadOnlyObjectProperty<BuyerState> getBuyerState() { return buyerState; } ReadOnlyObjectProperty<SellerState> getSellerState() { return sellerState; } public void withdrawAddressFocusOut(String text) { withdrawalButtonDisable.set(!btcAddressValidator.validate(text).isValid); } public String getPayoutAmount() { return dataModel.getTrade() != null ? formatter.formatCoinWithCode(dataModel.getTrade().getPayoutAmount()) : ""; } String getMarketLabel(PendingTradesListItem item) { if ((item == null)) return ""; return formatter.getCurrencyPair(item.getTrade().getOffer().getCurrencyCode()); } // trade period private long getMaxTradePeriod() { return dataModel.getOffer() != null ? dataModel.getOffer().getPaymentMethod().getMaxTradePeriod() : 0; } private long getTimeWhenDisputeOpens() { return dataModel.getTrade() != null ? dataModel.getTrade().getDate().getTime() + getMaxTradePeriod() : 0; } private long getTimeWhenHalfPeriodReached() { return dataModel.getTrade() != null ? dataModel.getTrade().getDate().getTime() + getMaxTradePeriod() / 2 : 0; } private Date getDateWhenDisputeOpens() { return new Date(getTimeWhenDisputeOpens()); } private Date getDateWhenHalfPeriodReached() { return new Date(getTimeWhenHalfPeriodReached()); } private long getRemainingTradeDuration() { return getDateWhenDisputeOpens().getTime() - new Date().getTime(); } public String getRemainingTradeDurationAsWords() { return formatter.formatDurationAsWords(Math.max(0, getRemainingTradeDuration())); } public double getRemainingTradeDurationAsPercentage() { long maxPeriod = getMaxTradePeriod(); long remaining = getRemainingTradeDuration(); if (maxPeriod != 0) { double v = 1 - (double) remaining / (double) maxPeriod; return v; } else return 0; } public String getDateForOpenDispute() { return formatter.formatDateTime(new Date(new Date().getTime() + getRemainingTradeDuration())); } public boolean showWarning() { return new Date().after(getDateWhenHalfPeriodReached()); } public boolean showDispute() { return new Date().after(getDateWhenDisputeOpens()); } // String getMyRole(PendingTradesListItem item) { Trade trade = item.getTrade(); Contract contract = trade.getContract(); if (contract != null) { Offer offer = trade.getOffer(); return formatter.getRole(contract.isBuyerOffererAndSellerTaker(), dataModel.isOfferer(offer), offer.getCurrencyCode()); } else { return ""; } } String getPaymentMethod(PendingTradesListItem item) { String result = ""; if (item != null) { Offer offer = item.getTrade().getOffer(); String method = BSResources.get(offer.getPaymentMethod().getId() + "_SHORT"); String methodCountryCode = offer.getCountryCode(); if (methodCountryCode != null) result = method + " (" + methodCountryCode + ")"; else result = method; } return result; } public void addBlockChainListener(BlockChainListener blockChainListener) { dataModel.addBlockChainListener(blockChainListener); } public void removeBlockChainListener(BlockChainListener blockChainListener) { dataModel.removeBlockChainListener(blockChainListener); } public long getLockTime() { return dataModel.getLockTime(); } public String getPaymentMethod() { if (dataModel.getTrade() != null && dataModel.getTrade().getContract() != null) return BSResources.get(dataModel.getTrade().getContract().getPaymentMethodName()); else return ""; } // summary public String getTradeVolume() { return dataModel.getTrade() != null ? formatter.formatCoinWithCode(dataModel.getTrade().getTradeAmount()) : ""; } public String getFiatVolume() { return dataModel.getTrade() != null ? formatter.formatVolumeWithCode(dataModel.getTrade().getTradeVolume()) : ""; } public String getTotalFees() { return formatter.formatCoinWithCode(dataModel.getTotalFees()); } public String getSecurityDeposit() { return formatter.formatCoinWithCode(FeePolicy.getSecurityDeposit(dataModel.getOffer())); } public boolean isBlockChainMethod() { return dataModel.getOffer() != null && dataModel.getOffer().getPaymentMethod().equals(PaymentMethod.BLOCK_CHAINS); } public int getNumPastTrades(Trade trade) { return closedTradableManager.getClosedTrades().stream() .filter(e -> { if (e instanceof Trade) { Trade t = (Trade) e; return t.getTradingPeerNodeAddress() != null && trade.getTradingPeerNodeAddress() != null && t.getTradingPeerNodeAddress().hostName.equals(trade.getTradingPeerNodeAddress().hostName); } else return false; }) .collect(Collectors.toSet()) .size(); } /////////////////////////////////////////////////////////////////////////////////////////// // States /////////////////////////////////////////////////////////////////////////////////////////// private void onTradeStateChanged(Trade.State tradeState) { Log.traceCall(tradeState.toString()); // TODO what is first valid state for trade? switch (tradeState) { case PREPARATION: sellerState.set(UNDEFINED); buyerState.set(PendingTradesViewModel.BuyerState.UNDEFINED); break; case TAKER_FEE_PAID: case OFFERER_SENT_PUBLISH_DEPOSIT_TX_REQUEST: case TAKER_PUBLISHED_DEPOSIT_TX: case DEPOSIT_SEEN_IN_NETWORK: case TAKER_SENT_DEPOSIT_TX_PUBLISHED_MSG: case OFFERER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG: sellerState.set(WAIT_FOR_BLOCKCHAIN_CONFIRMATION); buyerState.set(PendingTradesViewModel.BuyerState.WAIT_FOR_BLOCKCHAIN_CONFIRMATION); break; case DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN: sellerState.set(WAIT_FOR_FIAT_PAYMENT_STARTED); buyerState.set(PendingTradesViewModel.BuyerState.REQUEST_START_FIAT_PAYMENT); case BUYER_CONFIRMED_FIAT_PAYMENT_INITIATED: // we stick with the state until we get the msg sent success buyerState.set(PendingTradesViewModel.BuyerState.REQUEST_START_FIAT_PAYMENT); break; case BUYER_SENT_FIAT_PAYMENT_INITIATED_MSG: buyerState.set(PendingTradesViewModel.BuyerState.WAIT_FOR_FIAT_PAYMENT_RECEIPT); break; case SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG: // seller case SELLER_CONFIRMED_FIAT_PAYMENT_RECEIPT: // we stick with the state until we get the msg sent success sellerState.set(REQUEST_CONFIRM_FIAT_PAYMENT_RECEIVED); break; case SELLER_SENT_FIAT_PAYMENT_RECEIPT_MSG: sellerState.set(WAIT_FOR_PAYOUT_TX); break; case BUYER_RECEIVED_FIAT_PAYMENT_RECEIPT_MSG: case BUYER_COMMITTED_PAYOUT_TX: case BUYER_STARTED_SEND_PAYOUT_TX: // TODO would need extra state for wait until msg arrived and PAYOUT_BROAD_CASTED gets called. buyerState.set(PendingTradesViewModel.BuyerState.WAIT_FOR_BROADCAST_AFTER_UNLOCK); break; case SELLER_RECEIVED_AND_COMMITTED_PAYOUT_TX: sellerState.set(SellerState.WAIT_FOR_BROADCAST_AFTER_UNLOCK); break; case PAYOUT_BROAD_CASTED: sellerState.set(REQUEST_WITHDRAWAL); buyerState.set(PendingTradesViewModel.BuyerState.REQUEST_WITHDRAWAL); break; case WITHDRAW_COMPLETED: sellerState.set(UNDEFINED); buyerState.set(PendingTradesViewModel.BuyerState.UNDEFINED); break; default: sellerState.set(UNDEFINED); buyerState.set(PendingTradesViewModel.BuyerState.UNDEFINED); log.warn("unhandled processState " + tradeState); break; } } }