Merge branch 'percentbasedprice' into Development

This commit is contained in:
Manfred Karrer 2016-04-16 11:46:27 +02:00
commit 1434d37733
45 changed files with 613 additions and 152 deletions

View file

@ -22,6 +22,7 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
// TODO use https://github.com/timmolter/XChange
public class PriceFeed { public class PriceFeed {
private static final Logger log = LoggerFactory.getLogger(PriceFeed.class); private static final Logger log = LoggerFactory.getLogger(PriceFeed.class);
@ -44,9 +45,9 @@ public class PriceFeed {
} }
} }
private static final long PERIOD_FIAT_SEC = 2 * 60; private static final long PERIOD_FIAT_SEC = 90;
private static final long PERIOD_ALL_FIAT_SEC = 60 * 5; private static final long PERIOD_ALL_FIAT_SEC = 60 * 3;
private static final long PERIOD_ALL_CRYPTO_SEC = 60 * 5; private static final long PERIOD_ALL_CRYPTO_SEC = 60 * 3;
private final Map<String, MarketPrice> cache = new HashMap<>(); private final Map<String, MarketPrice> cache = new HashMap<>();
private final PriceProvider fiatPriceProvider = new BitcoinAveragePriceProvider(); private final PriceProvider fiatPriceProvider = new BitcoinAveragePriceProvider();

View file

