/*
* 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.offer;
import io.bitsquare.app.DevFlags;
import io.bitsquare.app.Version;
import io.bitsquare.btc.Restrictions;
import io.bitsquare.btc.pricefeed.MarketPrice;
import io.bitsquare.btc.pricefeed.PriceFeedService;
import io.bitsquare.common.crypto.KeyRing;
import io.bitsquare.common.crypto.PubKeyRing;
import io.bitsquare.common.handlers.ErrorMessageHandler;
import io.bitsquare.common.handlers.ResultHandler;
import io.bitsquare.common.util.JsonExclude;
import io.bitsquare.common.util.MathUtils;
import io.bitsquare.common.util.Utilities;
import io.bitsquare.locale.CurrencyUtil;
import io.bitsquare.p2p.NodeAddress;
import io.bitsquare.p2p.storage.payload.RequiresOwnerIsOnlinePayload;
import io.bitsquare.p2p.storage.payload.StoragePayload;
import io.bitsquare.payment.PaymentMethod;
import io.bitsquare.trade.exceptions.MarketPriceNotAvailableException;
import io.bitsquare.trade.exceptions.TradePriceOutOfToleranceException;
import io.bitsquare.trade.protocol.availability.OfferAvailabilityModel;
import io.bitsquare.trade.protocol.availability.OfferAvailabilityProtocol;
import javafx.beans.property.*;
import org.bitcoinj.core.Coin;
import org.bitcoinj.utils.ExchangeRate;
import org.bitcoinj.utils.Fiat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.io.IOException;
import java.security.PublicKey;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
public final class Offer implements StoragePayload, RequiresOwnerIsOnlinePayload {
///////////////////////////////////////////////////////////////////////////////////////////
// Static
///////////////////////////////////////////////////////////////////////////////////////////
// That object is sent over the wire, so we need to take care of version compatibility.
@JsonExclude
private static final long serialVersionUID = Version.P2P_NETWORK_VERSION;
@JsonExclude
private static final Logger log = LoggerFactory.getLogger(Offer.class);
public static final long TTL = TimeUnit.MINUTES.toMillis(DevFlags.STRESS_TEST_MODE ? 6 : 6);
public final static String TAC_OFFERER = "With placing that offer I agree to trade " +
"with any trader who fulfills the conditions as defined above.";
public static final String TAC_TAKER = "With taking that offer I agree to the trade conditions as defined above.";
///////////////////////////////////////////////////////////////////////////////////////////
// Enums
///////////////////////////////////////////////////////////////////////////////////////////
public enum Direction {BUY, SELL}
public enum State {
UNDEFINED,
OFFER_FEE_PAID,
AVAILABLE,
NOT_AVAILABLE,
REMOVED,
OFFERER_OFFLINE
}
///////////////////////////////////////////////////////////////////////////////////////////
// Instance fields
///////////////////////////////////////////////////////////////////////////////////////////
// Fields for filtering offers
private final Direction direction;
private final String currencyCode;
// payment method
private final String paymentMethodName;
@Nullable
private final String countryCode;
@Nullable
private final ArrayList<String> acceptedCountryCodes;
@Nullable
private final String bankId;
@Nullable
private final ArrayList<String> acceptedBankIds;
private final ArrayList<NodeAddress> arbitratorNodeAddresses;
private final String id;
private final long date;
private final long protocolVersion;
// We use 2 type of prices: fixed price or price based on distance from market price
private final boolean useMarketBasedPrice;
// fiatPrice if fixed price is used (usePercentageBasedPrice = false), otherwise 0
private final long fiatPrice;
// Distance form market price if percentage based price is used (usePercentageBasedPrice = true), otherwise 0.
// E.g. 0.1 -> 10%. Can be negative as well. Depending on direction the marketPriceMargin is above or below the market price.
// Positive values is always the usual case where you want a better price as the market.
// E.g. Buy offer with market price 400.- leads to a 360.- price.
// Sell offer with market price 400.- leads to a 440.- price.
private final double marketPriceMargin;
private final long amount;
private final long minAmount;
private final NodeAddress offererNodeAddress;
@JsonExclude
private final PubKeyRing pubKeyRing;
private final String offererPaymentAccountId;
// Mutable property. Has to be set before offer is save in P2P network as it changes the objects hash!
private String offerFeePaymentTxID;
@JsonExclude
transient private State state = State.UNDEFINED;
// Those state properties are transient and only used at runtime!
// don't access directly as it might be null; use getStateProperty() which creates an object if not instantiated
@JsonExclude
transient private ObjectProperty<State> stateProperty = new SimpleObjectProperty<>(state);
@JsonExclude
@Nullable
transient private OfferAvailabilityProtocol availabilityProtocol;
@JsonExclude
transient private StringProperty errorMessageProperty = new SimpleStringProperty();
@JsonExclude
transient private PriceFeedService priceFeedService;
@JsonExclude
transient private DecimalFormat decimalFormat;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public Offer(String id,
NodeAddress offererNodeAddress,
PubKeyRing pubKeyRing,
Direction direction,
long fiatPrice,
double marketPriceMargin,
boolean useMarketBasedPrice,
long amount,
long minAmount,
String currencyCode,
ArrayList<NodeAddress> arbitratorNodeAddresses,
String paymentMethodName,
String offererPaymentAccountId,
@Nullable String countryCode,
@Nullable ArrayList<String> acceptedCountryCodes,
@Nullable String bankId,
@Nullable ArrayList<String> acceptedBankIds,
PriceFeedService priceFeedService) {
this.id = id;
this.offererNodeAddress = offererNodeAddress;
this.pubKeyRing = pubKeyRing;
this.direction = direction;
this.fiatPrice = fiatPrice;
this.marketPriceMargin = marketPriceMargin;
this.useMarketBasedPrice = useMarketBasedPrice;
this.amount = amount;
this.minAmount = minAmount;
this.currencyCode = currencyCode;
this.arbitratorNodeAddresses = arbitratorNodeAddresses;
this.paymentMethodName = paymentMethodName;
this.offererPaymentAccountId = offererPaymentAccountId;
this.countryCode = countryCode;
this.acceptedCountryCodes = acceptedCountryCodes;
this.bankId = bankId;
this.acceptedBankIds = acceptedBankIds;
this.priceFeedService = priceFeedService;
protocolVersion = Version.TRADE_PROTOCOL_VERSION;
date = new Date().getTime();
setState(State.UNDEFINED);
decimalFormat = new DecimalFormat("#.#");
decimalFormat.setMaximumFractionDigits(Fiat.SMALLEST_UNIT_EXPONENT);
}
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
try {
in.defaultReadObject();
stateProperty = new SimpleObjectProperty<>(State.UNDEFINED);
// we don't need to fill it as the error message is only relevant locally, so we don't store it in the transmitted object
errorMessageProperty = new SimpleStringProperty();
decimalFormat = new DecimalFormat("#.#");
decimalFormat.setMaximumFractionDigits(Fiat.SMALLEST_UNIT_EXPONENT);
} catch (Throwable t) {
log.warn("Cannot be deserialized." + t.getMessage());
}
}
@Override
public NodeAddress getOwnerNodeAddress() {
return offererNodeAddress;
}
public void validate() {
checkNotNull(getAmount(), "Amount is null");
checkNotNull(getArbitratorNodeAddresses(), "Arbitrator is null");
checkNotNull(getDate(), "CreationDate is null");
checkNotNull(getCurrencyCode(), "Currency is null");
checkNotNull(getDirection(), "Direction is null");
checkNotNull(getId(), "Id is null");
checkNotNull(getPubKeyRing(), "pubKeyRing is null");
checkNotNull(getMinAmount(), "MinAmount is null");
checkNotNull(getPrice(), "Price is null");
checkArgument(getMinAmount().compareTo(Restrictions.MIN_TRADE_AMOUNT) >= 0, "MinAmount is less then "
+ Restrictions.MIN_TRADE_AMOUNT.toFriendlyString());
checkArgument(getAmount().compareTo(getPaymentMethod().getMaxTradeLimit()) <= 0, "Amount is larger then "
+ getPaymentMethod().getMaxTradeLimit().toFriendlyString());
checkArgument(getAmount().compareTo(getMinAmount()) >= 0, "MinAmount is larger then Amount");
checkArgument(getPrice().isPositive(), "Price is not a positive value");
// TODO check upper and lower bounds for fiat
}
public void resetState() {
setState(State.UNDEFINED);
}
public boolean isMyOffer(KeyRing keyRing) {
return getPubKeyRing().equals(keyRing.getPubKeyRing());
}
@Nullable
public Fiat getVolumeByAmount(Coin amount) {
Fiat price = getPrice();
if (price != null && amount != null) {
try {
return new ExchangeRate(price).coinToFiat(amount);
} catch (Throwable t) {
log.error("getVolumeByAmount failed. Error=" + t.getMessage());
return null;
}
} else {
return null;
}
}
@Nullable
public Fiat getOfferVolume() {
return getVolumeByAmount(getAmount());
}
@Nullable
public Fiat getMinOfferVolume() {
return getVolumeByAmount(getMinAmount());
}
///////////////////////////////////////////////////////////////////////////////////////////
// Availability
///////////////////////////////////////////////////////////////////////////////////////////
public void checkOfferAvailability(OfferAvailabilityModel model, ResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) {
availabilityProtocol = new OfferAvailabilityProtocol(model,
() -> {
cancelAvailabilityRequest();
resultHandler.handleResult();
},
(errorMessage) -> {
if (availabilityProtocol != null)
availabilityProtocol.cancel();
log.error(errorMessage);
errorMessageHandler.handleErrorMessage(errorMessage);
});
availabilityProtocol.sendOfferAvailabilityRequest();
}
public void cancelAvailabilityRequest() {
if (availabilityProtocol != null)
availabilityProtocol.cancel();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Setters
///////////////////////////////////////////////////////////////////////////////////////////
public void setPriceFeedService(PriceFeedService priceFeedService) {
this.priceFeedService = priceFeedService;
}
public void setState(State state) {
this.state = state;
stateProperty().set(state);
}
public void setOfferFeePaymentTxID(String offerFeePaymentTxID) {
this.offerFeePaymentTxID = offerFeePaymentTxID;
}
public void setErrorMessage(String errorMessage) {
this.errorMessageProperty.set(errorMessage);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getters
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public long getTTL() {
return TTL;
}
@Override
public PublicKey getOwnerPubKey() {
return pubKeyRing != null ? pubKeyRing.getSignaturePubKey() : null;
}
public long getProtocolVersion() {
return protocolVersion;
}
public String getId() {
return id;
}
public String getShortId() {
return Utilities.getShortId(id);
}
public NodeAddress getOffererNodeAddress() {
return offererNodeAddress;
}
public PubKeyRing getPubKeyRing() {
return pubKeyRing;
}
@Nullable
public Fiat getPrice() {
if (useMarketBasedPrice) {
checkNotNull(priceFeedService, "priceFeed must not be null");
MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
if (marketPrice != null) {
PriceFeedService.Type priceFeedType;
double factor;
if (CurrencyUtil.isCryptoCurrency(currencyCode)) {
priceFeedType = direction == Direction.BUY ? PriceFeedService.Type.ASK : PriceFeedService.Type.BID;
factor = direction == Offer.Direction.SELL ? 1 - marketPriceMargin : 1 + marketPriceMargin;
} else {
priceFeedType = direction == Direction.SELL ? PriceFeedService.Type.ASK : PriceFeedService.Type.BID;
factor = direction == Offer.Direction.BUY ? 1 - marketPriceMargin : 1 + marketPriceMargin;
}
double marketPriceAsDouble = marketPrice.getPrice(priceFeedType);
double targetPrice = marketPriceAsDouble * factor;
if (CurrencyUtil.isCryptoCurrency(currencyCode))
targetPrice = targetPrice != 0 ? 1d / targetPrice : 0;
try {
final double rounded = MathUtils.roundDouble(targetPrice, Fiat.SMALLEST_UNIT_EXPONENT);
return Fiat.parseFiat(currencyCode, decimalFormat.format(rounded).replace(",", "."));
} catch (Exception e) {
log.error("Exception at getPrice / parseToFiat: " + e.toString() + "\n" +
"That case should never happen.");
return null;
}
} else {
log.debug("We don't have a market price.\n" +
"That case could only happen if you don't have a price feed.");
return null;
}
} else {
return Fiat.valueOf(currencyCode, fiatPrice);
}
}
public void checkTradePriceTolerance(long takersTradePrice) throws TradePriceOutOfToleranceException, MarketPriceNotAvailableException, IllegalArgumentException {
checkArgument(takersTradePrice > 0, "takersTradePrice must be positive");
Fiat tradePriceAsFiat = Fiat.valueOf(getCurrencyCode(), takersTradePrice);
Fiat offerPriceAsFiat = getPrice();
if (offerPriceAsFiat == null)
throw new MarketPriceNotAvailableException("Market price required for calculating trade price is not available.");
double factor = (double) takersTradePrice / (double) offerPriceAsFiat.value;
// We allow max. 2 % difference between own offer price calculation and takers calculation.
// Market price might be different at offerer's and takers side so we need a bit of tolerance.
// The tolerance will get smaller once we have multiple price feeds avoiding fast price fluctuations
// from one provider.
if (Math.abs(1 - factor) > 0.02) {
String msg = "Taker's trade price is too far away from our calculated price based on the market price.\n" +
"tradePriceAsFiat=" + tradePriceAsFiat.toFriendlyString() + "\n" +
"offerPriceAsFiat=" + offerPriceAsFiat.toFriendlyString();
log.warn(msg);
throw new TradePriceOutOfToleranceException(msg);
}
}
public double getMarketPriceMargin() {
return marketPriceMargin;
}
public boolean getUseMarketBasedPrice() {
return useMarketBasedPrice;
}
public Coin getAmount() {
return Coin.valueOf(amount);
}
public Coin getMinAmount() {
return Coin.valueOf(minAmount);
}
public Direction getDirection() {
return direction;
}
public Direction getMirroredDirection() {
return direction == Direction.BUY ? Direction.SELL : Direction.BUY;
}
public PaymentMethod getPaymentMethod() {
return PaymentMethod.getPaymentMethodById(paymentMethodName);
}
public String getCurrencyCode() {
return currencyCode;
}
@Nullable
public String getCountryCode() {
return countryCode;
}
@Nullable
public List<String> getAcceptedCountryCodes() {
return acceptedCountryCodes;
}
@Nullable
public List<String> getAcceptedBankIds() {
return acceptedBankIds;
}
@Nullable
public String getBankId() {
return bankId;
}
public String getOfferFeePaymentTxID() {
return offerFeePaymentTxID;
}
public List<NodeAddress> getArbitratorNodeAddresses() {
return arbitratorNodeAddresses;
}
public Date getDate() {
return new Date(date);
}
public State getState() {
return state;
}
public ObjectProperty<State> stateProperty() {
return stateProperty;
}
public String getOffererPaymentAccountId() {
return offererPaymentAccountId;
}
public ReadOnlyStringProperty errorMessageProperty() {
return errorMessageProperty;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Offer)) return false;
Offer that = (Offer) o;
if (date != that.date) return false;
if (fiatPrice != that.fiatPrice) return false;
if (Double.compare(that.marketPriceMargin, marketPriceMargin) != 0) return false;
if (useMarketBasedPrice != that.useMarketBasedPrice) return false;
if (amount != that.amount) return false;
if (minAmount != that.minAmount) return false;
if (getId() != null ? !getId().equals(that.getId()) : that.getId() != null) return false;
if (direction != null && that.direction != null && direction.ordinal() != that.direction.ordinal())
return false;
else if ((direction == null && that.direction != null) || (direction != null && that.direction == null))
return false;
if (currencyCode != null ? !currencyCode.equals(that.currencyCode) : that.currencyCode != null) return false;
if (offererNodeAddress != null ? !offererNodeAddress.equals(that.offererNodeAddress) : that.offererNodeAddress != null)
return false;
if (pubKeyRing != null ? !pubKeyRing.equals(that.pubKeyRing) : that.pubKeyRing != null) return false;
if (paymentMethodName != null ? !paymentMethodName.equals(that.paymentMethodName) : that.paymentMethodName != null)
return false;
if (countryCode != null ? !countryCode.equals(that.countryCode) : that.countryCode != null)
return false;
if (offererPaymentAccountId != null ? !offererPaymentAccountId.equals(that.offererPaymentAccountId) : that.offererPaymentAccountId != null)
return false;
if (acceptedCountryCodes != null ? !acceptedCountryCodes.equals(that.acceptedCountryCodes) : that.acceptedCountryCodes != null)
return false;
if (bankId != null ? !bankId.equals(that.bankId) : that.bankId != null) return false;
if (acceptedBankIds != null ? !acceptedBankIds.equals(that.acceptedBankIds) : that.acceptedBankIds != null)
return false;
if (arbitratorNodeAddresses != null ? !arbitratorNodeAddresses.equals(that.arbitratorNodeAddresses) : that.arbitratorNodeAddresses != null)
return false;
return !(offerFeePaymentTxID != null ? !offerFeePaymentTxID.equals(that.offerFeePaymentTxID) : that.offerFeePaymentTxID != null);
}
@Override
public int hashCode() {
int result = getId() != null ? getId().hashCode() : 0;
result = 31 * result + (direction != null ? direction.ordinal() : 0);
result = 31 * result + (currencyCode != null ? currencyCode.hashCode() : 0);
result = 31 * result + (int) (date ^ (date >>> 32));
result = 31 * result + (int) (fiatPrice ^ (fiatPrice >>> 32));
long temp = Double.doubleToLongBits(marketPriceMargin);
result = 31 * result + (int) (temp ^ (temp >>> 32));
result = 31 * result + (useMarketBasedPrice ? 1 : 0);
result = 31 * result + (int) (amount ^ (amount >>> 32));
result = 31 * result + (int) (minAmount ^ (minAmount >>> 32));
result = 31 * result + (offererNodeAddress != null ? offererNodeAddress.hashCode() : 0);
result = 31 * result + (pubKeyRing != null ? pubKeyRing.hashCode() : 0);
result = 31 * result + (paymentMethodName != null ? paymentMethodName.hashCode() : 0);
result = 31 * result + (countryCode != null ? countryCode.hashCode() : 0);
result = 31 * result + (bankId != null ? bankId.hashCode() : 0);
result = 31 * result + (offererPaymentAccountId != null ? offererPaymentAccountId.hashCode() : 0);
result = 31 * result + (acceptedCountryCodes != null ? acceptedCountryCodes.hashCode() : 0);
result = 31 * result + (acceptedBankIds != null ? acceptedBankIds.hashCode() : 0);
result = 31 * result + (arbitratorNodeAddresses != null ? arbitratorNodeAddresses.hashCode() : 0);
result = 31 * result + (offerFeePaymentTxID != null ? offerFeePaymentTxID.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "Offer{" +
"\n\tid='" + getId() + '\'' +
"\n\tdirection=" + direction +
"\n\tcurrencyCode='" + currencyCode + '\'' +
"\n\tdate=" + new Date(date) +
"\n\tdateAsTime=" + date +
"\n\tfiatPrice=" + fiatPrice +
"\n\tmarketPriceMargin=" + marketPriceMargin +
"\n\tuseMarketBasedPrice=" + useMarketBasedPrice +
"\n\tamount=" + amount +
"\n\tminAmount=" + minAmount +
"\n\toffererAddress=" + offererNodeAddress +
"\n\tpubKeyRing=" + pubKeyRing +
"\n\tpaymentMethodName='" + paymentMethodName + '\'' +
"\n\tpaymentMethodCountryCode='" + countryCode + '\'' +
"\n\toffererPaymentAccountId='" + offererPaymentAccountId + '\'' +
"\n\tacceptedCountryCodes=" + acceptedCountryCodes +
"\n\tbankId=" + bankId +
"\n\tacceptedBanks=" + acceptedBankIds +
"\n\tarbitratorAddresses=" + arbitratorNodeAddresses +
"\n\tofferFeePaymentTxID='" + offerFeePaymentTxID + '\'' +
"\n\tstate=" + state +
"\n\tstateProperty=" + stateProperty +
"\n\tavailabilityProtocol=" + availabilityProtocol +
"\n\terrorMessageProperty=" + errorMessageProperty +
"\n\tTAC_OFFERER=" + TAC_OFFERER +
"\n\tTAC_TAKER=" + TAC_TAKER +
"\n\thashCode=" + hashCode() +
'}';
}
}