/* * 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.trade; import com.google.common.base.Throwables; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import io.bitsquare.app.Log; import io.bitsquare.app.Version; import io.bitsquare.arbitration.Arbitrator; import io.bitsquare.arbitration.ArbitratorManager; import io.bitsquare.btc.TradeWalletService; import io.bitsquare.btc.WalletService; import io.bitsquare.common.crypto.KeyRing; import io.bitsquare.common.taskrunner.Model; import io.bitsquare.crypto.DecryptedMsgWithPubKey; import io.bitsquare.filter.FilterManager; import io.bitsquare.p2p.NodeAddress; import io.bitsquare.p2p.P2PService; import io.bitsquare.storage.Storage; import io.bitsquare.trade.offer.Offer; import io.bitsquare.trade.offer.OpenOfferManager; import io.bitsquare.trade.protocol.trade.ProcessModel; import io.bitsquare.trade.protocol.trade.TradeProtocol; import io.bitsquare.user.User; import javafx.beans.property.*; import org.bitcoinj.core.Coin; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionConfidence; import org.bitcoinj.utils.ExchangeRate; import org.bitcoinj.utils.Fiat; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.io.IOException; import java.io.ObjectInputStream; import java.util.Date; import java.util.HashSet; import java.util.Set; import static com.google.common.base.Preconditions.checkNotNull; /** * Holds all data which are relevant to the trade, but not those which are only needed in the trade process as shared data between tasks. Those data are * stored in the task model. */ public abstract class Trade implements Tradable, Model { // That object is saved to disc. We need to take care of changes to not break deserialization. private static final long serialVersionUID = Version.LOCAL_DB_VERSION; private static final Logger log = LoggerFactory.getLogger(Trade.class); public enum State { PREPARATION(Phase.PREPARATION), TAKER_FEE_PAID(Phase.TAKER_FEE_PAID), OFFERER_SENT_PUBLISH_DEPOSIT_TX_REQUEST(Phase.DEPOSIT_REQUESTED), TAKER_PUBLISHED_DEPOSIT_TX(Phase.DEPOSIT_PAID), DEPOSIT_SEEN_IN_NETWORK(Phase.DEPOSIT_PAID), // triggered by balance update, used only in error cases TAKER_SENT_DEPOSIT_TX_PUBLISHED_MSG(Phase.DEPOSIT_PAID), OFFERER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG(Phase.DEPOSIT_PAID), DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN(Phase.DEPOSIT_PAID), BUYER_CONFIRMED_FIAT_PAYMENT_INITIATED(Phase.FIAT_SENT), BUYER_SENT_FIAT_PAYMENT_INITIATED_MSG(Phase.FIAT_SENT), SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG(Phase.FIAT_SENT), SELLER_CONFIRMED_FIAT_PAYMENT_RECEIPT(Phase.FIAT_RECEIVED), SELLER_SENT_FIAT_PAYMENT_RECEIPT_MSG(Phase.FIAT_RECEIVED), BUYER_RECEIVED_FIAT_PAYMENT_RECEIPT_MSG(Phase.FIAT_RECEIVED), BUYER_COMMITTED_PAYOUT_TX(Phase.PAYOUT_PAID), //TODO needed? BUYER_STARTED_SEND_PAYOUT_TX(Phase.PAYOUT_PAID), // not from the success/arrived handler! SELLER_RECEIVED_AND_COMMITTED_PAYOUT_TX(Phase.PAYOUT_PAID), PAYOUT_BROAD_CASTED(Phase.PAYOUT_PAID), WITHDRAW_COMPLETED(Phase.WITHDRAWN); public Phase getPhase() { return phase; } private final Phase phase; State(Phase phase) { this.phase = phase; } } public enum Phase { PREPARATION, TAKER_FEE_PAID, DEPOSIT_REQUESTED, DEPOSIT_PAID, FIAT_SENT, FIAT_RECEIVED, PAYOUT_PAID, WITHDRAWN, DISPUTE } public enum DisputeState { NONE, DISPUTE_REQUESTED, DISPUTE_STARTED_BY_PEER, DISPUTE_CLOSED } public enum TradePeriodState { NORMAL, HALF_REACHED, TRADE_PERIOD_OVER } /////////////////////////////////////////////////////////////////////////////////////////// // Fields /////////////////////////////////////////////////////////////////////////////////////////// // Transient/Immutable transient private ObjectProperty<State> stateProperty; transient private ObjectProperty<DisputeState> disputeStateProperty; transient private ObjectProperty<TradePeriodState> tradePeriodStateProperty; // Trades are saved in the TradeList @Nullable transient private Storage<? extends TradableList> storage; transient protected TradeProtocol tradeProtocol; transient private Date maxTradePeriodDate, halfTradePeriodDate; // Immutable private final Offer offer; private final ProcessModel processModel; // Mutable private DecryptedMsgWithPubKey decryptedMsgWithPubKey; private Date takeOfferDate; private Coin tradeAmount; private long tradePrice; private NodeAddress tradingPeerNodeAddress; @Nullable private String takeOfferFeeTxId; protected State state; private DisputeState disputeState = DisputeState.NONE; private TradePeriodState tradePeriodState = TradePeriodState.NORMAL; private Transaction depositTx; private Contract contract; private String contractAsJson; private byte[] contractHash; private String takerContractSignature; private String offererContractSignature; private Transaction payoutTx; private long lockTimeAsBlockHeight; private NodeAddress arbitratorNodeAddress; private byte[] arbitratorBtcPubKey; private String takerPaymentAccountId; private String errorMessage; transient private StringProperty errorMessageProperty; transient private ObjectProperty<Coin> tradeAmountProperty; transient private ObjectProperty<Fiat> tradeVolumeProperty; transient private Set<DecryptedMsgWithPubKey> mailboxMessageSet = new HashSet<>(); /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, initialization /////////////////////////////////////////////////////////////////////////////////////////// // offerer protected Trade(Offer offer, Storage<? extends TradableList> storage) { this.offer = offer; this.storage = storage; this.takeOfferDate = new Date(); processModel = new ProcessModel(); tradeVolumeProperty = new SimpleObjectProperty<>(); tradeAmountProperty = new SimpleObjectProperty<>(); errorMessageProperty = new SimpleStringProperty(); initStates(); initStateProperties(); } // taker protected Trade(Offer offer, Coin tradeAmount, long tradePrice, NodeAddress tradingPeerNodeAddress, Storage<? extends TradableList> storage) { this(offer, storage); this.tradeAmount = tradeAmount; this.tradePrice = tradePrice; this.tradingPeerNodeAddress = tradingPeerNodeAddress; tradeAmountProperty.set(tradeAmount); tradeVolumeProperty.set(getTradeVolume()); this.takeOfferDate = new Date(); } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { try { in.defaultReadObject(); initStateProperties(); initAmountProperty(); errorMessageProperty = new SimpleStringProperty(errorMessage); mailboxMessageSet = new HashSet<>(); } catch (Throwable t) { log.warn("Cannot be deserialized." + t.getMessage()); } } public void init(P2PService p2PService, WalletService walletService, TradeWalletService tradeWalletService, ArbitratorManager arbitratorManager, TradeManager tradeManager, OpenOfferManager openOfferManager, User user, FilterManager filterManager, KeyRing keyRing, boolean useSavingsWallet, Coin fundsNeededForTrade) { Log.traceCall(); processModel.onAllServicesInitialized(offer, tradeManager, openOfferManager, p2PService, walletService, tradeWalletService, arbitratorManager, user, filterManager, keyRing, useSavingsWallet, fundsNeededForTrade); createProtocol(); log.trace("init: decryptedMsgWithPubKey = " + decryptedMsgWithPubKey); if (decryptedMsgWithPubKey != null && !mailboxMessageSet.contains(decryptedMsgWithPubKey)) { mailboxMessageSet.add(decryptedMsgWithPubKey); tradeProtocol.applyMailboxMessage(decryptedMsgWithPubKey, this); } } protected void initStateProperties() { stateProperty = new SimpleObjectProperty<>(state); disputeStateProperty = new SimpleObjectProperty<>(disputeState); tradePeriodStateProperty = new SimpleObjectProperty<>(tradePeriodState); } protected void initAmountProperty() { tradeAmountProperty = new SimpleObjectProperty<>(); tradeVolumeProperty = new SimpleObjectProperty<>(); if (tradeAmount != null) { tradeAmountProperty.set(tradeAmount); tradeVolumeProperty.set(getTradeVolume()); } } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// // The deserialized tx has not actual confidence data, so we need to get the fresh one from the wallet. public void updateDepositTxFromWallet() { if (depositTx != null) setDepositTx(processModel.getTradeWalletService().getWalletTx(depositTx.getHash())); } public void setDepositTx(Transaction tx) { log.debug("setDepositTx " + tx); this.depositTx = tx; setupConfidenceListener(); persist(); } @Nullable public Transaction getDepositTx() { return depositTx; } public void setMailboxMessage(DecryptedMsgWithPubKey decryptedMsgWithPubKey) { log.trace("setMailboxMessage decryptedMsgWithPubKey=" + decryptedMsgWithPubKey); this.decryptedMsgWithPubKey = decryptedMsgWithPubKey; if (tradeProtocol != null && decryptedMsgWithPubKey != null && !mailboxMessageSet.contains(decryptedMsgWithPubKey)) { mailboxMessageSet.add(decryptedMsgWithPubKey); tradeProtocol.applyMailboxMessage(decryptedMsgWithPubKey, this); } } public DecryptedMsgWithPubKey getMailboxMessage() { return decryptedMsgWithPubKey; } public void setStorage(Storage<? extends TradableList> storage) { this.storage = storage; } /////////////////////////////////////////////////////////////////////////////////////////// // States /////////////////////////////////////////////////////////////////////////////////////////// public void setState(State state) { log.info("Trade.setState: " + state); boolean changed = this.state != state; this.state = state; stateProperty.set(state); if (changed) persist(); } public void setDisputeState(DisputeState disputeState) { Log.traceCall("disputeState=" + disputeState + "\n\ttrade=" + this); boolean changed = this.disputeState != disputeState; this.disputeState = disputeState; disputeStateProperty.set(disputeState); if (changed) persist(); } public DisputeState getDisputeState() { return disputeState; } public void setTradePeriodState(TradePeriodState tradePeriodState) { boolean changed = this.tradePeriodState != tradePeriodState; this.tradePeriodState = tradePeriodState; tradePeriodStateProperty.set(tradePeriodState); if (changed) persist(); } public TradePeriodState getTradePeriodState() { return tradePeriodState; } public boolean isTakerFeePaid() { return state.getPhase() != null && state.getPhase().ordinal() >= Phase.TAKER_FEE_PAID.ordinal(); } public boolean isDepositPaid() { return state.getPhase() != null && state.getPhase().ordinal() >= Phase.DEPOSIT_PAID.ordinal(); } public State getState() { return state; } /////////////////////////////////////////////////////////////////////////////////////////// // Model implementation /////////////////////////////////////////////////////////////////////////////////////////// // Get called from taskRunner after each completed task @Override public void persist() { if (storage != null) storage.queueUpForSave(); } @Override public void onComplete() { persist(); } /////////////////////////////////////////////////////////////////////////////////////////// // Getter only /////////////////////////////////////////////////////////////////////////////////////////// public String getId() { return offer.getId(); } public String getShortId() { return offer.getShortId(); } public Offer getOffer() { return offer; } abstract public Coin getPayoutAmount(); public ProcessModel getProcessModel() { return processModel; } @Nullable public Fiat getTradeVolume() { if (tradeAmount != null && getTradePrice() != null) return new ExchangeRate(getTradePrice()).coinToFiat(tradeAmount); else return null; } @Nullable public Date getMaxTradePeriodDate() { if (maxTradePeriodDate == null && takeOfferDate != null) maxTradePeriodDate = new Date(takeOfferDate.getTime() + getOffer().getPaymentMethod().getMaxTradePeriod()); return maxTradePeriodDate; } @Nullable public Date getHalfTradePeriodDate() { if (halfTradePeriodDate == null && takeOfferDate != null) halfTradePeriodDate = new Date(takeOfferDate.getTime() + getOffer().getPaymentMethod().getMaxTradePeriod() / 2); return halfTradePeriodDate; } public ReadOnlyObjectProperty<? extends State> stateProperty() { return stateProperty; } public ReadOnlyObjectProperty<Coin> tradeAmountProperty() { return tradeAmountProperty; } public ReadOnlyObjectProperty<Fiat> tradeVolumeProperty() { return tradeVolumeProperty; } public ReadOnlyObjectProperty<DisputeState> disputeStateProperty() { return disputeStateProperty; } public ReadOnlyObjectProperty<TradePeriodState> getTradePeriodStateProperty() { return tradePeriodStateProperty; } /////////////////////////////////////////////////////////////////////////////////////////// // Getter/Setter for Mutable objects /////////////////////////////////////////////////////////////////////////////////////////// public Date getDate() { return takeOfferDate; } public void setTradingPeerNodeAddress(NodeAddress tradingPeerNodeAddress) { if (tradingPeerNodeAddress == null) log.error("tradingPeerAddress=null"); else this.tradingPeerNodeAddress = tradingPeerNodeAddress; } @Nullable public NodeAddress getTradingPeerNodeAddress() { return tradingPeerNodeAddress; } public void setTradeAmount(Coin tradeAmount) { this.tradeAmount = tradeAmount; tradeAmountProperty.set(tradeAmount); tradeVolumeProperty.set(getTradeVolume()); } public void setTradePrice(long tradePrice) { this.tradePrice = tradePrice; } public Fiat getTradePrice() { return Fiat.valueOf(offer.getCurrencyCode(), tradePrice); } @Nullable public Coin getTradeAmount() { return tradeAmount; } public void setLockTimeAsBlockHeight(long lockTimeAsBlockHeight) { this.lockTimeAsBlockHeight = lockTimeAsBlockHeight; } public long getLockTimeAsBlockHeight() { return lockTimeAsBlockHeight; } public void setTakerContractSignature(String takerSignature) { this.takerContractSignature = takerSignature; } @Nullable public String getTakerContractSignature() { return takerContractSignature; } public void setOffererContractSignature(String offererContractSignature) { this.offererContractSignature = offererContractSignature; } @Nullable public String getOffererContractSignature() { return offererContractSignature; } public void setContractAsJson(String contractAsJson) { this.contractAsJson = contractAsJson; } @Nullable public String getContractAsJson() { return contractAsJson; } public void setContract(Contract contract) { this.contract = contract; } @Nullable public Contract getContract() { return contract; } public void setPayoutTx(Transaction payoutTx) { this.payoutTx = payoutTx; } // Not used now, but will be used in some reporting UI @Nullable public Transaction getPayoutTx() { return payoutTx; } public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; errorMessageProperty.set(errorMessage); } public ReadOnlyStringProperty errorMessageProperty() { return errorMessageProperty; } public NodeAddress getArbitratorNodeAddress() { return arbitratorNodeAddress; } public void applyArbitratorNodeAddress(NodeAddress arbitratorNodeAddress) { this.arbitratorNodeAddress = arbitratorNodeAddress; Arbitrator arbitrator = processModel.getUser().getAcceptedArbitratorByAddress(arbitratorNodeAddress); checkNotNull(arbitrator, "arbitrator must not be null"); arbitratorBtcPubKey = arbitrator.getBtcPubKey(); } public byte[] getArbitratorPubKey() { // Prior to v0.4.8.4 we did not store the arbitratorBtcPubKey in the trade object so we need to support the // previously used version as well and request the arbitrator from the user object (but that caused sometimes a bug when // the client did not get delivered an arbitrator from the P2P network). if (arbitratorBtcPubKey == null) { Arbitrator arbitrator = processModel.getUser().getAcceptedArbitratorByAddress(arbitratorNodeAddress); checkNotNull(arbitrator, "arbitrator must not be null"); arbitratorBtcPubKey = arbitrator.getBtcPubKey(); } checkNotNull(arbitratorBtcPubKey, "ArbitratorPubKey must not be null"); return arbitratorBtcPubKey; } public String getTakerPaymentAccountId() { return takerPaymentAccountId; } public void setTakerPaymentAccountId(String takerPaymentAccountId) { this.takerPaymentAccountId = takerPaymentAccountId; } public void setContractHash(byte[] contractHash) { this.contractHash = contractHash; } public byte[] getContractHash() { return contractHash; } public void setTakeOfferFeeTxId(String takeOfferFeeTxId) { this.takeOfferFeeTxId = takeOfferFeeTxId; } @org.jetbrains.annotations.Nullable public String getTakeOfferFeeTxId() { return takeOfferFeeTxId; } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void setupConfidenceListener() { log.debug("setupConfidenceListener"); if (depositTx != null) { TransactionConfidence transactionConfidence = depositTx.getConfidence(); log.debug("transactionConfidence " + transactionConfidence.getDepthInBlocks()); if (transactionConfidence.getDepthInBlocks() > 0) { setConfirmedState(); } else { ListenableFuture<TransactionConfidence> future = transactionConfidence.getDepthFuture(1); Futures.addCallback(future, new FutureCallback<TransactionConfidence>() { @Override public void onSuccess(TransactionConfidence result) { log.debug("transactionConfidence " + transactionConfidence.getDepthInBlocks()); log.debug("state " + state); setConfirmedState(); } @Override public void onFailure(@NotNull Throwable t) { t.printStackTrace(); log.error(t.getMessage()); Throwables.propagate(t); } }); } } else { log.error("depositTx == null. That must not happen."); } } abstract protected void createProtocol(); private void setConfirmedState() { // we oly apply the state if we are not already further in the process if (state.ordinal() < State.DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN.ordinal()) setState(State.DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN); } abstract protected void initStates(); @Override public String toString() { return "Trade{" + "\n\ttradeAmount=" + tradeAmount + "\n\ttradingPeerNodeAddress=" + tradingPeerNodeAddress + "\n\ttradeVolume=" + tradeVolumeProperty.get() + "\n\toffer=" + offer + "\n\tprocessModel=" + processModel + "\n\tdecryptedMsgWithPubKey=" + decryptedMsgWithPubKey + "\n\ttakeOfferDate=" + takeOfferDate + "\n\tstate=" + state + "\n\tdisputeState=" + disputeState + "\n\ttradePeriodState=" + tradePeriodState + "\n\tdepositTx=" + depositTx + "\n\ttakeOfferFeeTxId=" + takeOfferFeeTxId + "\n\tcontract=" + contract + "\n\ttakerContractSignature.hashCode()='" + (takerContractSignature != null ? takerContractSignature.hashCode() : "") + '\'' + "\n\toffererContractSignature.hashCode()='" + (offererContractSignature != null ? offererContractSignature.hashCode() : "") + '\'' + "\n\tpayoutTx=" + payoutTx + "\n\tlockTimeAsBlockHeight=" + lockTimeAsBlockHeight + "\n\tarbitratorNodeAddress=" + arbitratorNodeAddress + "\n\ttakerPaymentAccountId='" + takerPaymentAccountId + '\'' + "\n\terrorMessage='" + errorMessage + '\'' + '}'; } }