@ -7,7 +7,6 @@ import java.io.IOException;
import java.io.Serializable; import java.io.Serializable;
import java.util.Map; import java.util.Map;
// https://api.bitfinex.com/v1/pubticker/BTCUSD
public interface PriceProvider extends Serializable { public interface PriceProvider extends Serializable {
Map<String, MarketPrice> getAllPrices() throws IOException, HttpException; Map<String, MarketPrice> getAllPrices() throws IOException, HttpException;

View file

@ -18,22 +18,18 @@
package io.bitsquare.trade; package io.bitsquare.trade;
import io.bitsquare.app.Version; import io.bitsquare.app.Version;
import io.bitsquare.btc.FeePolicy;
import io.bitsquare.p2p.NodeAddress; import io.bitsquare.p2p.NodeAddress;
import io.bitsquare.storage.Storage; import io.bitsquare.storage.Storage;
import io.bitsquare.trade.offer.Offer; import io.bitsquare.trade.offer.Offer;
import io.bitsquare.trade.protocol.trade.BuyerAsOffererProtocol; import io.bitsquare.trade.protocol.trade.BuyerAsOffererProtocol;
import io.bitsquare.trade.protocol.trade.OffererProtocol; import io.bitsquare.trade.protocol.trade.OffererProtocol;
import io.bitsquare.trade.protocol.trade.messages.TradeMessage; import io.bitsquare.trade.protocol.trade.messages.TradeMessage;
import org.bitcoinj.core.Coin;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.io.ObjectInputStream; import java.io.ObjectInputStream;
import static com.google.common.base.Preconditions.checkNotNull;
public final class BuyerAsOffererTrade extends BuyerTrade implements OffererTrade { public final class BuyerAsOffererTrade extends BuyerTrade implements OffererTrade {
// That object is saved to disc. We need to take care of changes to not break deserialization. // 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 long serialVersionUID = Version.LOCAL_DB_VERSION;
@ -73,12 +69,4 @@ public final class BuyerAsOffererTrade extends BuyerTrade implements OffererTrad
public void handleTakeOfferRequest(TradeMessage message, NodeAddress taker) { public void handleTakeOfferRequest(TradeMessage message, NodeAddress taker) {
((OffererProtocol) tradeProtocol).handleTakeOfferRequest(message, taker); ((OffererProtocol) tradeProtocol).handleTakeOfferRequest(message, taker);
} }
@Override
public Coin getPayoutAmount() {
checkNotNull(getTradeAmount(), "Invalid state: getTradeAmount() = null");
return FeePolicy.getSecurityDeposit().add(getTradeAmount());
}
} }

View file

@ -18,7 +18,6 @@
package io.bitsquare.trade; package io.bitsquare.trade;
import io.bitsquare.app.Version; import io.bitsquare.app.Version;
import io.bitsquare.btc.FeePolicy;
import io.bitsquare.p2p.NodeAddress; import io.bitsquare.p2p.NodeAddress;
import io.bitsquare.storage.Storage; import io.bitsquare.storage.Storage;
import io.bitsquare.trade.offer.Offer; import io.bitsquare.trade.offer.Offer;
@ -32,7 +31,6 @@ import java.io.IOException;
import java.io.ObjectInputStream; import java.io.ObjectInputStream;
import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
public final class BuyerAsTakerTrade extends BuyerTrade implements TakerTrade { public final class BuyerAsTakerTrade extends BuyerTrade implements TakerTrade {
// That object is saved to disc. We need to take care of changes to not break deserialization. // That object is saved to disc. We need to take care of changes to not break deserialization.
@ -45,8 +43,8 @@ public final class BuyerAsTakerTrade extends BuyerTrade implements TakerTrade {
// Constructor, initialization // Constructor, initialization
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
public BuyerAsTakerTrade(Offer offer, Coin tradeAmount, NodeAddress tradingPeerNodeAddress, Storage<? extends TradableList> storage) { public BuyerAsTakerTrade(Offer offer, Coin tradeAmount, long tradePrice, NodeAddress tradingPeerNodeAddress, Storage<? extends TradableList> storage) {
super(offer, tradeAmount, tradingPeerNodeAddress, storage); super(offer, tradeAmount, tradePrice, tradingPeerNodeAddress, storage);
} }
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
@ -74,11 +72,4 @@ public final class BuyerAsTakerTrade extends BuyerTrade implements TakerTrade {
checkArgument(tradeProtocol instanceof TakerProtocol, "tradeProtocol NOT instanceof TakerProtocol"); checkArgument(tradeProtocol instanceof TakerProtocol, "tradeProtocol NOT instanceof TakerProtocol");
((TakerProtocol) tradeProtocol).takeAvailableOffer(); ((TakerProtocol) tradeProtocol).takeAvailableOffer();
} }
@Override
public Coin getPayoutAmount() {
checkNotNull(getTradeAmount(), "Invalid state: getTradeAmount() = null");
return FeePolicy.getSecurityDeposit().add(getTradeAmount());
}
} }

View file

@ -18,6 +18,7 @@
package io.bitsquare.trade; package io.bitsquare.trade;
import io.bitsquare.app.Version; import io.bitsquare.app.Version;
import io.bitsquare.btc.FeePolicy;
import io.bitsquare.common.handlers.ErrorMessageHandler; import io.bitsquare.common.handlers.ErrorMessageHandler;
import io.bitsquare.common.handlers.ResultHandler; import io.bitsquare.common.handlers.ResultHandler;
import io.bitsquare.p2p.NodeAddress; import io.bitsquare.p2p.NodeAddress;
@ -29,6 +30,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
public abstract class BuyerTrade extends Trade { public abstract class BuyerTrade extends Trade {
// That object is saved to disc. We need to take care of changes to not break deserialization. // That object is saved to disc. We need to take care of changes to not break deserialization.
@ -36,8 +38,8 @@ public abstract class BuyerTrade extends Trade {
private static final Logger log = LoggerFactory.getLogger(BuyerAsOffererTrade.class); private static final Logger log = LoggerFactory.getLogger(BuyerAsOffererTrade.class);
BuyerTrade(Offer offer, Coin tradeAmount, NodeAddress tradingPeerNodeAddress, Storage<? extends TradableList> storage) { BuyerTrade(Offer offer, Coin tradeAmount, long tradePrice, NodeAddress tradingPeerNodeAddress, Storage<? extends TradableList> storage) {
super(offer, tradeAmount, tradingPeerNodeAddress, storage); super(offer, tradeAmount, tradePrice, tradingPeerNodeAddress, storage);
} }
BuyerTrade(Offer offer, Storage<? extends TradableList> storage) { BuyerTrade(Offer offer, Storage<? extends TradableList> storage) {
@ -64,6 +66,12 @@ public abstract class BuyerTrade extends Trade {
} }
} }
@Override
public Coin getPayoutAmount() {
checkNotNull(getTradeAmount(), "Invalid state: getTradeAmount() = null");
return FeePolicy.getSecurityDeposit().add(getTradeAmount());
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Setter for Mutable objects // Setter for Mutable objects

View file

@ -25,6 +25,7 @@ import io.bitsquare.p2p.NodeAddress;
import io.bitsquare.payment.PaymentAccountContractData; import io.bitsquare.payment.PaymentAccountContractData;
import io.bitsquare.trade.offer.Offer; import io.bitsquare.trade.offer.Offer;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
import org.bitcoinj.utils.Fiat;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
import java.util.Arrays; import java.util.Arrays;
@ -40,6 +41,7 @@ public final class Contract implements Payload {
public final Offer offer; public final Offer offer;
private final long tradeAmount; private final long tradeAmount;
private final long tradePrice;
public final String takeOfferFeeTxID; public final String takeOfferFeeTxID;
public final NodeAddress arbitratorNodeAddress; public final NodeAddress arbitratorNodeAddress;
private final boolean isBuyerOffererAndSellerTaker; private final boolean isBuyerOffererAndSellerTaker;
@ -64,6 +66,7 @@ public final class Contract implements Payload {
public Contract(Offer offer, public Contract(Offer offer,
Coin tradeAmount, Coin tradeAmount,
Fiat tradePrice,
String takeOfferFeeTxID, String takeOfferFeeTxID,
NodeAddress buyerNodeAddress, NodeAddress buyerNodeAddress,
NodeAddress sellerNodeAddress, NodeAddress sellerNodeAddress,
@ -80,6 +83,7 @@ public final class Contract implements Payload {
byte[] offererBtcPubKey, byte[] offererBtcPubKey,
byte[] takerBtcPubKey) { byte[] takerBtcPubKey) {
this.offer = offer; this.offer = offer;
this.tradePrice = tradePrice.value;
this.buyerNodeAddress = buyerNodeAddress; this.buyerNodeAddress = buyerNodeAddress;
this.sellerNodeAddress = sellerNodeAddress; this.sellerNodeAddress = sellerNodeAddress;
this.tradeAmount = tradeAmount.value; this.tradeAmount = tradeAmount.value;
@ -154,6 +158,10 @@ public final class Contract implements Payload {
return Coin.valueOf(tradeAmount); return Coin.valueOf(tradeAmount);
} }
public Fiat getTradePrice() {
return Fiat.valueOf(offer.getCurrencyCode(), tradePrice);
}
public NodeAddress getBuyerNodeAddress() { public NodeAddress getBuyerNodeAddress() {
return buyerNodeAddress; return buyerNodeAddress;
} }
@ -171,6 +179,7 @@ public final class Contract implements Payload {
Contract contract = (Contract) o; Contract contract = (Contract) o;
if (tradeAmount != contract.tradeAmount) return false; if (tradeAmount != contract.tradeAmount) return false;
if (tradePrice != contract.tradePrice) return false;
if (isBuyerOffererAndSellerTaker != contract.isBuyerOffererAndSellerTaker) return false; if (isBuyerOffererAndSellerTaker != contract.isBuyerOffererAndSellerTaker) return false;
if (offer != null ? !offer.equals(contract.offer) : contract.offer != null) return false; if (offer != null ? !offer.equals(contract.offer) : contract.offer != null) return false;
if (takeOfferFeeTxID != null ? !takeOfferFeeTxID.equals(contract.takeOfferFeeTxID) : contract.takeOfferFeeTxID != null) if (takeOfferFeeTxID != null ? !takeOfferFeeTxID.equals(contract.takeOfferFeeTxID) : contract.takeOfferFeeTxID != null)
@ -206,6 +215,7 @@ public final class Contract implements Payload {
public int hashCode() { public int hashCode() {
int result = offer != null ? offer.hashCode() : 0; int result = offer != null ? offer.hashCode() : 0;
result = 31 * result + (int) (tradeAmount ^ (tradeAmount >>> 32)); result = 31 * result + (int) (tradeAmount ^ (tradeAmount >>> 32));
result = 31 * result + (int) (tradePrice ^ (tradePrice >>> 32));
result = 31 * result + (takeOfferFeeTxID != null ? takeOfferFeeTxID.hashCode() : 0); result = 31 * result + (takeOfferFeeTxID != null ? takeOfferFeeTxID.hashCode() : 0);
result = 31 * result + (arbitratorNodeAddress != null ? arbitratorNodeAddress.hashCode() : 0); result = 31 * result + (arbitratorNodeAddress != null ? arbitratorNodeAddress.hashCode() : 0);
result = 31 * result + (isBuyerOffererAndSellerTaker ? 1 : 0); result = 31 * result + (isBuyerOffererAndSellerTaker ? 1 : 0);
@ -229,6 +239,7 @@ public final class Contract implements Payload {
return "Contract{" + return "Contract{" +
"\n\toffer=" + offer + "\n\toffer=" + offer +
"\n\ttradeAmount=" + tradeAmount + "\n\ttradeAmount=" + tradeAmount +
"\n\ttradePrice=" + tradePrice +
"\n\ttakeOfferFeeTxID='" + takeOfferFeeTxID + '\'' + "\n\ttakeOfferFeeTxID='" + takeOfferFeeTxID + '\'' +
"\n\tarbitratorAddress=" + arbitratorNodeAddress + "\n\tarbitratorAddress=" + arbitratorNodeAddress +
"\n\tisBuyerOffererAndSellerTaker=" + isBuyerOffererAndSellerTaker + "\n\tisBuyerOffererAndSellerTaker=" + isBuyerOffererAndSellerTaker +

View file

@ -42,8 +42,8 @@ public final class SellerAsTakerTrade extends SellerTrade implements TakerTrade
// Constructor, initialization // Constructor, initialization
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
public SellerAsTakerTrade(Offer offer, Coin tradeAmount, NodeAddress tradingPeerNodeAddress, Storage<? extends TradableList> storage) { public SellerAsTakerTrade(Offer offer, Coin tradeAmount, long tradePrice, NodeAddress tradingPeerNodeAddress, Storage<? extends TradableList> storage) {
super(offer, tradeAmount, tradingPeerNodeAddress, storage); super(offer, tradeAmount, tradePrice, tradingPeerNodeAddress, storage);
} }
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {

View file

@ -18,6 +18,7 @@
package io.bitsquare.trade; package io.bitsquare.trade;
import io.bitsquare.app.Version; import io.bitsquare.app.Version;
import io.bitsquare.btc.FeePolicy;
import io.bitsquare.common.handlers.ErrorMessageHandler; import io.bitsquare.common.handlers.ErrorMessageHandler;
import io.bitsquare.common.handlers.ResultHandler; import io.bitsquare.common.handlers.ResultHandler;
import io.bitsquare.p2p.NodeAddress; import io.bitsquare.p2p.NodeAddress;
@ -36,8 +37,8 @@ public abstract class SellerTrade extends Trade {
private static final Logger log = LoggerFactory.getLogger(BuyerAsTakerTrade.class); private static final Logger log = LoggerFactory.getLogger(BuyerAsTakerTrade.class);
SellerTrade(Offer offer, Coin tradeAmount, NodeAddress tradingPeerNodeAddress, Storage<? extends TradableList> storage) { SellerTrade(Offer offer, Coin tradeAmount, long tradePrice, NodeAddress tradingPeerNodeAddress, Storage<? extends TradableList> storage) {
super(offer, tradeAmount, tradingPeerNodeAddress, storage); super(offer, tradeAmount, tradePrice, tradingPeerNodeAddress, storage);
} }
SellerTrade(Offer offer, Storage<? extends TradableList> storage) { SellerTrade(Offer offer, Storage<? extends TradableList> storage) {
@ -64,6 +65,11 @@ public abstract class SellerTrade extends Trade {
} }
} }
@Override
public Coin getPayoutAmount() {
return FeePolicy.getSecurityDeposit();
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Setter for Mutable objects // Setter for Mutable objects
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////

View file

@ -24,7 +24,6 @@ import com.google.common.util.concurrent.ListenableFuture;
import io.bitsquare.app.Log; import io.bitsquare.app.Log;
import io.bitsquare.app.Version; import io.bitsquare.app.Version;
import io.bitsquare.arbitration.ArbitratorManager; import io.bitsquare.arbitration.ArbitratorManager;
import io.bitsquare.btc.FeePolicy;
import io.bitsquare.btc.TradeWalletService; import io.bitsquare.btc.TradeWalletService;
import io.bitsquare.btc.WalletService; import io.bitsquare.btc.WalletService;
import io.bitsquare.common.crypto.KeyRing; import io.bitsquare.common.crypto.KeyRing;
@ -42,6 +41,7 @@ import javafx.beans.property.*;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Transaction; import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionConfidence; import org.bitcoinj.core.TransactionConfidence;
import org.bitcoinj.utils.ExchangeRate;
import org.bitcoinj.utils.Fiat; import org.bitcoinj.utils.Fiat;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -168,6 +168,7 @@ public abstract class Trade implements Tradable, Model {
transient private ObjectProperty<Fiat> tradeVolumeProperty; transient private ObjectProperty<Fiat> tradeVolumeProperty;
@Nullable @Nullable
private String takeOfferFeeTxId; private String takeOfferFeeTxId;
private long tradePrice;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -190,11 +191,12 @@ public abstract class Trade implements Tradable, Model {
} }
// taker // taker
protected Trade(Offer offer, Coin tradeAmount, NodeAddress tradingPeerNodeAddress, protected Trade(Offer offer, Coin tradeAmount, long tradePrice, NodeAddress tradingPeerNodeAddress,
Storage<? extends TradableList> storage) { Storage<? extends TradableList> storage) {
this(offer, storage); this(offer, storage);
this.tradeAmount = tradeAmount; this.tradeAmount = tradeAmount;
this.tradePrice = tradePrice;
this.tradingPeerNodeAddress = tradingPeerNodeAddress; this.tradingPeerNodeAddress = tradingPeerNodeAddress;
tradeAmountProperty.set(tradeAmount); tradeAmountProperty.set(tradeAmount);
tradeVolumeProperty.set(getTradeVolume()); tradeVolumeProperty.set(getTradeVolume());
@ -374,9 +376,7 @@ public abstract class Trade implements Tradable, Model {
return offer; return offer;
} }
public Coin getPayoutAmount() { abstract public Coin getPayoutAmount();
return FeePolicy.getSecurityDeposit();
}
public ProcessModel getProcessModel() { public ProcessModel getProcessModel() {
return processModel; return processModel;
@ -384,8 +384,8 @@ public abstract class Trade implements Tradable, Model {
@Nullable @Nullable
public Fiat getTradeVolume() { public Fiat getTradeVolume() {
if (tradeAmount != null) if (tradeAmount != null && getTradePrice() != null)
return offer.getVolumeByAmount(tradeAmount); return new ExchangeRate(getTradePrice()).coinToFiat(tradeAmount);
else else
return null; return null;
} }
@ -455,6 +455,14 @@ public abstract class Trade implements Tradable, Model {
tradeVolumeProperty.set(getTradeVolume()); tradeVolumeProperty.set(getTradeVolume());
} }
public void setTradePrice(long tradePrice) {
this.tradePrice = tradePrice;
}
public Fiat getTradePrice() {
return Fiat.valueOf(offer.getCurrencyCode(), tradePrice);
}
@Nullable @Nullable
public Coin getTradeAmount() { public Coin getTradeAmount() {
return tradeAmount; return tradeAmount;

View file

@ -24,6 +24,7 @@ import io.bitsquare.btc.AddressEntry;
import io.bitsquare.btc.AddressEntryException; import io.bitsquare.btc.AddressEntryException;
import io.bitsquare.btc.TradeWalletService; import io.bitsquare.btc.TradeWalletService;
import io.bitsquare.btc.WalletService; import io.bitsquare.btc.WalletService;
import io.bitsquare.btc.pricefeed.PriceFeed;
import io.bitsquare.common.crypto.KeyRing; import io.bitsquare.common.crypto.KeyRing;
import io.bitsquare.common.handlers.FaultHandler; import io.bitsquare.common.handlers.FaultHandler;
import io.bitsquare.common.handlers.ResultHandler; import io.bitsquare.common.handlers.ResultHandler;
@ -97,6 +98,7 @@ public class TradeManager {
FailedTradesManager failedTradesManager, FailedTradesManager failedTradesManager,
ArbitratorManager arbitratorManager, ArbitratorManager arbitratorManager,
P2PService p2PService, P2PService p2PService,
PriceFeed priceFeed,
@Named("storage.dir") File storageDir) { @Named("storage.dir") File storageDir) {
this.user = user; this.user = user;
this.keyRing = keyRing; this.keyRing = keyRing;
@ -109,7 +111,8 @@ public class TradeManager {
this.p2PService = p2PService; this.p2PService = p2PService;
tradableListStorage = new Storage<>(storageDir); tradableListStorage = new Storage<>(storageDir);
this.trades = new TradableList<>(tradableListStorage, "PendingTrades"); trades = new TradableList<>(tradableListStorage, "PendingTrades");
trades.forEach(e -> e.getOffer().setPriceFeed(priceFeed));
p2PService.addDecryptedDirectMessageListener(new DecryptedDirectMessageListener() { p2PService.addDecryptedDirectMessageListener(new DecryptedDirectMessageListener() {
@Override @Override
@ -261,6 +264,7 @@ public class TradeManager {
// First we check if offer is still available then we create the trade with the protocol // First we check if offer is still available then we create the trade with the protocol
public void onTakeOffer(Coin amount, public void onTakeOffer(Coin amount,
long tradePrice,
Coin fundsNeededForTrade, Coin fundsNeededForTrade,
Offer offer, Offer offer,
String paymentAccountId, String paymentAccountId,
@ -270,11 +274,12 @@ public class TradeManager {
offer.checkOfferAvailability(model, offer.checkOfferAvailability(model,
() -> { () -> {
if (offer.getState() == Offer.State.AVAILABLE) if (offer.getState() == Offer.State.AVAILABLE)
createTrade(amount, fundsNeededForTrade, offer, paymentAccountId, useSavingsWallet, model, tradeResultHandler); createTrade(amount, tradePrice, fundsNeededForTrade, offer, paymentAccountId, useSavingsWallet, model, tradeResultHandler);
}); });
} }
private void createTrade(Coin amount, private void createTrade(Coin amount,
long tradePrice,
Coin fundsNeededForTrade, Coin fundsNeededForTrade,
Offer offer, Offer offer,
String paymentAccountId, String paymentAccountId,
@ -283,9 +288,9 @@ public class TradeManager {
TradeResultHandler tradeResultHandler) { TradeResultHandler tradeResultHandler) {
Trade trade; Trade trade;
if (offer.getDirection() == Offer.Direction.BUY) if (offer.getDirection() == Offer.Direction.BUY)
trade = new SellerAsTakerTrade(offer, amount, model.getPeerNodeAddress(), tradableListStorage); trade = new SellerAsTakerTrade(offer, amount, tradePrice, model.getPeerNodeAddress(), tradableListStorage);
else else
trade = new BuyerAsTakerTrade(offer, amount, model.getPeerNodeAddress(), tradableListStorage); trade = new BuyerAsTakerTrade(offer, amount, tradePrice, model.getPeerNodeAddress(), tradableListStorage);
trade.setTakerPaymentAccountId(paymentAccountId); trade.setTakerPaymentAccountId(paymentAccountId);

View file

@ -18,6 +18,7 @@
package io.bitsquare.trade.closed; package io.bitsquare.trade.closed;
import com.google.inject.Inject; import com.google.inject.Inject;
import io.bitsquare.btc.pricefeed.PriceFeed;
import io.bitsquare.common.crypto.KeyRing; import io.bitsquare.common.crypto.KeyRing;
import io.bitsquare.storage.Storage; import io.bitsquare.storage.Storage;
import io.bitsquare.trade.Tradable; import io.bitsquare.trade.Tradable;
@ -37,9 +38,10 @@ public class ClosedTradableManager {
private final KeyRing keyRing; private final KeyRing keyRing;
@Inject @Inject
public ClosedTradableManager(KeyRing keyRing, @Named("storage.dir") File storageDir) { public ClosedTradableManager(KeyRing keyRing, PriceFeed priceFeed, @Named("storage.dir") File storageDir) {
this.keyRing = keyRing; this.keyRing = keyRing;
this.closedTrades = new TradableList<>(new Storage<>(storageDir), "ClosedTrades"); this.closedTrades = new TradableList<>(new Storage<>(storageDir), "ClosedTrades");
closedTrades.forEach(e -> e.getOffer().setPriceFeed(priceFeed));
} }
public void add(Tradable tradable) { public void add(Tradable tradable) {

View file

@ -18,6 +18,7 @@
package io.bitsquare.trade.failed; package io.bitsquare.trade.failed;
import com.google.inject.Inject; import com.google.inject.Inject;
import io.bitsquare.btc.pricefeed.PriceFeed;
import io.bitsquare.common.crypto.KeyRing; import io.bitsquare.common.crypto.KeyRing;
import io.bitsquare.storage.Storage; import io.bitsquare.storage.Storage;
import io.bitsquare.trade.TradableList; import io.bitsquare.trade.TradableList;
@ -37,9 +38,10 @@ public class FailedTradesManager {
private final KeyRing keyRing; private final KeyRing keyRing;
@Inject @Inject
public FailedTradesManager(KeyRing keyRing, @Named("storage.dir") File storageDir) { public FailedTradesManager(KeyRing keyRing, PriceFeed priceFeed, @Named("storage.dir") File storageDir) {
this.keyRing = keyRing; this.keyRing = keyRing;
this.failedTrades = new TradableList<>(new Storage<>(storageDir), "FailedTrades"); this.failedTrades = new TradableList<>(new Storage<>(storageDir), "FailedTrades");
failedTrades.forEach(e -> e.getOffer().setPriceFeed(priceFeed));
} }
public void add(Trade trade) { public void add(Trade trade) {

View file

@ -19,6 +19,8 @@ package io.bitsquare.trade.offer;
import io.bitsquare.app.Version; import io.bitsquare.app.Version;
import io.bitsquare.btc.Restrictions; import io.bitsquare.btc.Restrictions;
import io.bitsquare.btc.pricefeed.MarketPrice;
import io.bitsquare.btc.pricefeed.PriceFeed;
import io.bitsquare.common.crypto.KeyRing; import io.bitsquare.common.crypto.KeyRing;
import io.bitsquare.common.crypto.PubKeyRing; import io.bitsquare.common.crypto.PubKeyRing;
import io.bitsquare.common.handlers.ResultHandler; import io.bitsquare.common.handlers.ResultHandler;
@ -106,6 +108,8 @@ public final class Offer implements StoragePayload, RequiresOwnerIsOnlinePayload
private final long date; private final long date;
private final long protocolVersion; private final long protocolVersion;
private final long fiatPrice; private final long fiatPrice;
private final double marketPriceMargin;
private final boolean usePercentageBasedPrice;
private final long amount; private final long amount;
private final long minAmount; private final long minAmount;
private final NodeAddress offererNodeAddress; private final NodeAddress offererNodeAddress;
@ -127,6 +131,7 @@ public final class Offer implements StoragePayload, RequiresOwnerIsOnlinePayload
transient private OfferAvailabilityProtocol availabilityProtocol; transient private OfferAvailabilityProtocol availabilityProtocol;
@JsonExclude @JsonExclude
transient private StringProperty errorMessageProperty = new SimpleStringProperty(); transient private StringProperty errorMessageProperty = new SimpleStringProperty();
transient private PriceFeed priceFeed;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -138,6 +143,8 @@ public final class Offer implements StoragePayload, RequiresOwnerIsOnlinePayload
PubKeyRing pubKeyRing, PubKeyRing pubKeyRing,
Direction direction, Direction direction,
long fiatPrice, long fiatPrice,
double marketPriceMargin,
boolean usePercentageBasedPrice,
long amount, long amount,
long minAmount, long minAmount,
String currencyCode, String currencyCode,
@ -147,12 +154,15 @@ public final class Offer implements StoragePayload, RequiresOwnerIsOnlinePayload
@Nullable String countryCode, @Nullable String countryCode,
@Nullable ArrayList<String> acceptedCountryCodes, @Nullable ArrayList<String> acceptedCountryCodes,
@Nullable String bankId, @Nullable String bankId,
@Nullable ArrayList<String> acceptedBankIds) { @Nullable ArrayList<String> acceptedBankIds,
PriceFeed priceFeed) {
this.id = id; this.id = id;
this.offererNodeAddress = offererNodeAddress; this.offererNodeAddress = offererNodeAddress;
this.pubKeyRing = pubKeyRing; this.pubKeyRing = pubKeyRing;
this.direction = direction; this.direction = direction;
this.fiatPrice = fiatPrice; this.fiatPrice = fiatPrice;
this.marketPriceMargin = marketPriceMargin;
this.usePercentageBasedPrice = usePercentageBasedPrice;
this.amount = amount; this.amount = amount;
this.minAmount = minAmount; this.minAmount = minAmount;
this.currencyCode = currencyCode; this.currencyCode = currencyCode;
@ -163,6 +173,7 @@ public final class Offer implements StoragePayload, RequiresOwnerIsOnlinePayload
this.acceptedCountryCodes = acceptedCountryCodes; this.acceptedCountryCodes = acceptedCountryCodes;
this.bankId = bankId; this.bankId = bankId;
this.acceptedBankIds = acceptedBankIds; this.acceptedBankIds = acceptedBankIds;
this.priceFeed = priceFeed;
protocolVersion = Version.TRADE_PROTOCOL_VERSION; protocolVersion = Version.TRADE_PROTOCOL_VERSION;
@ -218,7 +229,7 @@ public final class Offer implements StoragePayload, RequiresOwnerIsOnlinePayload
public Fiat getVolumeByAmount(Coin amount) { public Fiat getVolumeByAmount(Coin amount) {
if (fiatPrice != 0 && amount != null && !amount.isZero()) if (fiatPrice != 0 && amount != null && !amount.isZero())
return new ExchangeRate(Fiat.valueOf(currencyCode, fiatPrice)).coinToFiat(amount); return new ExchangeRate(getPrice()).coinToFiat(amount);
else else
return null; return null;
} }
@ -263,6 +274,10 @@ public final class Offer implements StoragePayload, RequiresOwnerIsOnlinePayload
// Setters // Setters
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
public void setPriceFeed(PriceFeed priceFeed) {
this.priceFeed = priceFeed;
}
public void setState(State state) { public void setState(State state) {
this.state = state; this.state = state;
stateProperty().set(state); stateProperty().set(state);
@ -312,7 +327,45 @@ public final class Offer implements StoragePayload, RequiresOwnerIsOnlinePayload
} }
public Fiat getPrice() { public Fiat getPrice() {
return Fiat.valueOf(currencyCode, fiatPrice); Fiat priceAsFiat = Fiat.valueOf(currencyCode, fiatPrice);
if (usePercentageBasedPrice && priceFeed != null) {
MarketPrice marketPrice = priceFeed.getMarketPrice(currencyCode);
if (marketPrice != null) {
PriceFeed.Type priceFeedType = direction == Direction.SELL ? PriceFeed.Type.ASK : PriceFeed.Type.BID;
double marketPriceAsDouble = marketPrice.getPrice(priceFeedType);
double factor = direction == Offer.Direction.BUY ? 1 - marketPriceMargin : 1 + marketPriceMargin;
double targetPrice = marketPriceAsDouble * factor;
// round
long factor1 = (long) Math.pow(10, 2);
targetPrice = targetPrice * factor1;
long tmp = Math.round(targetPrice);
targetPrice = (double) tmp / factor1;
try {
return Fiat.parseFiat(currencyCode, String.valueOf(targetPrice));
} catch (Exception e) {
log.warn("Exception at parseToFiat: " + e.toString());
log.warn("We use the static price.");
return priceAsFiat;
}
} else {
log.warn("We don't have a market price. We use the static price instead.");
return priceAsFiat;
}
} else {
if (priceFeed == null)
log.warn("priceFeed must not be null");
return priceAsFiat;
}
}
public double getMarketPriceMargin() {
return marketPriceMargin;
}
public boolean getUsePercentageBasedPrice() {
return usePercentageBasedPrice;
} }
public Coin getAmount() { public Coin getAmount() {
@ -391,11 +444,11 @@ public final class Offer implements StoragePayload, RequiresOwnerIsOnlinePayload
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
if (!(o instanceof Offer)) return false; if (!(o instanceof Offer)) return false;
Offer offer = (Offer) o; Offer offer = (Offer) o;
if (date != offer.date) return false; if (date != offer.date) return false;
if (fiatPrice != offer.fiatPrice) return false; if (fiatPrice != offer.fiatPrice) return false;
if (Double.compare(offer.marketPriceMargin, marketPriceMargin) != 0) return false;
if (usePercentageBasedPrice != offer.usePercentageBasedPrice) return false;
if (amount != offer.amount) return false; if (amount != offer.amount) return false;
if (minAmount != offer.minAmount) return false; if (minAmount != offer.minAmount) return false;
if (id != null ? !id.equals(offer.id) : offer.id != null) return false; if (id != null ? !id.equals(offer.id) : offer.id != null) return false;
@ -418,7 +471,6 @@ public final class Offer implements StoragePayload, RequiresOwnerIsOnlinePayload
if (arbitratorNodeAddresses != null ? !arbitratorNodeAddresses.equals(offer.arbitratorNodeAddresses) : offer.arbitratorNodeAddresses != null) if (arbitratorNodeAddresses != null ? !arbitratorNodeAddresses.equals(offer.arbitratorNodeAddresses) : offer.arbitratorNodeAddresses != null)
return false; return false;
return !(offerFeePaymentTxID != null ? !offerFeePaymentTxID.equals(offer.offerFeePaymentTxID) : offer.offerFeePaymentTxID != null); return !(offerFeePaymentTxID != null ? !offerFeePaymentTxID.equals(offer.offerFeePaymentTxID) : offer.offerFeePaymentTxID != null);
} }
@Override @Override
@ -428,6 +480,9 @@ public final class Offer implements StoragePayload, RequiresOwnerIsOnlinePayload
result = 31 * result + (currencyCode != null ? currencyCode.hashCode() : 0); result = 31 * result + (currencyCode != null ? currencyCode.hashCode() : 0);
result = 31 * result + (int) (date ^ (date >>> 32)); result = 31 * result + (int) (date ^ (date >>> 32));
result = 31 * result + (int) (fiatPrice ^ (fiatPrice >>> 32)); result = 31 * result + (int) (fiatPrice ^ (fiatPrice >>> 32));
long temp = Double.doubleToLongBits(marketPriceMargin);
result = 31 * result + (int) (temp ^ (temp >>> 32));
result = 31 * result + (usePercentageBasedPrice ? 1 : 0);
result = 31 * result + (int) (amount ^ (amount >>> 32)); result = 31 * result + (int) (amount ^ (amount >>> 32));
result = 31 * result + (int) (minAmount ^ (minAmount >>> 32)); result = 31 * result + (int) (minAmount ^ (minAmount >>> 32));
result = 31 * result + (offererNodeAddress != null ? offererNodeAddress.hashCode() : 0); result = 31 * result + (offererNodeAddress != null ? offererNodeAddress.hashCode() : 0);
@ -451,6 +506,8 @@ public final class Offer implements StoragePayload, RequiresOwnerIsOnlinePayload
"\n\tcurrencyCode='" + currencyCode + '\'' + "\n\tcurrencyCode='" + currencyCode + '\'' +
"\n\tdate=" + date + "\n\tdate=" + date +
"\n\tfiatPrice=" + fiatPrice + "\n\tfiatPrice=" + fiatPrice +
"\n\tmarketPriceMargin=" + marketPriceMargin +
"\n\tusePercentageBasedPrice=" + usePercentageBasedPrice +
"\n\tamount=" + amount + "\n\tamount=" + amount +
"\n\tminAmount=" + minAmount + "\n\tminAmount=" + minAmount +
"\n\toffererAddress=" + offererNodeAddress + "\n\toffererAddress=" + offererNodeAddress +
@ -471,5 +528,4 @@ public final class Offer implements StoragePayload, RequiresOwnerIsOnlinePayload
"\n\tTAC_TAKER=" + TAC_TAKER + "\n\tTAC_TAKER=" + TAC_TAKER +
'}'; '}';
} }
} }

View file

@ -17,6 +17,7 @@
package io.bitsquare.trade.offer; package io.bitsquare.trade.offer;
import io.bitsquare.btc.pricefeed.PriceFeed;
import io.bitsquare.common.handlers.ErrorMessageHandler; import io.bitsquare.common.handlers.ErrorMessageHandler;
import io.bitsquare.common.handlers.ResultHandler; import io.bitsquare.common.handlers.ResultHandler;
import io.bitsquare.p2p.P2PService; import io.bitsquare.p2p.P2PService;
@ -45,6 +46,7 @@ public class OfferBookService {
} }
private final P2PService p2PService; private final P2PService p2PService;
private PriceFeed priceFeed;
private final List<OfferBookChangedListener> offerBookChangedListeners = new LinkedList<>(); private final List<OfferBookChangedListener> offerBookChangedListeners = new LinkedList<>();
@ -53,15 +55,19 @@ public class OfferBookService {
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@Inject @Inject
public OfferBookService(P2PService p2PService) { public OfferBookService(P2PService p2PService, PriceFeed priceFeed) {
this.p2PService = p2PService; this.p2PService = p2PService;
this.priceFeed = priceFeed;
p2PService.addHashSetChangedListener(new HashMapChangedListener() { p2PService.addHashSetChangedListener(new HashMapChangedListener() {
@Override @Override
public void onAdded(ProtectedStorageEntry data) { public void onAdded(ProtectedStorageEntry data) {
offerBookChangedListeners.stream().forEach(listener -> { offerBookChangedListeners.stream().forEach(listener -> {
if (data.getStoragePayload() instanceof Offer) if (data.getStoragePayload() instanceof Offer) {
listener.onAdded((Offer) data.getStoragePayload()); Offer offer = (Offer) data.getStoragePayload();
offer.setPriceFeed(priceFeed);
listener.onAdded(offer);
}
}); });
} }
@ -118,7 +124,11 @@ public class OfferBookService {
public List<Offer> getOffers() { public List<Offer> getOffers() {
return p2PService.getDataMap().values().stream() return p2PService.getDataMap().values().stream()
.filter(data -> data.getStoragePayload() instanceof Offer) .filter(data -> data.getStoragePayload() instanceof Offer)
.map(data -> (Offer) data.getStoragePayload()) .map(data -> {
Offer offer = (Offer) data.getStoragePayload();
offer.setPriceFeed(priceFeed);
return offer;
})
.collect(Collectors.toList()); .collect(Collectors.toList());
} }

View file

@ -22,6 +22,7 @@ import io.bitsquare.app.Log;
import io.bitsquare.btc.AddressEntry; import io.bitsquare.btc.AddressEntry;
import io.bitsquare.btc.TradeWalletService; import io.bitsquare.btc.TradeWalletService;
import io.bitsquare.btc.WalletService; import io.bitsquare.btc.WalletService;
import io.bitsquare.btc.pricefeed.PriceFeed;
import io.bitsquare.common.Timer; import io.bitsquare.common.Timer;
import io.bitsquare.common.UserThread; import io.bitsquare.common.UserThread;
import io.bitsquare.common.crypto.KeyRing; import io.bitsquare.common.crypto.KeyRing;
@ -95,6 +96,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
TradeWalletService tradeWalletService, TradeWalletService tradeWalletService,
OfferBookService offerBookService, OfferBookService offerBookService,
ClosedTradableManager closedTradableManager, ClosedTradableManager closedTradableManager,
PriceFeed priceFeed,
@Named("storage.dir") File storageDir) { @Named("storage.dir") File storageDir) {
this.keyRing = keyRing; this.keyRing = keyRing;
this.user = user; this.user = user;
@ -105,7 +107,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
this.closedTradableManager = closedTradableManager; this.closedTradableManager = closedTradableManager;
openOffersStorage = new Storage<>(storageDir); openOffersStorage = new Storage<>(storageDir);
this.openOffers = new TradableList<>(openOffersStorage, "OpenOffers"); openOffers = new TradableList<>(openOffersStorage, "OpenOffers");
openOffers.forEach(e -> e.getOffer().setPriceFeed(priceFeed));
// In case the app did get killed the shutDown from the modules is not called, so we use a shutdown hook // In case the app did get killed the shutDown from the modules is not called, so we use a shutdown hook
Runtime.getRuntime().addShutdownHook(new Thread(OpenOfferManager.this::shutDown, Runtime.getRuntime().addShutdownHook(new Thread(OpenOfferManager.this::shutDown,

View file

@ -68,7 +68,6 @@ public class ProcessModel implements Model, Serializable {
transient private KeyRing keyRing; transient private KeyRing keyRing;
transient private P2PService p2PService; transient private P2PService p2PService;
// Mutable // Mutable
public final TradingPeer tradingPeer; public final TradingPeer tradingPeer;
transient private TradeMessage tradeMessage; transient private TradeMessage tradeMessage;

View file

@ -35,6 +35,7 @@ public final class PayDepositRequest extends TradeMessage implements MailboxMess
private static final long serialVersionUID = Version.P2P_NETWORK_VERSION; private static final long serialVersionUID = Version.P2P_NETWORK_VERSION;
public final long tradeAmount; public final long tradeAmount;
public final long tradePrice;
public final byte[] takerMultiSigPubKey; public final byte[] takerMultiSigPubKey;
public final ArrayList<RawTransactionInput> rawTransactionInputs; public final ArrayList<RawTransactionInput> rawTransactionInputs;
public final long changeOutputValue; public final long changeOutputValue;
@ -52,6 +53,7 @@ public final class PayDepositRequest extends TradeMessage implements MailboxMess
public PayDepositRequest(NodeAddress senderNodeAddress, public PayDepositRequest(NodeAddress senderNodeAddress,
String tradeId, String tradeId,
long tradeAmount, long tradeAmount,
long tradePrice,
ArrayList<RawTransactionInput> rawTransactionInputs, ArrayList<RawTransactionInput> rawTransactionInputs,
long changeOutputValue, long changeOutputValue,
String changeOutputAddress, String changeOutputAddress,
@ -66,6 +68,7 @@ public final class PayDepositRequest extends TradeMessage implements MailboxMess
super(tradeId); super(tradeId);
this.senderNodeAddress = senderNodeAddress; this.senderNodeAddress = senderNodeAddress;
this.tradeAmount = tradeAmount; this.tradeAmount = tradeAmount;
this.tradePrice = tradePrice;
this.rawTransactionInputs = rawTransactionInputs; this.rawTransactionInputs = rawTransactionInputs;
this.changeOutputValue = changeOutputValue; this.changeOutputValue = changeOutputValue;
this.changeOutputAddress = changeOutputAddress; this.changeOutputAddress = changeOutputAddress;

View file

@ -60,6 +60,7 @@ public class CreateAndSignContract extends TradeTask {
Contract contract = new Contract( Contract contract = new Contract(
processModel.getOffer(), processModel.getOffer(),
trade.getTradeAmount(), trade.getTradeAmount(),
trade.getTradePrice(),
trade.getTakeOfferFeeTxId(), trade.getTakeOfferFeeTxId(),
buyerNodeAddress, buyerNodeAddress,
sellerNodeAddress, sellerNodeAddress,

View file

@ -26,6 +26,7 @@ import io.bitsquare.trade.Trade;
import io.bitsquare.trade.protocol.trade.messages.PayDepositRequest; import io.bitsquare.trade.protocol.trade.messages.PayDepositRequest;
import io.bitsquare.trade.protocol.trade.tasks.TradeTask; import io.bitsquare.trade.protocol.trade.tasks.TradeTask;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
import org.bitcoinj.utils.Fiat;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -76,6 +77,26 @@ public class ProcessPayDepositRequest extends TradeTask {
if (payDepositRequest.acceptedArbitratorNodeAddresses.size() < 1) if (payDepositRequest.acceptedArbitratorNodeAddresses.size() < 1)
failed("acceptedArbitratorNames size must be at least 1"); failed("acceptedArbitratorNames size must be at least 1");
trade.setArbitratorNodeAddress(checkNotNull(payDepositRequest.arbitratorNodeAddress)); trade.setArbitratorNodeAddress(checkNotNull(payDepositRequest.arbitratorNodeAddress));
long takersTradePrice = payDepositRequest.tradePrice;
checkArgument(takersTradePrice > 0);
Fiat tradePriceAsFiat = Fiat.valueOf(trade.getOffer().getCurrencyCode(), takersTradePrice);
Fiat offerPriceAsFiat = trade.getOffer().getPrice();
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 offerers 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 = "Takers tradePrice is outside our market price tolerance.\n" +
"tradePriceAsFiat=" + tradePriceAsFiat.toFriendlyString() + "\n" +
"offerPriceAsFiat=" + offerPriceAsFiat.toFriendlyString();
log.warn(msg);
failed(msg);
}
trade.setTradePrice(takersTradePrice);
checkArgument(payDepositRequest.tradeAmount > 0); checkArgument(payDepositRequest.tradeAmount > 0);
trade.setTradeAmount(Coin.valueOf(payDepositRequest.tradeAmount)); trade.setTradeAmount(Coin.valueOf(payDepositRequest.tradeAmount));

View file

@ -49,6 +49,7 @@ public class SendPayDepositRequest extends TradeTask {
processModel.getMyAddress(), processModel.getMyAddress(),
processModel.getId(), processModel.getId(),
trade.getTradeAmount().value, trade.getTradeAmount().value,
trade.getTradePrice().value,
processModel.getRawTransactionInputs(), processModel.getRawTransactionInputs(),
processModel.getChangeOutputValue(), processModel.getChangeOutputValue(),
processModel.getChangeOutputAddress(), processModel.getChangeOutputAddress(),

View file

@ -61,6 +61,7 @@ public class VerifyAndSignContract extends TradeTask {
Contract contract = new Contract( Contract contract = new Contract(
processModel.getOffer(), processModel.getOffer(),
trade.getTradeAmount(), trade.getTradeAmount(),
trade.getTradePrice(),
trade.getTakeOfferFeeTxId(), trade.getTakeOfferFeeTxId(),
buyerNodeAddress, buyerNodeAddress,
sellerNodeAddress, sellerNodeAddress,

View file

@ -110,6 +110,7 @@ public final class Preferences implements Persistable {
private double maxPriceDistanceInPercent; private double maxPriceDistanceInPercent;
private boolean useInvertedMarketPrice; private boolean useInvertedMarketPrice;
private boolean useStickyMarketPrice = false; private boolean useStickyMarketPrice = false;
private boolean usePercentageBasedPrice = false;
// Observable wrappers // Observable wrappers
transient private final StringProperty btcDenominationProperty = new SimpleStringProperty(btcDenomination); transient private final StringProperty btcDenominationProperty = new SimpleStringProperty(btcDenomination);
@ -162,6 +163,7 @@ public final class Preferences implements Persistable {
// useTorForBitcoinJ = persisted.getUseTorForBitcoinJ(); // useTorForBitcoinJ = persisted.getUseTorForBitcoinJ();
useTorForBitcoinJ = false; useTorForBitcoinJ = false;
useStickyMarketPrice = persisted.getUseStickyMarketPrice(); useStickyMarketPrice = persisted.getUseStickyMarketPrice();
usePercentageBasedPrice = persisted.getUsePercentageBasedPrice();
showOwnOffersInOfferBook = persisted.getShowOwnOffersInOfferBook(); showOwnOffersInOfferBook = persisted.getShowOwnOffersInOfferBook();
maxPriceDistanceInPercent = persisted.getMaxPriceDistanceInPercent(); maxPriceDistanceInPercent = persisted.getMaxPriceDistanceInPercent();
// Backward compatible to version 0.3.6. Can be removed after a while // Backward compatible to version 0.3.6. Can be removed after a while
@ -368,6 +370,12 @@ public final class Preferences implements Persistable {
storage.queueUpForSave(); storage.queueUpForSave();
} }
public void setUsePercentageBasedPrice(boolean usePercentageBasedPrice) {
this.usePercentageBasedPrice = usePercentageBasedPrice;
storage.queueUpForSave();
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Getter // Getter
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -488,6 +496,10 @@ public final class Preferences implements Persistable {
return useStickyMarketPrice; return useStickyMarketPrice;
} }
public boolean getUsePercentageBasedPrice() {
return usePercentageBasedPrice;
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Private // Private

View file

@ -76,7 +76,7 @@ import static io.bitsquare.app.BitsquareEnvironment.APP_NAME_KEY;
public class BitsquareApp extends Application { public class BitsquareApp extends Application {
private static final Logger log = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(BitsquareApp.class); private static final Logger log = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(BitsquareApp.class);
public static final boolean DEV_MODE = false; public static final boolean DEV_MODE = true;
public static final boolean IS_RELEASE_VERSION = !DEV_MODE && true; public static final boolean IS_RELEASE_VERSION = !DEV_MODE && true;
private static Environment env; private static Environment env;

View file

@ -496,6 +496,26 @@ textfield */
-fx-border-insets: 0 0 0 -2; -fx-border-insets: 0 0 0 -2;
} }
#toggle-price-left {
-fx-border-radius: 4 0 0 4;
-fx-padding: 4 4 4 4;
-fx-border-color: #aaa;
-fx-border-style: solid none solid solid;
-fx-border-insets: 0 -2 0 0;
-fx-background-insets: 0 -2 0 0;
-fx-background-radius: 4 0 0 4;
}
#toggle-price-right {
-fx-border-radius: 0 4 4 0;
-fx-padding: 4 4 4 4;
-fx-border-color: #aaa;
-fx-border-style: solid solid solid none;
-fx-border-insets: 0 0 0 -2;
-fx-background-insets: 0 0 0 -2;
-fx-background-radius: 0 4 4 0;
}
#totals-separator { #totals-separator {
-fx-background: #AAAAAA; -fx-background: #AAAAAA;
} }

View file

@ -404,7 +404,7 @@ public class MainViewModel implements ViewModel {
result = numPeersString + " / synchronized with " + btcNetworkAsString; result = numPeersString + " / synchronized with " + btcNetworkAsString;
btcSplashSyncIconId.set("image-connection-synced"); btcSplashSyncIconId.set("image-connection-synced");
} else if (percentage > 0.0) { } else if (percentage > 0.0) {
result = numPeersString + " / synchronizing with " + btcNetworkAsString + ": " + formatter.formatToPercent(percentage); result = numPeersString + " / synchronizing with " + btcNetworkAsString + ": " + formatter.formatToPercentWithSymbol(percentage);
} else { } else {
result = numPeersString + " / connecting to " + btcNetworkAsString; result = numPeersString + " / connecting to " + btcNetworkAsString;
} }
@ -497,7 +497,11 @@ public class MainViewModel implements ViewModel {
setupBtcNumPeersWatcher(); setupBtcNumPeersWatcher();
setupP2PNumPeersWatcher(); setupP2PNumPeersWatcher();
updateBalance(); updateBalance();
if (BitsquareApp.DEV_MODE) {
preferences.setShowOwnOffersInOfferBook(true);
if (user.getPaymentAccounts().isEmpty())
setupDevDummyPaymentAccount(); setupDevDummyPaymentAccount();
}
setupMarketPriceFeed(); setupMarketPriceFeed();
swapPendingOfferFundingEntries(); swapPendingOfferFundingEntries();
fillPriceFeedComboBoxItems(); fillPriceFeedComboBoxItems();
@ -896,7 +900,6 @@ public class MainViewModel implements ViewModel {
} }
private void setupDevDummyPaymentAccount() { private void setupDevDummyPaymentAccount() {
if (BitsquareApp.DEV_MODE && user.getPaymentAccounts().isEmpty()) {
OKPayAccount okPayAccount = new OKPayAccount(); OKPayAccount okPayAccount = new OKPayAccount();
okPayAccount.setAccountNr("dummy"); okPayAccount.setAccountNr("dummy");
okPayAccount.setAccountName("OKPay dummy"); okPayAccount.setAccountName("OKPay dummy");
@ -904,4 +907,3 @@ public class MainViewModel implements ViewModel {
user.addPaymentAccount(okPayAccount); user.addPaymentAccount(okPayAccount);
} }
} }
}

View file

@ -89,6 +89,7 @@ class CreateOfferDataModel extends ActivatableDataModel {
final StringProperty btcCode = new SimpleStringProperty(); final StringProperty btcCode = new SimpleStringProperty();
final BooleanProperty isWalletFunded = new SimpleBooleanProperty(); final BooleanProperty isWalletFunded = new SimpleBooleanProperty();
final BooleanProperty usePercentageBasedPrice = new SimpleBooleanProperty();
//final BooleanProperty isMainNet = new SimpleBooleanProperty(); //final BooleanProperty isMainNet = new SimpleBooleanProperty();
//final BooleanProperty isFeeFromFundingTxSufficient = new SimpleBooleanProperty(); //final BooleanProperty isFeeFromFundingTxSufficient = new SimpleBooleanProperty();
@ -108,6 +109,7 @@ class CreateOfferDataModel extends ActivatableDataModel {
private Notification walletFundedNotification; private Notification walletFundedNotification;
boolean useSavingsWallet; boolean useSavingsWallet;
Coin totalAvailableBalance; Coin totalAvailableBalance;
private double percentageBasedPrice = 0;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -138,6 +140,8 @@ class CreateOfferDataModel extends ActivatableDataModel {
networkFeeAsCoin = FeePolicy.getFixedTxFeeForTrades(); networkFeeAsCoin = FeePolicy.getFixedTxFeeForTrades();
securityDepositAsCoin = FeePolicy.getSecurityDeposit(); securityDepositAsCoin = FeePolicy.getSecurityDeposit();
usePercentageBasedPrice.set(preferences.getUsePercentageBasedPrice());
balanceListener = new BalanceListener(getAddressEntry().getAddress()) { balanceListener = new BalanceListener(getAddressEntry().getAddress()) {
@Override @Override
public void onBalanceChanged(Coin balance, Transaction tx) { public void onBalanceChanged(Coin balance, Transaction tx) {
@ -253,6 +257,7 @@ class CreateOfferDataModel extends ActivatableDataModel {
Offer createAndGetOffer() { Offer createAndGetOffer() {
long fiatPrice = priceAsFiat.get() != null ? priceAsFiat.get().getValue() : 0L; long fiatPrice = priceAsFiat.get() != null ? priceAsFiat.get().getValue() : 0L;
long amount = amountAsCoin.get() != null ? amountAsCoin.get().getValue() : 0L; long amount = amountAsCoin.get() != null ? amountAsCoin.get().getValue() : 0L;
long minAmount = minAmountAsCoin.get() != null ? minAmountAsCoin.get().getValue() : 0L; long minAmount = minAmountAsCoin.get() != null ? minAmountAsCoin.get().getValue() : 0L;
@ -284,6 +289,8 @@ class CreateOfferDataModel extends ActivatableDataModel {
keyRing.getPubKeyRing(), keyRing.getPubKeyRing(),
direction, direction,
fiatPrice, fiatPrice,
percentageBasedPrice,
usePercentageBasedPrice.get(),
amount, amount,
minAmount, minAmount,
tradeCurrencyCode.get(), tradeCurrencyCode.get(),
@ -293,7 +300,8 @@ class CreateOfferDataModel extends ActivatableDataModel {
countryCode, countryCode,
acceptedCountryCodes, acceptedCountryCodes,
bankId, bankId,
acceptedBanks); acceptedBanks,
priceFeed);
} }
void onPlaceOffer(Offer offer, TransactionResultHandler resultHandler) { void onPlaceOffer(Offer offer, TransactionResultHandler resultHandler) {
@ -378,6 +386,11 @@ class CreateOfferDataModel extends ActivatableDataModel {
return user.getAcceptedArbitrators().size() > 0; return user.getAcceptedArbitrators().size() > 0;
} }
public void setUsePercentageBasedPrice(boolean usePercentageBasedPrice) {
this.usePercentageBasedPrice.set(usePercentageBasedPrice);
preferences.setUsePercentageBasedPrice(usePercentageBasedPrice);
}
/*boolean isFeeFromFundingTxSufficient() { /*boolean isFeeFromFundingTxSufficient() {
return !isMainNet.get() || feeFromFundingTxProperty.get().compareTo(FeePolicy.getMinRequiredFeeForFundingTx()) >= 0; return !isMainNet.get() || feeFromFundingTxProperty.get().compareTo(FeePolicy.getMinRequiredFeeForFundingTx()) >= 0;
}*/ }*/
@ -484,4 +497,12 @@ class CreateOfferDataModel extends ActivatableDataModel {
walletService.swapTradeEntryToAvailableEntry(offerId, AddressEntry.Context.OFFER_FUNDING); walletService.swapTradeEntryToAvailableEntry(offerId, AddressEntry.Context.OFFER_FUNDING);
walletService.swapTradeEntryToAvailableEntry(offerId, AddressEntry.Context.RESERVED_FOR_TRADE); walletService.swapTradeEntryToAvailableEntry(offerId, AddressEntry.Context.RESERVED_FOR_TRADE);
} }
double getPercentageBasedPrice() {
return percentageBasedPrice;
}
void setPercentageBasedPrice(double percentageBasedPrice) {
this.percentageBasedPrice = percentageBasedPrice;
}
} }

View file

@ -52,6 +52,7 @@ import javafx.collections.FXCollections;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
import javafx.event.EventHandler; import javafx.event.EventHandler;
import javafx.geometry.*; import javafx.geometry.*;
import javafx.scene.Node;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
@ -70,6 +71,8 @@ import org.jetbrains.annotations.NotNull;
import javax.inject.Inject; import javax.inject.Inject;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.net.URI; import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static io.bitsquare.gui.util.FormBuilder.*; import static io.bitsquare.gui.util.FormBuilder.*;
@ -88,22 +91,23 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
private BalanceTextField balanceTextField; private BalanceTextField balanceTextField;
private TitledGroupBg payFundsPane; private TitledGroupBg payFundsPane;
private ProgressIndicator spinner; private ProgressIndicator spinner;
private Button nextButton, cancelButton1, cancelButton2, fundFromSavingsWalletButton, fundFromExternalWalletButton, placeOfferButton; private Button nextButton, cancelButton1, cancelButton2, fundFromSavingsWalletButton, fundFromExternalWalletButton, placeOfferButton, usePercentageBasedPriceButton;
private InputTextField amountTextField, minAmountTextField, priceTextField, volumeTextField; private InputTextField amountTextField, minAmountTextField, priceTextField, priceAsPercentageTextField, volumeTextField;
private TextField currencyTextField; private TextField currencyTextField;
private Label directionLabel, amountDescriptionLabel, addressLabel, balanceLabel, totalToPayLabel, totalToPayInfoIconLabel, amountBtcLabel, priceCurrencyLabel, private Label directionLabel, amountDescriptionLabel, addressLabel, balanceLabel, totalToPayLabel, totalToPayInfoIconLabel, amountBtcLabel, priceCurrencyLabel,
volumeCurrencyLabel, minAmountBtcLabel, priceDescriptionLabel, volumeDescriptionLabel, currencyTextFieldLabel, volumeCurrencyLabel, minAmountBtcLabel, priceDescriptionLabel, volumeDescriptionLabel, currencyTextFieldLabel,
currencyComboBoxLabel, spinnerInfoLabel; currencyComboBoxLabel, spinnerInfoLabel, priceAsPercentageLabel;
private TextFieldWithCopyIcon totalToPayTextField; private TextFieldWithCopyIcon totalToPayTextField;
private ComboBox<PaymentAccount> paymentAccountsComboBox; private ComboBox<PaymentAccount> paymentAccountsComboBox;
private ComboBox<TradeCurrency> currencyComboBox; private ComboBox<TradeCurrency> currencyComboBox;
private PopOver totalToPayInfoPopover; private PopOver totalToPayInfoPopover;
private ToggleButton fixedPriceButton, percentagePriceButton;
private OfferView.CloseHandler closeHandler; private OfferView.CloseHandler closeHandler;
private ChangeListener<Boolean> amountFocusedListener; private ChangeListener<Boolean> amountFocusedListener;
private ChangeListener<Boolean> minAmountFocusedListener; private ChangeListener<Boolean> minAmountFocusedListener;
private ChangeListener<Boolean> priceFocusedListener; private ChangeListener<Boolean> priceFocusedListener, priceAsPercentageFocusedListener;
private ChangeListener<Boolean> volumeFocusedListener; private ChangeListener<Boolean> volumeFocusedListener;
private ChangeListener<Boolean> showWarningInvalidBtcDecimalPlacesListener; private ChangeListener<Boolean> showWarningInvalidBtcDecimalPlacesListener;
private ChangeListener<Boolean> showWarningInvalidFiatDecimalPlacesPlacesListener; private ChangeListener<Boolean> showWarningInvalidFiatDecimalPlacesPlacesListener;
@ -122,6 +126,7 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
private Subscription isSpinnerVisibleSubscription; private Subscription isSpinnerVisibleSubscription;
private Subscription cancelButton2StyleSubscription; private Subscription cancelButton2StyleSubscription;
private Subscription balanceSubscription; private Subscription balanceSubscription;
private List<Node> editOfferElements = new ArrayList<>();
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -175,6 +180,9 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
if (spinner != null && spinner.isVisible()) if (spinner != null && spinner.isVisible())
spinner.setProgress(-1); spinner.setProgress(-1);
percentagePriceButton.setSelected(model.dataModel.usePercentageBasedPrice.get());
fixedPriceButton.setSelected(!model.dataModel.usePercentageBasedPrice.get());
directionLabel.setText(model.getDirectionLabel()); directionLabel.setText(model.getDirectionLabel());
amountDescriptionLabel.setText(model.getAmountDescription()); amountDescriptionLabel.setText(model.getAmountDescription());
addressTextField.setAddress(model.getAddressAsString()); addressTextField.setAddress(model.getAddressAsString());
@ -277,12 +285,10 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
private void onShowPayFundsScreen() { private void onShowPayFundsScreen() {
model.onShowPayFundsScreen(); model.onShowPayFundsScreen();
amountTextField.setMouseTransparent(true); editOfferElements.stream().forEach(node -> {
minAmountTextField.setMouseTransparent(true); node.setMouseTransparent(true);
priceTextField.setMouseTransparent(true); node.setFocusTraversable(false);
volumeTextField.setMouseTransparent(true); });
currencyComboBox.setMouseTransparent(true);
paymentAccountsComboBox.setMouseTransparent(true);
balanceTextField.setTargetAmount(model.dataModel.totalToPayAsCoin.get()); balanceTextField.setTargetAmount(model.dataModel.totalToPayAsCoin.get());
@ -407,6 +413,11 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
private void addBindings() { private void addBindings() {
amountBtcLabel.textProperty().bind(model.btcCode); amountBtcLabel.textProperty().bind(model.btcCode);
priceCurrencyLabel.textProperty().bind(createStringBinding(() -> model.tradeCurrencyCode.get() + "/" + model.btcCode.get(), model.btcCode, model.tradeCurrencyCode)); priceCurrencyLabel.textProperty().bind(createStringBinding(() -> model.tradeCurrencyCode.get() + "/" + model.btcCode.get(), model.btcCode, model.tradeCurrencyCode));
priceTextField.disableProperty().bind(model.dataModel.usePercentageBasedPrice);
priceCurrencyLabel.disableProperty().bind(model.dataModel.usePercentageBasedPrice);
priceAsPercentageTextField.disableProperty().bind(model.dataModel.usePercentageBasedPrice.not());
priceAsPercentageLabel.disableProperty().bind(model.dataModel.usePercentageBasedPrice.not());
priceAsPercentageLabel.prefWidthProperty().bind(priceCurrencyLabel.widthProperty());
volumeCurrencyLabel.textProperty().bind(model.tradeCurrencyCode); volumeCurrencyLabel.textProperty().bind(model.tradeCurrencyCode);
minAmountBtcLabel.textProperty().bind(model.btcCode); minAmountBtcLabel.textProperty().bind(model.btcCode);
priceDescriptionLabel.textProperty().bind(createStringBinding(() -> BSResources.get("createOffer.amountPriceBox.priceDescription", model.tradeCurrencyCode.get()), model.tradeCurrencyCode)); priceDescriptionLabel.textProperty().bind(createStringBinding(() -> BSResources.get("createOffer.amountPriceBox.priceDescription", model.tradeCurrencyCode.get()), model.tradeCurrencyCode));
@ -414,6 +425,7 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
amountTextField.textProperty().bindBidirectional(model.amount); amountTextField.textProperty().bindBidirectional(model.amount);
minAmountTextField.textProperty().bindBidirectional(model.minAmount); minAmountTextField.textProperty().bindBidirectional(model.minAmount);
priceTextField.textProperty().bindBidirectional(model.price); priceTextField.textProperty().bindBidirectional(model.price);
priceAsPercentageTextField.textProperty().bindBidirectional(model.priceAsPercentage);
volumeTextField.textProperty().bindBidirectional(model.volume); volumeTextField.textProperty().bindBidirectional(model.volume);
volumeTextField.promptTextProperty().bind(model.volumePromptLabel); volumeTextField.promptTextProperty().bind(model.volumePromptLabel);
totalToPayTextField.textProperty().bind(model.totalToPay); totalToPayTextField.textProperty().bind(model.totalToPay);
@ -452,6 +464,10 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
private void removeBindings() { private void removeBindings() {
amountBtcLabel.textProperty().unbind(); amountBtcLabel.textProperty().unbind();
priceCurrencyLabel.textProperty().unbind(); priceCurrencyLabel.textProperty().unbind();
priceTextField.disableProperty().unbind();
priceCurrencyLabel.disableProperty().unbind();
priceAsPercentageTextField.disableProperty().unbind();
priceAsPercentageLabel.disableProperty().unbind();
volumeCurrencyLabel.textProperty().unbind(); volumeCurrencyLabel.textProperty().unbind();
minAmountBtcLabel.textProperty().unbind(); minAmountBtcLabel.textProperty().unbind();
priceDescriptionLabel.textProperty().unbind(); priceDescriptionLabel.textProperty().unbind();
@ -459,6 +475,8 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
amountTextField.textProperty().unbindBidirectional(model.amount); amountTextField.textProperty().unbindBidirectional(model.amount);
minAmountTextField.textProperty().unbindBidirectional(model.minAmount); minAmountTextField.textProperty().unbindBidirectional(model.minAmount);
priceTextField.textProperty().unbindBidirectional(model.price); priceTextField.textProperty().unbindBidirectional(model.price);
priceAsPercentageTextField.textProperty().unbindBidirectional(model.priceAsPercentage);
priceAsPercentageLabel.prefWidthProperty().unbind();
volumeTextField.textProperty().unbindBidirectional(model.volume); volumeTextField.textProperty().unbindBidirectional(model.volume);
volumeTextField.promptTextProperty().unbindBidirectional(model.volume); volumeTextField.promptTextProperty().unbindBidirectional(model.volume);
totalToPayTextField.textProperty().unbind(); totalToPayTextField.textProperty().unbind();
@ -524,6 +542,10 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
model.onFocusOutPriceTextField(oldValue, newValue, priceTextField.getText()); model.onFocusOutPriceTextField(oldValue, newValue, priceTextField.getText());
priceTextField.setText(model.price.get()); priceTextField.setText(model.price.get());
}; };
priceAsPercentageFocusedListener = (o, oldValue, newValue) -> {
model.onFocusOutPriceAsPercentageTextField(oldValue, newValue, priceAsPercentageTextField.getText());
priceAsPercentageTextField.setText(model.priceAsPercentage.get());
};
volumeFocusedListener = (o, oldValue, newValue) -> { volumeFocusedListener = (o, oldValue, newValue) -> {
model.onFocusOutVolumeTextField(oldValue, newValue, volumeTextField.getText()); model.onFocusOutVolumeTextField(oldValue, newValue, volumeTextField.getText());
volumeTextField.setText(model.volume.get()); volumeTextField.setText(model.volume.get());
@ -583,6 +605,7 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
tradeCurrencyCodeListener = (observable, oldValue, newValue) -> { tradeCurrencyCodeListener = (observable, oldValue, newValue) -> {
priceTextField.clear(); priceTextField.clear();
priceAsPercentageTextField.clear();
volumeTextField.clear(); volumeTextField.clear();
}; };
@ -621,6 +644,7 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
amountTextField.focusedProperty().addListener(amountFocusedListener); amountTextField.focusedProperty().addListener(amountFocusedListener);
minAmountTextField.focusedProperty().addListener(minAmountFocusedListener); minAmountTextField.focusedProperty().addListener(minAmountFocusedListener);
priceTextField.focusedProperty().addListener(priceFocusedListener); priceTextField.focusedProperty().addListener(priceFocusedListener);
priceAsPercentageTextField.focusedProperty().addListener(priceAsPercentageFocusedListener);
volumeTextField.focusedProperty().addListener(volumeFocusedListener); volumeTextField.focusedProperty().addListener(volumeFocusedListener);
// warnings // warnings
@ -644,6 +668,7 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
amountTextField.focusedProperty().removeListener(amountFocusedListener); amountTextField.focusedProperty().removeListener(amountFocusedListener);
minAmountTextField.focusedProperty().removeListener(minAmountFocusedListener); minAmountTextField.focusedProperty().removeListener(minAmountFocusedListener);
priceTextField.focusedProperty().removeListener(priceFocusedListener); priceTextField.focusedProperty().removeListener(priceFocusedListener);
priceAsPercentageTextField.focusedProperty().removeListener(priceAsPercentageFocusedListener);
volumeTextField.focusedProperty().removeListener(volumeFocusedListener); volumeTextField.focusedProperty().removeListener(volumeFocusedListener);
// warnings // warnings
@ -702,11 +727,14 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
paymentAccountsComboBox = addLabelComboBox(gridPane, gridRow, "Payment account:", Layout.FIRST_ROW_DISTANCE).second; paymentAccountsComboBox = addLabelComboBox(gridPane, gridRow, "Payment account:", Layout.FIRST_ROW_DISTANCE).second;
paymentAccountsComboBox.setPromptText("Select payment account"); paymentAccountsComboBox.setPromptText("Select payment account");
editOfferElements.add(paymentAccountsComboBox);
// we display either currencyComboBox (multi currency account) or currencyTextField (single) // we display either currencyComboBox (multi currency account) or currencyTextField (single)
Tuple2<Label, ComboBox> currencyComboBoxTuple = addLabelComboBox(gridPane, ++gridRow, "Currency:"); Tuple2<Label, ComboBox> currencyComboBoxTuple = addLabelComboBox(gridPane, ++gridRow, "Currency:");
currencyComboBoxLabel = currencyComboBoxTuple.first; currencyComboBoxLabel = currencyComboBoxTuple.first;
editOfferElements.add(currencyComboBoxLabel);
currencyComboBox = currencyComboBoxTuple.second; currencyComboBox = currencyComboBoxTuple.second;
editOfferElements.add(currencyComboBox);
currencyComboBox.setPromptText("Select currency"); currencyComboBox.setPromptText("Select currency");
currencyComboBox.setConverter(new StringConverter<TradeCurrency>() { currencyComboBox.setConverter(new StringConverter<TradeCurrency>() {
@Override @Override
@ -722,7 +750,9 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
Tuple2<Label, TextField> currencyTextFieldTuple = addLabelTextField(gridPane, gridRow, "Currency:", "", 5); Tuple2<Label, TextField> currencyTextFieldTuple = addLabelTextField(gridPane, gridRow, "Currency:", "", 5);
currencyTextFieldLabel = currencyTextFieldTuple.first; currencyTextFieldLabel = currencyTextFieldTuple.first;
editOfferElements.add(currencyTextFieldLabel);
currencyTextField = currencyTextFieldTuple.second; currencyTextField = currencyTextFieldTuple.second;
editOfferElements.add(currencyTextField);
} }
private void addAmountPriceGroup() { private void addAmountPriceGroup() {
@ -745,14 +775,15 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
gridPane.getChildren().add(imageVBox); gridPane.getChildren().add(imageVBox);
addAmountPriceFields(); addAmountPriceFields();
addSecondRow();
addMinAmountBox();
Tuple2<Button, Button> tuple = add2ButtonsAfterGroup(gridPane, ++gridRow, BSResources.get("createOffer.amountPriceBox.next"), BSResources.get("shared.cancel")); Tuple2<Button, Button> tuple = add2ButtonsAfterGroup(gridPane, ++gridRow, BSResources.get("createOffer.amountPriceBox.next"), BSResources.get("shared.cancel"));
nextButton = tuple.first; nextButton = tuple.first;
editOfferElements.add(nextButton);
nextButton.disableProperty().bind(model.isNextButtonDisabled); nextButton.disableProperty().bind(model.isNextButtonDisabled);
//UserThread.runAfter(() -> nextButton.requestFocus(), 100, TimeUnit.MILLISECONDS); //UserThread.runAfter(() -> nextButton.requestFocus(), 100, TimeUnit.MILLISECONDS);
cancelButton1 = tuple.second; cancelButton1 = tuple.second;
editOfferElements.add(cancelButton1);
cancelButton1.setDefaultButton(false); cancelButton1.setDefaultButton(false);
cancelButton1.setOnAction(e -> { cancelButton1.setOnAction(e -> {
close(); close();
@ -887,9 +918,12 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
Tuple3<HBox, InputTextField, Label> amountValueCurrencyBoxTuple = FormBuilder.getValueCurrencyBox(BSResources.get("createOffer.amount.prompt")); Tuple3<HBox, InputTextField, Label> amountValueCurrencyBoxTuple = FormBuilder.getValueCurrencyBox(BSResources.get("createOffer.amount.prompt"));
HBox amountValueCurrencyBox = amountValueCurrencyBoxTuple.first; HBox amountValueCurrencyBox = amountValueCurrencyBoxTuple.first;
amountTextField = amountValueCurrencyBoxTuple.second; amountTextField = amountValueCurrencyBoxTuple.second;
editOfferElements.add(amountTextField);
amountBtcLabel = amountValueCurrencyBoxTuple.third; amountBtcLabel = amountValueCurrencyBoxTuple.third;
editOfferElements.add(amountBtcLabel);
Tuple2<Label, VBox> amountInputBoxTuple = getTradeInputBox(amountValueCurrencyBox, model.getAmountDescription()); Tuple2<Label, VBox> amountInputBoxTuple = getTradeInputBox(amountValueCurrencyBox, model.getAmountDescription());
amountDescriptionLabel = amountInputBoxTuple.first; amountDescriptionLabel = amountInputBoxTuple.first;
editOfferElements.add(amountDescriptionLabel);
VBox amountBox = amountInputBoxTuple.second; VBox amountBox = amountInputBoxTuple.second;
// x // x
@ -897,15 +931,42 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
xLabel.setFont(Font.font("Helvetica-Bold", 20)); xLabel.setFont(Font.font("Helvetica-Bold", 20));
xLabel.setPadding(new Insets(14, 3, 0, 3)); xLabel.setPadding(new Insets(14, 3, 0, 3));
// price // price as fiat
Tuple3<HBox, InputTextField, Label> priceValueCurrencyBoxTuple = FormBuilder.getValueCurrencyBox(BSResources.get("createOffer.price.prompt")); Tuple3<HBox, InputTextField, Label> priceValueCurrencyBoxTuple = FormBuilder.getValueCurrencyBox(BSResources.get("createOffer.price.prompt"));
HBox priceValueCurrencyBox = priceValueCurrencyBoxTuple.first; HBox priceValueCurrencyBox = priceValueCurrencyBoxTuple.first;
priceTextField = priceValueCurrencyBoxTuple.second; priceTextField = priceValueCurrencyBoxTuple.second;
editOfferElements.add(priceTextField);
priceCurrencyLabel = priceValueCurrencyBoxTuple.third; priceCurrencyLabel = priceValueCurrencyBoxTuple.third;
editOfferElements.add(priceCurrencyLabel);
Tuple2<Label, VBox> priceInputBoxTuple = getTradeInputBox(priceValueCurrencyBox, BSResources.get("createOffer.amountPriceBox.priceDescription")); Tuple2<Label, VBox> priceInputBoxTuple = getTradeInputBox(priceValueCurrencyBox, BSResources.get("createOffer.amountPriceBox.priceDescription"));
priceDescriptionLabel = priceInputBoxTuple.first; priceDescriptionLabel = priceInputBoxTuple.first;
editOfferElements.add(priceDescriptionLabel);
VBox priceBox = priceInputBoxTuple.second; VBox priceBox = priceInputBoxTuple.second;
// Fixed/Percentage toggle
ToggleGroup toggleGroup = new ToggleGroup();
fixedPriceButton = new ToggleButton("Fixed");
editOfferElements.add(fixedPriceButton);
fixedPriceButton.setId("toggle-price-left");
fixedPriceButton.setToggleGroup(toggleGroup);
fixedPriceButton.selectedProperty().addListener((ov, oldValue, newValue) -> {
model.dataModel.setUsePercentageBasedPrice(!newValue);
percentagePriceButton.setSelected(!newValue);
});
percentagePriceButton = new ToggleButton("Percentage");
editOfferElements.add(percentagePriceButton);
percentagePriceButton.setId("toggle-price-right");
percentagePriceButton.setToggleGroup(toggleGroup);
percentagePriceButton.selectedProperty().addListener((ov, oldValue, newValue) -> {
model.dataModel.setUsePercentageBasedPrice(newValue);
fixedPriceButton.setSelected(!newValue);
});
HBox toggleButtons = new HBox();
toggleButtons.setPadding(new Insets(18, 0, 0, 0));
toggleButtons.getChildren().addAll(fixedPriceButton, percentagePriceButton);
// = // =
Label resultLabel = new Label("="); Label resultLabel = new Label("=");
resultLabel.setFont(Font.font("Helvetica-Bold", 20)); resultLabel.setFont(Font.font("Helvetica-Bold", 20));
@ -915,15 +976,18 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
Tuple3<HBox, InputTextField, Label> volumeValueCurrencyBoxTuple = FormBuilder.getValueCurrencyBox(BSResources.get("createOffer.volume.prompt")); Tuple3<HBox, InputTextField, Label> volumeValueCurrencyBoxTuple = FormBuilder.getValueCurrencyBox(BSResources.get("createOffer.volume.prompt"));
HBox volumeValueCurrencyBox = volumeValueCurrencyBoxTuple.first; HBox volumeValueCurrencyBox = volumeValueCurrencyBoxTuple.first;
volumeTextField = volumeValueCurrencyBoxTuple.second; volumeTextField = volumeValueCurrencyBoxTuple.second;
editOfferElements.add(volumeTextField);
volumeCurrencyLabel = volumeValueCurrencyBoxTuple.third; volumeCurrencyLabel = volumeValueCurrencyBoxTuple.third;
editOfferElements.add(volumeCurrencyLabel);
Tuple2<Label, VBox> volumeInputBoxTuple = getTradeInputBox(volumeValueCurrencyBox, model.volumeDescriptionLabel.get()); Tuple2<Label, VBox> volumeInputBoxTuple = getTradeInputBox(volumeValueCurrencyBox, model.volumeDescriptionLabel.get());
volumeDescriptionLabel = volumeInputBoxTuple.first; volumeDescriptionLabel = volumeInputBoxTuple.first;
editOfferElements.add(volumeDescriptionLabel);
VBox volumeBox = volumeInputBoxTuple.second; VBox volumeBox = volumeInputBoxTuple.second;
HBox hBox = new HBox(); HBox hBox = new HBox();
hBox.setSpacing(5); hBox.setSpacing(5);
hBox.setAlignment(Pos.CENTER_LEFT); hBox.setAlignment(Pos.CENTER_LEFT);
hBox.getChildren().addAll(amountBox, xLabel, priceBox, resultLabel, volumeBox); hBox.getChildren().addAll(amountBox, xLabel, priceBox, toggleButtons, resultLabel, volumeBox);
GridPane.setRowIndex(hBox, gridRow); GridPane.setRowIndex(hBox, gridRow);
GridPane.setColumnIndex(hBox, 1); GridPane.setColumnIndex(hBox, 1);
GridPane.setMargin(hBox, new Insets(Layout.FIRST_ROW_AND_GROUP_DISTANCE, 10, 0, 0)); GridPane.setMargin(hBox, new Insets(Layout.FIRST_ROW_AND_GROUP_DISTANCE, 10, 0, 0));
@ -931,19 +995,46 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
gridPane.getChildren().add(hBox); gridPane.getChildren().add(hBox);
} }
private void addMinAmountBox() { private void addSecondRow() {
Tuple3<HBox, InputTextField, Label> priceAsPercentageTuple = FormBuilder.getValueCurrencyBox(BSResources.get("createOffer.price.prompt"));
HBox priceAsPercentageValueCurrencyBox = priceAsPercentageTuple.first;
priceAsPercentageTextField = priceAsPercentageTuple.second;
editOfferElements.add(priceAsPercentageTextField);
priceAsPercentageLabel = priceAsPercentageTuple.third;
editOfferElements.add(priceAsPercentageLabel);
Tuple2<Label, VBox> priceAsPercentageInputBoxTuple = getTradeInputBox(priceAsPercentageValueCurrencyBox, "Distance in % from market price");
priceAsPercentageInputBoxTuple.first.setPrefWidth(200);
VBox priceAsPercentageInputBox = priceAsPercentageInputBoxTuple.second;
priceAsPercentageTextField.setPromptText("Enter % value");
priceAsPercentageLabel.setText("%");
priceAsPercentageLabel.setStyle("-fx-alignment: center;");
Tuple3<HBox, InputTextField, Label> amountValueCurrencyBoxTuple = getValueCurrencyBox(BSResources.get("createOffer.amount.prompt")); Tuple3<HBox, InputTextField, Label> amountValueCurrencyBoxTuple = getValueCurrencyBox(BSResources.get("createOffer.amount.prompt"));
HBox amountValueCurrencyBox = amountValueCurrencyBoxTuple.first; HBox amountValueCurrencyBox = amountValueCurrencyBoxTuple.first;
minAmountTextField = amountValueCurrencyBoxTuple.second; minAmountTextField = amountValueCurrencyBoxTuple.second;
editOfferElements.add(minAmountTextField);
minAmountBtcLabel = amountValueCurrencyBoxTuple.third; minAmountBtcLabel = amountValueCurrencyBoxTuple.third;
editOfferElements.add(minAmountBtcLabel);
Tuple2<Label, VBox> amountInputBoxTuple = getTradeInputBox(amountValueCurrencyBox, BSResources.get("createOffer.amountPriceBox" + Tuple2<Label, VBox> amountInputBoxTuple = getTradeInputBox(amountValueCurrencyBox, BSResources.get("createOffer.amountPriceBox" +
".minAmountDescription")); ".minAmountDescription"));
VBox box = amountInputBoxTuple.second;
GridPane.setRowIndex(box, ++gridRow); Label xLabel = new Label("x");
GridPane.setColumnIndex(box, 1); xLabel.setFont(Font.font("Helvetica-Bold", 20));
GridPane.setMargin(box, new Insets(5, 10, 5, 0)); xLabel.setPadding(new Insets(14, 3, 0, 3));
gridPane.getChildren().add(box); xLabel.setVisible(false); // we just use it to get the same layout as the upper row
HBox hBox = new HBox();
hBox.setSpacing(5);
hBox.setAlignment(Pos.CENTER_LEFT);
hBox.getChildren().addAll(amountInputBoxTuple.second, xLabel, priceAsPercentageInputBox);
GridPane.setRowIndex(hBox, ++gridRow);
GridPane.setColumnIndex(hBox, 1);
GridPane.setMargin(hBox, new Insets(5, 10, 5, 0));
GridPane.setColumnSpan(hBox, 2);
gridPane.getChildren().add(hBox);
} }

View file

@ -71,6 +71,7 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
final StringProperty amount = new SimpleStringProperty(); final StringProperty amount = new SimpleStringProperty();
final StringProperty minAmount = new SimpleStringProperty(); final StringProperty minAmount = new SimpleStringProperty();
final StringProperty price = new SimpleStringProperty(); final StringProperty price = new SimpleStringProperty();
final StringProperty priceAsPercentage = new SimpleStringProperty();
final StringProperty volume = new SimpleStringProperty(); final StringProperty volume = new SimpleStringProperty();
final StringProperty volumeDescriptionLabel = new SimpleStringProperty(); final StringProperty volumeDescriptionLabel = new SimpleStringProperty();
final StringProperty volumePromptLabel = new SimpleStringProperty(); final StringProperty volumePromptLabel = new SimpleStringProperty();
@ -103,7 +104,7 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
private ChangeListener<String> amountListener; private ChangeListener<String> amountListener;
private ChangeListener<String> minAmountListener; private ChangeListener<String> minAmountListener;
private ChangeListener<String> priceListener; private ChangeListener<String> priceListener, priceAsPercentageListener;
private ChangeListener<String> volumeListener; private ChangeListener<String> volumeListener;
private ChangeListener<Coin> amountAsCoinListener; private ChangeListener<Coin> amountAsCoinListener;
private ChangeListener<Coin> minAmountAsCoinListener; private ChangeListener<Coin> minAmountAsCoinListener;
@ -114,6 +115,9 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
private ChangeListener<String> errorMessageListener; private ChangeListener<String> errorMessageListener;
private Offer offer; private Offer offer;
private Timer timeoutTimer; private Timer timeoutTimer;
private PriceFeed.Type priceFeedType;
private boolean priceAsPercentageIsInput;
private ChangeListener<Boolean> usePercentageBasedPriceListener;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -235,9 +239,70 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
setPriceToModel(); setPriceToModel();
calculateVolume(); calculateVolume();
dataModel.calculateTotalToPay(); dataModel.calculateTotalToPay();
if (!priceAsPercentageIsInput) {
MarketPrice marketPrice = priceFeed.getMarketPrice(dataModel.tradeCurrencyCode.get());
if (marketPrice != null) {
double marketPriceAsDouble = marketPrice.getPrice(priceFeedType);
try {
double priceAsDouble = formatter.parseNumberStringToDouble(price.get());
double priceFactor = priceAsDouble / marketPriceAsDouble;
priceFactor = dataModel.getDirection() == Offer.Direction.BUY ? 1 - priceFactor : 1 + priceFactor;
priceAsPercentage.set(formatter.formatToPercent(priceFactor, 2));
} catch (NumberFormatException t) {
priceAsPercentage.set("");
new Popup().warning("Your input is not a valid number.")
.show();
}
}
}
} }
updateButtonDisableState(); updateButtonDisableState();
}; };
priceAsPercentageListener = (ov, oldValue, newValue) -> {
if (priceAsPercentageIsInput) {
try {
if (!newValue.isEmpty() && !newValue.equals("-")) {
double percentageBasedPrice = formatter.parsePercentStringToDouble(newValue);
if (percentageBasedPrice >= 1 || percentageBasedPrice <= -1) {
dataModel.setPercentageBasedPrice(0);
UserThread.execute(() -> priceAsPercentage.set("0"));
new Popup().warning("You cannot set a percentage of 100% or larger. Please enter a percentage number like \"5.4\" for 5.4%")
.show();
} else {
MarketPrice marketPrice = priceFeed.getMarketPrice(dataModel.tradeCurrencyCode.get());
if (marketPrice != null) {
percentageBasedPrice = formatter.roundDouble(percentageBasedPrice, 4);
dataModel.setPercentageBasedPrice(percentageBasedPrice);
double marketPriceAsDouble = marketPrice.getPrice(priceFeedType);
double factor = dataModel.getDirection() == Offer.Direction.BUY ? 1 - percentageBasedPrice : 1 + percentageBasedPrice;
double targetPrice = marketPriceAsDouble * factor;
price.set(formatter.formatToNumberString(targetPrice, 2));
setPriceToModel();
calculateVolume();
dataModel.calculateTotalToPay();
updateButtonDisableState();
} else {
new Popup().warning("There is no price feed available for that currency. You cannot use percent based price.")
.show();
}
}
} else {
dataModel.setPercentageBasedPrice(0);
}
} catch (Throwable t) {
dataModel.setPercentageBasedPrice(0);
UserThread.execute(() -> priceAsPercentage.set("0"));
new Popup().warning("Your input is not a valid number. Please enter a percentage number like \"5.4\" for 5.4%")
.show();
}
}
};
usePercentageBasedPriceListener = (observable, oldValue, newValue) -> {
if (newValue)
priceValidationResult.set(new InputValidator.ValidationResult(true));
};
volumeListener = (ov, oldValue, newValue) -> { volumeListener = (ov, oldValue, newValue) -> {
if (isFiatInputValid(newValue).isValid) { if (isFiatInputValid(newValue).isValid) {
setVolumeToModel(); setVolumeToModel();
@ -266,6 +331,8 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
amount.addListener(amountListener); amount.addListener(amountListener);
minAmount.addListener(minAmountListener); minAmount.addListener(minAmountListener);
price.addListener(priceListener); price.addListener(priceListener);
priceAsPercentage.addListener(priceAsPercentageListener);
dataModel.usePercentageBasedPrice.addListener(usePercentageBasedPriceListener);
volume.addListener(volumeListener); volume.addListener(volumeListener);
// Binding with Bindings.createObjectBinding does not work because of bi-directional binding // Binding with Bindings.createObjectBinding does not work because of bi-directional binding
@ -282,6 +349,8 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
amount.removeListener(amountListener); amount.removeListener(amountListener);
minAmount.removeListener(minAmountListener); minAmount.removeListener(minAmountListener);
price.removeListener(priceListener); price.removeListener(priceListener);
priceAsPercentage.removeListener(priceAsPercentageListener);
dataModel.usePercentageBasedPrice.removeListener(usePercentageBasedPriceListener);
volume.removeListener(volumeListener); volume.removeListener(volumeListener);
// Binding with Bindings.createObjectBinding does not work because of bi-directional binding // Binding with Bindings.createObjectBinding does not work because of bi-directional binding
@ -307,6 +376,8 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
if (dataModel.paymentAccount != null) if (dataModel.paymentAccount != null)
btcValidator.setMaxTradeLimitInBitcoin(dataModel.paymentAccount.getPaymentMethod().getMaxTradeLimit()); btcValidator.setMaxTradeLimitInBitcoin(dataModel.paymentAccount.getPaymentMethod().getMaxTradeLimit());
priceFeedType = direction == Offer.Direction.SELL ? PriceFeed.Type.ASK : PriceFeed.Type.BID;
return result; return result;
} }
@ -417,8 +488,9 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
// handle minAmount/amount relationship // handle minAmount/amount relationship
if (!dataModel.isMinAmountLessOrEqualAmount()) { if (!dataModel.isMinAmountLessOrEqualAmount()) {
amountValidationResult.set(new InputValidator.ValidationResult(false, minAmount.set(amount.get());
BSResources.get("createOffer.validation.amountSmallerThanMinAmount"))); /*amountValidationResult.set(new InputValidator.ValidationResult(false,
BSResources.get("createOffer.validation.amountSmallerThanMinAmount")));*/
} else { } else {
amountValidationResult.set(result); amountValidationResult.set(result);
if (minAmount.get() != null) if (minAmount.get() != null)
@ -438,8 +510,9 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
minAmount.set(formatter.formatCoin(dataModel.minAmountAsCoin.get())); minAmount.set(formatter.formatCoin(dataModel.minAmountAsCoin.get()));
if (!dataModel.isMinAmountLessOrEqualAmount()) { if (!dataModel.isMinAmountLessOrEqualAmount()) {
minAmountValidationResult.set(new InputValidator.ValidationResult(false, amount.set(minAmount.get());
BSResources.get("createOffer.validation.minAmountLargerThanAmount"))); /* minAmountValidationResult.set(new InputValidator.ValidationResult(false,
BSResources.get("createOffer.validation.minAmountLargerThanAmount")));*/
} else { } else {
minAmountValidationResult.set(result); minAmountValidationResult.set(result);
if (amount.get() != null) if (amount.get() != null)
@ -464,6 +537,12 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
} }
} }
void onFocusOutPriceAsPercentageTextField(boolean oldValue, boolean newValue, String userInput) {
priceAsPercentageIsInput = !oldValue && newValue;
if (oldValue && !newValue)
priceAsPercentage.set(formatter.formatToNumberString(dataModel.getPercentageBasedPrice() * 100, 2));
}
void onFocusOutVolumeTextField(boolean oldValue, boolean newValue, String userInput) { void onFocusOutVolumeTextField(boolean oldValue, boolean newValue, String userInput) {
if (oldValue && !newValue) { if (oldValue && !newValue) {
InputValidator.ValidationResult result = isFiatInputValid(volume.get()); InputValidator.ValidationResult result = isFiatInputValid(volume.get());
@ -492,21 +571,14 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
public boolean isPriceInRange() { public boolean isPriceInRange() {
MarketPrice marketPrice = priceFeed.getMarketPrice(getTradeCurrency().getCode()); MarketPrice marketPrice = priceFeed.getMarketPrice(getTradeCurrency().getCode());
if (marketPrice != null) { if (marketPrice != null) {
double marketPriceAsDouble = marketPrice.getPrice(PriceFeed.Type.LAST); double marketPriceAsDouble = marketPrice.getPrice(priceFeedType);
Fiat priceAsFiat = dataModel.priceAsFiat.get(); Fiat priceAsFiat = dataModel.priceAsFiat.get();
long shiftDivisor = checkedPow(10, priceAsFiat.smallestUnitExponent()); long shiftDivisor = checkedPow(10, priceAsFiat.smallestUnitExponent());
double offerPrice = ((double) priceAsFiat.longValue()) / ((double) shiftDivisor); double offerPrice = ((double) priceAsFiat.longValue()) / ((double) shiftDivisor);
if (marketPriceAsDouble != 0 && Math.abs(1 - (offerPrice / marketPriceAsDouble)) > preferences.getMaxPriceDistanceInPercent()) { double percentage = Math.abs(1 - (offerPrice / marketPriceAsDouble));
Popup popup = new Popup(); percentage = formatter.roundDouble(percentage, 2);
popup.warning("The price you have entered is outside the max. allowed deviation from the market price.\n" + if (marketPriceAsDouble != 0 && percentage > preferences.getMaxPriceDistanceInPercent()) {
"The max. allowed deviation is " + displayPriceOutOfRangePopup();
formatter.formatToPercent(preferences.getMaxPriceDistanceInPercent()) +
" and can be adjusted in the preferences.")
.actionButtonText("Change price")
.onAction(() -> popup.hide())
.closeButtonText("Go to \"Preferences\"")
.onClose(() -> navigation.navigateTo(MainView.class, SettingsView.class, PreferencesView.class))
.show();
return false; return false;
} else { } else {
return true; return true;
@ -516,6 +588,19 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
} }
} }
private void displayPriceOutOfRangePopup() {
Popup popup = new Popup();
popup.warning("The price you have entered is outside the max. allowed deviation from the market price.\n" +
"The max. allowed deviation is " +
formatter.formatToPercentWithSymbol(preferences.getMaxPriceDistanceInPercent()) +
" and can be adjusted in the preferences.")
.actionButtonText("Change price")
.onAction(() -> popup.hide())
.closeButtonText("Go to \"Preferences\"")
.onClose(() -> navigation.navigateTo(MainView.class, SettingsView.class, PreferencesView.class))
.show();
}
BSFormatter getFormatter() { BSFormatter getFormatter() {
return formatter; return formatter;
} }

View file

@ -257,10 +257,19 @@ class OfferBookViewModel extends ActivatableViewModel {
} }
String getPrice(OfferBookListItem item) { String getPrice(OfferBookListItem item) {
if ((item == null))
return "";
Offer offer = item.getOffer();
Fiat price = offer.getPrice();
String postFix = "";
if (offer.getUsePercentageBasedPrice()) {
postFix = " (" + formatter.formatToPercentWithSymbol(offer.getMarketPriceMargin()) + ")";
}
if (showAllTradeCurrenciesProperty.get()) if (showAllTradeCurrenciesProperty.get())
return (item != null) ? formatter.formatFiatWithCode(item.getOffer().getPrice()) : ""; return formatter.formatPriceWithCode(price) + postFix;
else else
return (item != null) ? formatter.formatFiat(item.getOffer().getPrice()) : ""; return formatter.formatFiat(price) + postFix;
} }
String getVolume(OfferBookListItem item) { String getVolume(OfferBookListItem item) {

View file

@ -92,6 +92,7 @@ class TakeOfferDataModel extends ActivatableDataModel {
boolean useSavingsWallet; boolean useSavingsWallet;
Coin totalAvailableBalance; Coin totalAvailableBalance;
private Notification walletFundedNotification; private Notification walletFundedNotification;
Fiat tradePrice;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -158,6 +159,7 @@ class TakeOfferDataModel extends ActivatableDataModel {
// called before activate // called before activate
void initWithData(Offer offer) { void initWithData(Offer offer) {
this.offer = offer; this.offer = offer;
tradePrice = offer.getPrice();
addressEntry = walletService.getOrCreateAddressEntry(offer.getId(), AddressEntry.Context.OFFER_FUNDING); addressEntry = walletService.getOrCreateAddressEntry(offer.getId(), AddressEntry.Context.OFFER_FUNDING);
checkNotNull(addressEntry, "addressEntry must not be null"); checkNotNull(addressEntry, "addressEntry must not be null");
@ -227,6 +229,7 @@ class TakeOfferDataModel extends ActivatableDataModel {
// have it persisted as well. // have it persisted as well.
void onTakeOffer(TradeResultHandler tradeResultHandler) { void onTakeOffer(TradeResultHandler tradeResultHandler) {
tradeManager.onTakeOffer(amountAsCoin.get(), tradeManager.onTakeOffer(amountAsCoin.get(),
tradePrice.getValue(),
totalToPayAsCoin.get().subtract(takerFeeAsCoin), totalToPayAsCoin.get().subtract(takerFeeAsCoin),
offer, offer,
paymentAccount.getId(), paymentAccount.getId(),
@ -308,7 +311,7 @@ class TakeOfferDataModel extends ActivatableDataModel {
if (offer != null && if (offer != null &&
amountAsCoin.get() != null && amountAsCoin.get() != null &&
!amountAsCoin.get().isZero()) { !amountAsCoin.get().isZero()) {
volumeAsFiat.set(new ExchangeRate(offer.getPrice()).coinToFiat(amountAsCoin.get())); volumeAsFiat.set(new ExchangeRate(tradePrice).coinToFiat(amountAsCoin.get()));
updateBalance(); updateBalance();
} }

View file

@ -90,9 +90,9 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
private TitledGroupBg payFundsPane; private TitledGroupBg payFundsPane;
private Button nextButton, cancelButton1, cancelButton2, fundFromSavingsWalletButton, fundFromExternalWalletButton, takeOfferButton; private Button nextButton, cancelButton1, cancelButton2, fundFromSavingsWalletButton, fundFromExternalWalletButton, takeOfferButton;
private InputTextField amountTextField; private InputTextField amountTextField;
private TextField paymentMethodTextField, currencyTextField, priceTextField, volumeTextField, amountRangeTextField; private TextField paymentMethodTextField, currencyTextField, priceTextField, priceAsPercentageTextField, volumeTextField, amountRangeTextField;
private Label directionLabel, amountDescriptionLabel, addressLabel, balanceLabel, totalToPayLabel, totalToPayInfoIconLabel, private Label directionLabel, amountDescriptionLabel, addressLabel, balanceLabel, totalToPayLabel, totalToPayInfoIconLabel,
amountBtcLabel, priceCurrencyLabel, amountBtcLabel, priceCurrencyLabel, priceAsPercentageLabel,
volumeCurrencyLabel, amountRangeBtcLabel, priceDescriptionLabel, volumeDescriptionLabel, spinnerInfoLabel, offerAvailabilitySpinnerLabel; volumeCurrencyLabel, amountRangeBtcLabel, priceDescriptionLabel, volumeDescriptionLabel, spinnerInfoLabel, offerAvailabilitySpinnerLabel;
private TextFieldWithCopyIcon totalToPayTextField; private TextFieldWithCopyIcon totalToPayTextField;
private PopOver totalToPayInfoPopover; private PopOver totalToPayInfoPopover;
@ -116,6 +116,7 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
// private Subscription noSufficientFeeSubscription; // private Subscription noSufficientFeeSubscription;
// private MonadicBinding<Boolean> noSufficientFeeBinding; // private MonadicBinding<Boolean> noSufficientFeeBinding;
private Subscription cancelButton2StyleSubscription; private Subscription cancelButton2StyleSubscription;
private VBox priceAsPercentageInputBox;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -195,6 +196,8 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
public void initWithData(Offer offer) { public void initWithData(Offer offer) {
model.initWithData(offer); model.initWithData(offer);
priceAsPercentageInputBox.setVisible(offer.getUsePercentageBasedPrice());
if (model.getOffer().getDirection() == Offer.Direction.SELL) { if (model.getOffer().getDirection() == Offer.Direction.SELL) {
imageView.setId("image-buy-large"); imageView.setId("image-buy-large");
directionLabel.setId("direction-icon-label-buy"); directionLabel.setId("direction-icon-label-buy");
@ -211,7 +214,6 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
takeOfferButton.setText("Review offer for selling bitcoin"); takeOfferButton.setText("Review offer for selling bitcoin");
} }
boolean showComboBox = model.getPossiblePaymentAccounts().size() > 1; boolean showComboBox = model.getPossiblePaymentAccounts().size() > 1;
paymentAccountsLabel.setVisible(showComboBox); paymentAccountsLabel.setVisible(showComboBox);
paymentAccountsLabel.setManaged(showComboBox); paymentAccountsLabel.setManaged(showComboBox);
@ -228,6 +230,7 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
amountDescriptionLabel.setText(model.getAmountDescription()); amountDescriptionLabel.setText(model.getAmountDescription());
amountRangeTextField.setText(model.getAmountRange()); amountRangeTextField.setText(model.getAmountRange());
priceTextField.setText(model.getPrice()); priceTextField.setText(model.getPrice());
priceAsPercentageTextField.setText(model.marketPriceMargin);
addressTextField.setPaymentLabel(model.getPaymentLabel()); addressTextField.setPaymentLabel(model.getPaymentLabel());
addressTextField.setAddress(model.dataModel.getAddressEntry().getAddressString()); addressTextField.setAddress(model.dataModel.getAddressEntry().getAddressString());
} }
@ -269,7 +272,7 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
offerDetailsWindow.hide(); offerDetailsWindow.hide();
offerDetailsWindowDisplayed = false; offerDetailsWindowDisplayed = false;
}) })
).show(model.getOffer(), model.dataModel.amountAsCoin.get()); ).show(model.getOffer(), model.dataModel.amountAsCoin.get(), model.dataModel.tradePrice);
offerDetailsWindowDisplayed = true; offerDetailsWindowDisplayed = true;
} else { } else {
new Popup().warning("You have no arbitrator selected.\n" + new Popup().warning("You have no arbitrator selected.\n" +
@ -284,7 +287,9 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
model.onShowPayFundsScreen(); model.onShowPayFundsScreen();
amountTextField.setMouseTransparent(true); amountTextField.setMouseTransparent(true);
amountTextField.setFocusTraversable(false);
priceTextField.setMouseTransparent(true); priceTextField.setMouseTransparent(true);
priceAsPercentageTextField.setMouseTransparent(true);
volumeTextField.setMouseTransparent(true); volumeTextField.setMouseTransparent(true);
balanceTextField.setTargetAmount(model.dataModel.totalToPayAsCoin.get()); balanceTextField.setTargetAmount(model.dataModel.totalToPayAsCoin.get());
@ -389,6 +394,7 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
addressTextField.amountAsCoinProperty().bind(model.dataModel.missingCoin); addressTextField.amountAsCoinProperty().bind(model.dataModel.missingCoin);
amountTextField.validationResultProperty().bind(model.amountValidationResult); amountTextField.validationResultProperty().bind(model.amountValidationResult);
priceCurrencyLabel.textProperty().bind(createStringBinding(() -> model.dataModel.getCurrencyCode() + "/" + model.btcCode.get(), model.btcCode)); priceCurrencyLabel.textProperty().bind(createStringBinding(() -> model.dataModel.getCurrencyCode() + "/" + model.btcCode.get(), model.btcCode));
priceAsPercentageLabel.prefWidthProperty().bind(priceCurrencyLabel.widthProperty());
amountRangeBtcLabel.textProperty().bind(model.btcCode); amountRangeBtcLabel.textProperty().bind(model.btcCode);
nextButton.disableProperty().bind(model.isNextButtonDisabled); nextButton.disableProperty().bind(model.isNextButtonDisabled);
@ -414,6 +420,7 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
addressTextField.amountAsCoinProperty().unbind(); addressTextField.amountAsCoinProperty().unbind();
amountTextField.validationResultProperty().unbind(); amountTextField.validationResultProperty().unbind();
priceCurrencyLabel.textProperty().unbind(); priceCurrencyLabel.textProperty().unbind();
priceAsPercentageLabel.prefWidthProperty().unbind();
amountRangeBtcLabel.textProperty().unbind(); amountRangeBtcLabel.textProperty().unbind();
nextButton.disableProperty().unbind(); nextButton.disableProperty().unbind();
@ -638,8 +645,7 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
gridPane.getChildren().add(imageVBox); gridPane.getChildren().add(imageVBox);
addAmountPriceFields(); addAmountPriceFields();
addSecondRow();
addAmountRangeBox();
HBox hBox = new HBox(); HBox hBox = new HBox();
hBox.setSpacing(10); hBox.setSpacing(10);
@ -839,18 +845,43 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
gridPane.getChildren().add(hBox); gridPane.getChildren().add(hBox);
} }
private void addAmountRangeBox() { private void addSecondRow() {
Tuple3<HBox, TextField, Label> priceAsPercentageTuple = getValueCurrencyBox();
HBox priceAsPercentageValueCurrencyBox = priceAsPercentageTuple.first;
priceAsPercentageTextField = priceAsPercentageTuple.second;
priceAsPercentageLabel = priceAsPercentageTuple.third;
Tuple2<Label, VBox> priceAsPercentageInputBoxTuple = getTradeInputBox(priceAsPercentageValueCurrencyBox, "Distance in % from market price");
priceAsPercentageInputBoxTuple.first.setPrefWidth(200);
priceAsPercentageInputBox = priceAsPercentageInputBoxTuple.second;
priceAsPercentageTextField.setPromptText("Enter % value");
priceAsPercentageLabel.setText("%");
priceAsPercentageLabel.setStyle("-fx-alignment: center;");
Tuple3<HBox, TextField, Label> amountValueCurrencyBoxTuple = getValueCurrencyBox(); Tuple3<HBox, TextField, Label> amountValueCurrencyBoxTuple = getValueCurrencyBox();
HBox amountValueCurrencyBox = amountValueCurrencyBoxTuple.first; HBox amountValueCurrencyBox = amountValueCurrencyBoxTuple.first;
amountRangeTextField = amountValueCurrencyBoxTuple.second; amountRangeTextField = amountValueCurrencyBoxTuple.second;
amountRangeBtcLabel = amountValueCurrencyBoxTuple.third; amountRangeBtcLabel = amountValueCurrencyBoxTuple.third;
Tuple2<Label, VBox> amountInputBoxTuple = getTradeInputBox(amountValueCurrencyBox, BSResources.get("takeOffer.amountPriceBox.amountRangeDescription")); Tuple2<Label, VBox> amountInputBoxTuple = getTradeInputBox(amountValueCurrencyBox, BSResources.get("takeOffer.amountPriceBox.amountRangeDescription"));
VBox box = amountInputBoxTuple.second;
GridPane.setRowIndex(box, ++gridRow); Label xLabel = new Label("x");
GridPane.setColumnIndex(box, 1); xLabel.setFont(Font.font("Helvetica-Bold", 20));
GridPane.setMargin(box, new Insets(5, 10, 5, 0)); xLabel.setPadding(new Insets(14, 3, 0, 3));
gridPane.getChildren().add(box); xLabel.setVisible(false); // we just use it to get the same layout as the upper row
HBox hBox = new HBox();
hBox.setSpacing(5);
hBox.setAlignment(Pos.CENTER_LEFT);
hBox.getChildren().addAll(amountInputBoxTuple.second, xLabel, priceAsPercentageInputBox);
GridPane.setRowIndex(hBox, ++gridRow);
GridPane.setColumnIndex(hBox, 1);
GridPane.setMargin(hBox, new Insets(5, 10, 5, 0));
GridPane.setColumnSpan(hBox, 2);
gridPane.getChildren().add(hBox);
} }

View file

@ -18,6 +18,7 @@
package io.bitsquare.gui.main.offer.takeoffer; package io.bitsquare.gui.main.offer.takeoffer;
import io.bitsquare.arbitration.Arbitrator; import io.bitsquare.arbitration.Arbitrator;
import io.bitsquare.btc.pricefeed.PriceFeed;
import io.bitsquare.gui.Navigation; import io.bitsquare.gui.Navigation;
import io.bitsquare.gui.common.model.ActivatableWithDataModel; import io.bitsquare.gui.common.model.ActivatableWithDataModel;
import io.bitsquare.gui.common.model.ViewModel; import io.bitsquare.gui.common.model.ViewModel;
@ -52,6 +53,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
final TakeOfferDataModel dataModel; final TakeOfferDataModel dataModel;
private final BtcValidator btcValidator; private final BtcValidator btcValidator;
private final P2PService p2PService; private final P2PService p2PService;
private PriceFeed priceFeed;
private final Navigation navigation; private final Navigation navigation;
final BSFormatter formatter; final BSFormatter formatter;
@ -94,6 +96,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
private ConnectionListener connectionListener; private ConnectionListener connectionListener;
// private Subscription isFeeSufficientSubscription; // private Subscription isFeeSufficientSubscription;
private Runnable takeOfferSucceededHandler; private Runnable takeOfferSucceededHandler;
String marketPriceMargin;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -101,13 +104,14 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@Inject @Inject
public TakeOfferViewModel(TakeOfferDataModel dataModel, BtcValidator btcValidator, P2PService p2PService, public TakeOfferViewModel(TakeOfferDataModel dataModel, BtcValidator btcValidator, P2PService p2PService, PriceFeed priceFeed,
Navigation navigation, BSFormatter formatter) { Navigation navigation, BSFormatter formatter) {
super(dataModel); super(dataModel);
this.dataModel = dataModel; this.dataModel = dataModel;
this.btcValidator = btcValidator; this.btcValidator = btcValidator;
this.p2PService = p2PService; this.p2PService = p2PService;
this.priceFeed = priceFeed;
this.navigation = navigation; this.navigation = navigation;
this.formatter = formatter; this.formatter = formatter;
@ -159,7 +163,8 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
} }
amountRange = formatter.formatCoin(offer.getMinAmount()) + " - " + formatter.formatCoin(offer.getAmount()); amountRange = formatter.formatCoin(offer.getMinAmount()) + " - " + formatter.formatCoin(offer.getAmount());
price = formatter.formatFiat(offer.getPrice()); price = formatter.formatFiat(dataModel.tradePrice);
marketPriceMargin = formatter.formatToPercentWithSymbol(offer.getMarketPriceMargin());
paymentLabel = BSResources.get("takeOffer.fundsBox.paymentLabel", offer.getId()); paymentLabel = BSResources.get("takeOffer.fundsBox.paymentLabel", offer.getId());
checkNotNull(dataModel.getAddressEntry(), "dataModel.getAddressEntry() must not be null"); checkNotNull(dataModel.getAddressEntry(), "dataModel.getAddressEntry() must not be null");

View file

@ -123,7 +123,7 @@ public class ContractWindow extends Overlay<ContractWindow> {
addLabelTextField(gridPane, ++rowIndex, "Offer date:", formatter.formatDateTime(offer.getDate())); addLabelTextField(gridPane, ++rowIndex, "Offer date:", formatter.formatDateTime(offer.getDate()));
addLabelTextField(gridPane, ++rowIndex, "Trade date:", formatter.formatDateTime(dispute.getTradeDate())); addLabelTextField(gridPane, ++rowIndex, "Trade date:", formatter.formatDateTime(dispute.getTradeDate()));
addLabelTextField(gridPane, ++rowIndex, "Trade type:", formatter.getDirectionBothSides(offer.getDirection())); addLabelTextField(gridPane, ++rowIndex, "Trade type:", formatter.getDirectionBothSides(offer.getDirection()));
addLabelTextField(gridPane, ++rowIndex, "Price:", formatter.formatFiat(offer.getPrice()) + " " + offer.getCurrencyCode()); addLabelTextField(gridPane, ++rowIndex, "Trade price:", formatter.formatFiat(contract.getTradePrice()) + " " + offer.getCurrencyCode());
addLabelTextField(gridPane, ++rowIndex, "Trade amount:", formatter.formatCoinWithCode(contract.getTradeAmount())); addLabelTextField(gridPane, ++rowIndex, "Trade amount:", formatter.formatCoinWithCode(contract.getTradeAmount()));
addLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, "Buyer bitcoin address:", addLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, "Buyer bitcoin address:",
contract.getBuyerPayoutAddressString()).second.setMouseTransparent(false); contract.getBuyerPayoutAddressString()).second.setMouseTransparent(false);

View file

@ -47,6 +47,7 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import org.bitcoinj.core.AddressFormatException; import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
import org.bitcoinj.utils.ExchangeRate;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -232,8 +233,8 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
} }
addLabelTextField(gridPane, ++rowIndex, "Traders role:", role); addLabelTextField(gridPane, ++rowIndex, "Traders role:", role);
addLabelTextField(gridPane, ++rowIndex, "Trade amount:", formatter.formatCoinWithCode(contract.getTradeAmount())); addLabelTextField(gridPane, ++rowIndex, "Trade amount:", formatter.formatCoinWithCode(contract.getTradeAmount()));
addLabelTextField(gridPane, ++rowIndex, "Trade volume:", formatter.formatFiatWithCode(contract.offer.getVolumeByAmount(contract.getTradeAmount()))); addLabelTextField(gridPane, ++rowIndex, "Trade price:", formatter.formatFiatWithCode(contract.getTradePrice()));
addLabelTextField(gridPane, ++rowIndex, "Price:", formatter.formatFiatWithCode(contract.offer.getPrice())); addLabelTextField(gridPane, ++rowIndex, "Trade volume:", formatter.formatFiatWithCode(new ExchangeRate(contract.getTradePrice()).coinToFiat(contract.getTradeAmount())));
} }
private void addCheckboxes() { private void addCheckboxes() {

View file

@ -41,6 +41,7 @@ import javafx.geometry.Insets;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
import org.bitcoinj.utils.Fiat;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -61,6 +62,7 @@ public class OfferDetailsWindow extends Overlay<OfferDetailsWindow> {
private final Navigation navigation; private final Navigation navigation;
private Offer offer; private Offer offer;
private Coin tradeAmount; private Coin tradeAmount;
private Fiat tradePrice;
private Optional<Runnable> placeOfferHandlerOptional = Optional.empty(); private Optional<Runnable> placeOfferHandlerOptional = Optional.empty();
private Optional<Runnable> takeOfferHandlerOptional = Optional.empty(); private Optional<Runnable> takeOfferHandlerOptional = Optional.empty();
private ProgressIndicator spinner; private ProgressIndicator spinner;
@ -80,9 +82,10 @@ public class OfferDetailsWindow extends Overlay<OfferDetailsWindow> {
type = Type.Confirmation; type = Type.Confirmation;
} }
public void show(Offer offer, Coin tradeAmount) { public void show(Offer offer, Coin tradeAmount, Fiat tradePrice) {
this.offer = offer; this.offer = offer;
this.tradeAmount = tradeAmount; this.tradeAmount = tradeAmount;
this.tradePrice = tradePrice;
rowIndex = -1; rowIndex = -1;
width = 900; width = 900;
@ -172,6 +175,9 @@ public class OfferDetailsWindow extends Overlay<OfferDetailsWindow> {
addLabelTextField(gridPane, ++rowIndex, CurrencyUtil.getNameByCode(offer.getCurrencyCode()) + " amount" + fiatDirectionInfo, formatter.formatFiatWithCode(offer.getVolumeByAmount(offer.getAmount()))); addLabelTextField(gridPane, ++rowIndex, CurrencyUtil.getNameByCode(offer.getCurrencyCode()) + " amount" + fiatDirectionInfo, formatter.formatFiatWithCode(offer.getVolumeByAmount(offer.getAmount())));
} }
if (takeOfferHandlerOptional.isPresent())
addLabelTextField(gridPane, ++rowIndex, "Price:", formatter.formatFiat(tradePrice) + " " + offer.getCurrencyCode() + "/" + "BTC");
else
addLabelTextField(gridPane, ++rowIndex, "Price:", formatter.formatFiat(offer.getPrice()) + " " + offer.getCurrencyCode() + "/" + "BTC"); addLabelTextField(gridPane, ++rowIndex, "Price:", formatter.formatFiat(offer.getPrice()) + " " + offer.getCurrencyCode() + "/" + "BTC");
if (offer.isMyOffer(keyRing) && user.getPaymentAccount(offer.getOffererPaymentAccountId()) != null) if (offer.isMyOffer(keyRing) && user.getPaymentAccount(offer.getOffererPaymentAccountId()) != null)

View file

@ -127,7 +127,7 @@ public class TradeDetailsWindow extends Overlay<TradeDetailsWindow> {
addLabelTextField(gridPane, ++rowIndex, "Bitcoin amount" + btcDirectionInfo, formatter.formatCoinWithCode(trade.getTradeAmount())); addLabelTextField(gridPane, ++rowIndex, "Bitcoin amount" + btcDirectionInfo, formatter.formatCoinWithCode(trade.getTradeAmount()));
addLabelTextField(gridPane, ++rowIndex, CurrencyUtil.getNameByCode(offer.getCurrencyCode()) + " amount" + fiatDirectionInfo, formatter.formatFiatWithCode(trade.getTradeVolume())); addLabelTextField(gridPane, ++rowIndex, CurrencyUtil.getNameByCode(offer.getCurrencyCode()) + " amount" + fiatDirectionInfo, formatter.formatFiatWithCode(trade.getTradeVolume()));
addLabelTextField(gridPane, ++rowIndex, "Price:", formatter.formatPriceWithCode(offer.getPrice())); addLabelTextField(gridPane, ++rowIndex, "Trade price:", formatter.formatPriceWithCode(trade.getTradePrice()));
addLabelTextField(gridPane, ++rowIndex, "Payment method:", BSResources.get(offer.getPaymentMethod().getId())); addLabelTextField(gridPane, ++rowIndex, "Payment method:", BSResources.get(offer.getPaymentMethod().getId()));
// second group // second group

View file

@ -81,7 +81,13 @@ public class ClosedTradesView extends ActivatableViewAndModel<VBox, ClosedTrades
tradeIdColumn.setComparator((o1, o2) -> o1.getTradable().getId().compareTo(o2.getTradable().getId())); tradeIdColumn.setComparator((o1, o2) -> o1.getTradable().getId().compareTo(o2.getTradable().getId()));
dateColumn.setComparator((o1, o2) -> o1.getTradable().getDate().compareTo(o2.getTradable().getDate())); dateColumn.setComparator((o1, o2) -> o1.getTradable().getDate().compareTo(o2.getTradable().getDate()));
directionColumn.setComparator((o1, o2) -> o1.getTradable().getOffer().getDirection().compareTo(o2.getTradable().getOffer().getDirection())); directionColumn.setComparator((o1, o2) -> o1.getTradable().getOffer().getDirection().compareTo(o2.getTradable().getOffer().getDirection()));
priceColumn.setComparator((o1, o2) -> o1.getTradable().getOffer().getPrice().compareTo(o2.getTradable().getOffer().getPrice())); priceColumn.setComparator((o1, o2) -> {
Tradable tradable = o1.getTradable();
if (tradable instanceof Trade)
return ((Trade) o1.getTradable()).getTradePrice().compareTo(((Trade) o2.getTradable()).getTradePrice());
else
return o1.getTradable().getOffer().getPrice().compareTo(o2.getTradable().getOffer().getPrice());
});
volumeColumn.setComparator((o1, o2) -> { volumeColumn.setComparator((o1, o2) -> {
if (o1.getTradable() instanceof Trade && o2.getTradable() instanceof Trade) { if (o1.getTradable() instanceof Trade && o2.getTradable() instanceof Trade) {
Fiat tradeVolume1 = ((Trade) o1.getTradable()).getTradeVolume(); Fiat tradeVolume1 = ((Trade) o1.getTradable()).getTradeVolume();

View file

@ -21,6 +21,7 @@ import com.google.inject.Inject;
import io.bitsquare.gui.common.model.ActivatableWithDataModel; import io.bitsquare.gui.common.model.ActivatableWithDataModel;
import io.bitsquare.gui.common.model.ViewModel; import io.bitsquare.gui.common.model.ViewModel;
import io.bitsquare.gui.util.BSFormatter; import io.bitsquare.gui.util.BSFormatter;
import io.bitsquare.trade.Tradable;
import io.bitsquare.trade.Trade; import io.bitsquare.trade.Trade;
import io.bitsquare.trade.offer.OpenOffer; import io.bitsquare.trade.offer.OpenOffer;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
@ -54,7 +55,13 @@ class ClosedTradesViewModel extends ActivatableWithDataModel<ClosedTradesDataMod
} }
String getPrice(ClosedTradableListItem item) { String getPrice(ClosedTradableListItem item) {
return (item != null) ? formatter.formatFiat(item.getTradable().getOffer().getPrice()) : ""; if (item == null)
return "";
Tradable tradable = item.getTradable();
if (tradable instanceof Trade)
return formatter.formatFiat(((Trade) tradable).getTradePrice());
else
return formatter.formatFiat(tradable.getOffer().getPrice());
} }
String getVolume(ClosedTradableListItem item) { String getVolume(ClosedTradableListItem item) {

View file

@ -62,7 +62,7 @@ public class FailedTradesView extends ActivatableViewAndModel<VBox, FailedTrades
tradeIdColumn.setComparator((o1, o2) -> o1.getTrade().getId().compareTo(o2.getTrade().getId())); tradeIdColumn.setComparator((o1, o2) -> o1.getTrade().getId().compareTo(o2.getTrade().getId()));
dateColumn.setComparator((o1, o2) -> o1.getTrade().getDate().compareTo(o2.getTrade().getDate())); dateColumn.setComparator((o1, o2) -> o1.getTrade().getDate().compareTo(o2.getTrade().getDate()));
priceColumn.setComparator((o1, o2) -> o1.getTrade().getOffer().getPrice().compareTo(o2.getTrade().getOffer().getPrice())); priceColumn.setComparator((o1, o2) -> o1.getTrade().getTradePrice().compareTo(o2.getTrade().getTradePrice()));
volumeColumn.setComparator((o1, o2) -> o1.getTrade().getTradeVolume().compareTo(o2.getTrade().getTradeVolume())); volumeColumn.setComparator((o1, o2) -> o1.getTrade().getTradeVolume().compareTo(o2.getTrade().getTradeVolume()));
amountColumn.setComparator((o1, o2) -> o1.getTrade().getTradeAmount().compareTo(o2.getTrade().getTradeAmount())); amountColumn.setComparator((o1, o2) -> o1.getTrade().getTradeAmount().compareTo(o2.getTrade().getTradeAmount()));
stateColumn.setComparator((o1, o2) -> model.getState(o1).compareTo(model.getState(o2))); stateColumn.setComparator((o1, o2) -> model.getState(o1).compareTo(model.getState(o2)));

View file

@ -51,7 +51,7 @@ class FailedTradesViewModel extends ActivatableWithDataModel<FailedTradesDataMod
} }
String getPrice(FailedTradesListItem item) { String getPrice(FailedTradesListItem item) {
return (item != null) ? formatter.formatFiat(item.getTrade().getOffer().getPrice()) : ""; return (item != null) ? formatter.formatFiat(item.getTrade().getTradePrice()) : "";
} }
String getVolume(FailedTradesListItem item) { String getVolume(FailedTradesListItem item) {

View file

@ -46,7 +46,7 @@ public class PendingTradesListItem {
} }
public Fiat getPrice() { public Fiat getPrice() {
return trade.getOffer().getPrice(); return trade.getTradePrice();
} }
} }

View file

@ -336,7 +336,7 @@ public class PendingTradesView extends ActivatableViewAndModel<VBox, PendingTrad
public void updateItem(final PendingTradesListItem item, boolean empty) { public void updateItem(final PendingTradesListItem item, boolean empty) {
super.updateItem(item, empty); super.updateItem(item, empty);
if (item != null && !empty) if (item != null && !empty)
setText(formatter.formatCoinWithCode(item.getTrade().getPayoutAmount())); setText(formatter.formatCoinWithCode(item.getTrade().getTradeAmount()));
else else
setText(null); setText(null);
} }
@ -380,7 +380,7 @@ public class PendingTradesView extends ActivatableViewAndModel<VBox, PendingTrad
public void updateItem(final PendingTradesListItem item, boolean empty) { public void updateItem(final PendingTradesListItem item, boolean empty) {
super.updateItem(item, empty); super.updateItem(item, empty);
if (item != null && !empty) if (item != null && !empty)
setText(formatter.formatPriceWithCode(item.getTrade().getTradeVolume())); setText(formatter.formatFiatWithCode(item.getTrade().getTradeVolume()));
else else
setText(null); setText(null);
} }

View file

@ -296,19 +296,16 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Activatab
deviationListener = (observable, oldValue, newValue) -> { deviationListener = (observable, oldValue, newValue) -> {
try { try {
String input = newValue.replace("%", ""); double value = formatter.parsePercentStringToDouble(newValue);
input = input.replace(",", "."); preferences.setMaxPriceDistanceInPercent(value);
input = input.replace(" ", ""); } catch (NumberFormatException t) {
double value = Double.parseDouble(input);
preferences.setMaxPriceDistanceInPercent(value / 100);
} catch (Throwable t) {
log.error("Exception at parseDouble deviation: " + t.toString()); log.error("Exception at parseDouble deviation: " + t.toString());
UserThread.runAfter(() -> deviationInputTextField.setText(formatter.formatToPercent(preferences.getMaxPriceDistanceInPercent())), 100, TimeUnit.MILLISECONDS); UserThread.runAfter(() -> deviationInputTextField.setText(formatter.formatToPercentWithSymbol(preferences.getMaxPriceDistanceInPercent())), 100, TimeUnit.MILLISECONDS);
} }
}; };
deviationFocusedListener = (observable1, oldValue1, newValue1) -> { deviationFocusedListener = (observable1, oldValue1, newValue1) -> {
if (oldValue1 && !newValue1) if (oldValue1 && !newValue1)
UserThread.runAfter(() -> deviationInputTextField.setText(formatter.formatToPercent(preferences.getMaxPriceDistanceInPercent())), 100, TimeUnit.MILLISECONDS); UserThread.runAfter(() -> deviationInputTextField.setText(formatter.formatToPercentWithSymbol(preferences.getMaxPriceDistanceInPercent())), 100, TimeUnit.MILLISECONDS);
}; };
transactionFeeInputTextField = addLabelInputTextField(root, ++gridRow, "Withdrawal transaction fee (satoshi/byte):").second; transactionFeeInputTextField = addLabelInputTextField(root, ++gridRow, "Withdrawal transaction fee (satoshi/byte):").second;
@ -427,7 +424,7 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Activatab
}); });
blockChainExplorerComboBox.setOnAction(e -> preferences.setBlockChainExplorer(blockChainExplorerComboBox.getSelectionModel().getSelectedItem())); blockChainExplorerComboBox.setOnAction(e -> preferences.setBlockChainExplorer(blockChainExplorerComboBox.getSelectionModel().getSelectedItem()));
deviationInputTextField.setText(formatter.formatToPercent(preferences.getMaxPriceDistanceInPercent())); deviationInputTextField.setText(formatter.formatToPercentWithSymbol(preferences.getMaxPriceDistanceInPercent()));
deviationInputTextField.textProperty().addListener(deviationListener); deviationInputTextField.textProperty().addListener(deviationListener);
deviationInputTextField.focusedProperty().addListener(deviationFocusedListener); deviationInputTextField.focusedProperty().addListener(deviationFocusedListener);

View file

@ -325,11 +325,57 @@ public class BSFormatter {
} }
public String formatToPercent(double value) { public String formatToPercent(double value) {
return formatToPercent(value, 1);
}
public String formatToPercent(double value, int digits) {
DecimalFormat decimalFormat = (DecimalFormat) DecimalFormat.getInstance(locale); DecimalFormat decimalFormat = (DecimalFormat) DecimalFormat.getInstance(locale);
decimalFormat.setMinimumFractionDigits(1); decimalFormat.setMinimumFractionDigits(digits);
decimalFormat.setMaximumFractionDigits(1); decimalFormat.setMaximumFractionDigits(digits);
decimalFormat.setGroupingUsed(false); decimalFormat.setGroupingUsed(false);
return decimalFormat.format(value * 100.0) + " %"; return decimalFormat.format(value * 100.0);
}
public String formatToNumberString(double value, int digits) {
DecimalFormat decimalFormat = (DecimalFormat) DecimalFormat.getInstance(locale);
decimalFormat.setMinimumFractionDigits(digits);
decimalFormat.setMaximumFractionDigits(digits);
decimalFormat.setGroupingUsed(false);
return decimalFormat.format(value);
}
public double parseNumberStringToDouble(String percentString) throws NumberFormatException {
try {
String input = percentString.replace(",", ".");
input = input.replace(" ", "");
return Double.parseDouble(input);
} catch (NumberFormatException e) {
throw e;
}
}
public String formatToPercentWithSymbol(double value) {
return formatToPercent(value) + " %";
}
public double parsePercentStringToDouble(String percentString) throws NumberFormatException {
try {
String input = percentString.replace("%", "");
input = input.replace(",", ".");
input = input.replace(" ", "");
double value = Double.parseDouble(input);
return value / 100;
} catch (NumberFormatException e) {
throw e;
}
}
public double roundDouble(double value, int places) {
if (places < 0) throw new IllegalArgumentException();
long factor = (long) Math.pow(10, places);
value = value * factor;
long tmp = Math.round(value);
return (double) tmp / factor;
} }
private String cleanInput(String input) { private String cleanInput(String input) {

View file

@ -267,6 +267,8 @@ public class OfferBookViewModelTest {
null, null,
0, 0,
0, 0,
false,
0,
0, 0,
tradeCurrencyCode, tradeCurrencyCode,
null, null,
@ -275,6 +277,7 @@ public class OfferBookViewModelTest {
countryCode, countryCode,
acceptedCountryCodes, acceptedCountryCodes,
bankId, bankId,
acceptedBanks); acceptedBanks,
null);
} }
} }