mirror of
https://github.com/bisq-network/bisq.git
synced 2024-11-19 09:52:23 +01:00
Merge branch 'LiveStats' into Development
This commit is contained in:
commit
2240daf9c0
@ -20,11 +20,14 @@ package io.bitsquare.app;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class Version {
|
||||
private static final Logger log = LoggerFactory.getLogger(Version.class);
|
||||
|
||||
// The application versions
|
||||
public static final String VERSION = "0.4.9";
|
||||
public static final String VERSION = "0.4.9.1";
|
||||
|
||||
// The version nr. for the objects sent over the network. A change will break the serialization of old objects.
|
||||
// If objects are used for both network and database the network version is applied.
|
||||
@ -39,14 +42,13 @@ public class Version {
|
||||
// VERSION = 0.3.5 -> LOCAL_DB_VERSION = 2
|
||||
// VERSION = 0.4.0 -> LOCAL_DB_VERSION = 3
|
||||
// VERSION = 0.4.2 -> LOCAL_DB_VERSION = 4
|
||||
public static final int LOCAL_DB_VERSION = 4;
|
||||
public static final int LOCAL_DB_VERSION = 4;
|
||||
|
||||
// The version nr. of the current protocol. The offer holds that version.
|
||||
// A taker will check the version of the offers to see if his version is compatible.
|
||||
public static final int TRADE_PROTOCOL_VERSION = 1;
|
||||
private static int p2pMessageVersion;
|
||||
|
||||
|
||||
public static int getP2PMessageVersion() {
|
||||
// TODO investigate why a changed NETWORK_PROTOCOL_VERSION for the serialized objects does not trigger
|
||||
// reliable a disconnect., but java serialisation should be replaced anyway, so using one existing field
|
||||
@ -81,4 +83,29 @@ public class Version {
|
||||
", getP2PNetworkId()=" + getP2PMessageVersion() +
|
||||
'}');
|
||||
}
|
||||
|
||||
// We can define here special features the client is supporting.
|
||||
// Useful for updates to new versions where a new data type would break backwards compatibility or to
|
||||
// limit a node to certain behaviour and roles like the seed nodes.
|
||||
// We don't use the Enum in any serialized data, as changes in the enum would break backwards compatibility. We use the ordinal integer instead.
|
||||
// Sequence in the enum must not be changed (append only).
|
||||
public enum Capability {
|
||||
SEED_NODE,
|
||||
TRADE_STATISTICS
|
||||
}
|
||||
|
||||
public static void setCapabilities(ArrayList<Integer> capabilities) {
|
||||
Version.capabilities = capabilities;
|
||||
}
|
||||
|
||||
private static ArrayList<Integer> capabilities = new ArrayList<>(Arrays.asList(
|
||||
Capability.TRADE_STATISTICS.ordinal()
|
||||
));
|
||||
|
||||
/**
|
||||
* @return The Capabilities as ordinal integer the client supports.
|
||||
*/
|
||||
public static ArrayList<Integer> getCapabilities() {
|
||||
return capabilities;
|
||||
}
|
||||
}
|
||||
|
@ -2,5 +2,5 @@ package io.bitsquare.common;
|
||||
|
||||
public class CommonOptionKeys {
|
||||
public static final String LOG_LEVEL_KEY = "logLevel";
|
||||
public static final String IGNORE_DEV_MSG_KEY = "ignoreDevMsg";
|
||||
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import com.google.gson.*;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -465,4 +466,12 @@ public class Utilities {
|
||||
// This simply matches the Oracle JRE, but not OpenJDK.
|
||||
return "Java(TM) SE Runtime Environment".equals(System.getProperty("java.runtime.name"));
|
||||
}
|
||||
|
||||
public static String toTruncatedString(Object message, int maxLenght) {
|
||||
return StringUtils.abbreviate(message.toString(), maxLenght).replace("\n", "");
|
||||
}
|
||||
|
||||
public static String toTruncatedString(Object message) {
|
||||
return toTruncatedString(message, 200);
|
||||
}
|
||||
}
|
||||
|
@ -77,7 +77,7 @@ public class Storage<T extends Serializable> {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public T initAndGetPersisted(String fileName) {
|
||||
public T initAndGetPersistedWithFileName(String fileName) {
|
||||
this.fileName = fileName;
|
||||
storageFile = new File(dir, fileName);
|
||||
fileManager = new FileManager<>(dir, storageFile, 300);
|
||||
|
@ -19,7 +19,7 @@ package io.bitsquare.alert;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.name.Named;
|
||||
import io.bitsquare.common.CommonOptionKeys;
|
||||
import io.bitsquare.app.CoreOptionKeys;
|
||||
import io.bitsquare.common.crypto.KeyRing;
|
||||
import io.bitsquare.p2p.P2PService;
|
||||
import io.bitsquare.p2p.storage.HashMapChangedListener;
|
||||
@ -56,7 +56,7 @@ public class AlertManager {
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Inject
|
||||
public AlertManager(P2PService p2PService, KeyRing keyRing, User user, @Named(CommonOptionKeys.IGNORE_DEV_MSG_KEY) boolean ignoreDevMsg) {
|
||||
public AlertManager(P2PService p2PService, KeyRing keyRing, User user, @Named(CoreOptionKeys.IGNORE_DEV_MSG_KEY) boolean ignoreDevMsg) {
|
||||
this.p2PService = p2PService;
|
||||
this.keyRing = keyRing;
|
||||
this.user = user;
|
||||
|
@ -19,10 +19,13 @@ package io.bitsquare.alert;
|
||||
|
||||
import com.google.inject.Singleton;
|
||||
import io.bitsquare.app.AppModule;
|
||||
import io.bitsquare.app.CoreOptionKeys;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
import static com.google.inject.name.Names.named;
|
||||
|
||||
public class AlertModule extends AppModule {
|
||||
private static final Logger log = LoggerFactory.getLogger(AlertModule.class);
|
||||
|
||||
@ -34,5 +37,6 @@ public class AlertModule extends AppModule {
|
||||
protected final void configure() {
|
||||
bind(AlertManager.class).in(Singleton.class);
|
||||
bind(PrivateNotificationManager.class).in(Singleton.class);
|
||||
bindConstant().annotatedWith(named(CoreOptionKeys.IGNORE_DEV_MSG_KEY)).to(env.getRequiredProperty(CoreOptionKeys.IGNORE_DEV_MSG_KEY));
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ package io.bitsquare.alert;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.name.Named;
|
||||
import io.bitsquare.common.CommonOptionKeys;
|
||||
import io.bitsquare.app.CoreOptionKeys;
|
||||
import io.bitsquare.common.crypto.KeyRing;
|
||||
import io.bitsquare.crypto.DecryptedMsgWithPubKey;
|
||||
import io.bitsquare.p2p.Message;
|
||||
@ -58,7 +58,7 @@ public class PrivateNotificationManager {
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Inject
|
||||
public PrivateNotificationManager(P2PService p2PService, KeyRing keyRing, @Named(CommonOptionKeys.IGNORE_DEV_MSG_KEY) boolean ignoreDevMsg) {
|
||||
public PrivateNotificationManager(P2PService p2PService, KeyRing keyRing, @Named(CoreOptionKeys.IGNORE_DEV_MSG_KEY) boolean ignoreDevMsg) {
|
||||
this.p2PService = p2PService;
|
||||
this.keyRing = keyRing;
|
||||
|
||||
|
@ -85,7 +85,7 @@ public class BitsquareEnvironment extends StandardEnvironment {
|
||||
private final String btcNetworkDir;
|
||||
private final String logLevel;
|
||||
private BitcoinNetwork bitcoinNetwork;
|
||||
private final String btcSeedNodes, seedNodes, ignoreDevMsg, useTorForBtc, myAddress, banList;
|
||||
private final String btcSeedNodes, seedNodes, ignoreDevMsg, useTorForBtc, myAddress, banList, dumpStatistics;
|
||||
|
||||
public BitsquareEnvironment(OptionSet options) {
|
||||
this(new JOptCommandLinePropertySource(BITSQUARE_COMMANDLINE_PROPERTY_SOURCE_NAME, checkNotNull(
|
||||
@ -153,8 +153,8 @@ public class BitsquareEnvironment extends StandardEnvironment {
|
||||
(String) commandLineProperties.getProperty(NetworkOptionKeys.BAN_LIST) :
|
||||
"";
|
||||
|
||||
ignoreDevMsg = commandLineProperties.containsProperty(CommonOptionKeys.IGNORE_DEV_MSG_KEY) ?
|
||||
(String) commandLineProperties.getProperty(CommonOptionKeys.IGNORE_DEV_MSG_KEY) :
|
||||
ignoreDevMsg = commandLineProperties.containsProperty(CoreOptionKeys.IGNORE_DEV_MSG_KEY) ?
|
||||
(String) commandLineProperties.getProperty(CoreOptionKeys.IGNORE_DEV_MSG_KEY) :
|
||||
"";
|
||||
|
||||
btcSeedNodes = commandLineProperties.containsProperty(BtcOptionKeys.BTC_SEED_NODES) ?
|
||||
@ -165,6 +165,10 @@ public class BitsquareEnvironment extends StandardEnvironment {
|
||||
(String) commandLineProperties.getProperty(BtcOptionKeys.USE_TOR_FOR_BTC) :
|
||||
"";
|
||||
|
||||
dumpStatistics = commandLineProperties.containsProperty(CoreOptionKeys.DUMP_STATISTICS) ?
|
||||
(String) commandLineProperties.getProperty(CoreOptionKeys.DUMP_STATISTICS) :
|
||||
"";
|
||||
|
||||
|
||||
MutablePropertySources propertySources = this.getPropertySources();
|
||||
propertySources.addFirst(commandLineProperties);
|
||||
@ -226,7 +230,8 @@ public class BitsquareEnvironment extends StandardEnvironment {
|
||||
setProperty(NetworkOptionKeys.SEED_NODES_KEY, seedNodes);
|
||||
setProperty(NetworkOptionKeys.MY_ADDRESS, myAddress);
|
||||
setProperty(NetworkOptionKeys.BAN_LIST, banList);
|
||||
setProperty(CommonOptionKeys.IGNORE_DEV_MSG_KEY, ignoreDevMsg);
|
||||
setProperty(CoreOptionKeys.IGNORE_DEV_MSG_KEY, ignoreDevMsg);
|
||||
setProperty(CoreOptionKeys.DUMP_STATISTICS, dumpStatistics);
|
||||
|
||||
setProperty(BtcOptionKeys.BTC_SEED_NODES, btcSeedNodes);
|
||||
setProperty(BtcOptionKeys.USE_TOR_FOR_BTC, useTorForBtc);
|
||||
|
@ -86,11 +86,15 @@ public abstract class BitsquareExecutable {
|
||||
parser.accepts(NetworkOptionKeys.BAN_LIST, description("Nodes to exclude from network connections.", ""))
|
||||
.withRequiredArg();
|
||||
|
||||
parser.accepts(CommonOptionKeys.IGNORE_DEV_MSG_KEY, description("If set to true all signed messages from Bitsquare developers are ignored " +
|
||||
parser.accepts(CoreOptionKeys.IGNORE_DEV_MSG_KEY, description("If set to true all signed messages from Bitsquare developers are ignored " +
|
||||
"(Global alert, Version update alert, Filters for offers, nodes or payment account data)", false))
|
||||
.withRequiredArg()
|
||||
.ofType(boolean.class);
|
||||
|
||||
parser.accepts(CoreOptionKeys.DUMP_STATISTICS, description("If set to true the trade statistics are stored as json file in the data dir.", false))
|
||||
.withRequiredArg()
|
||||
.ofType(boolean.class);
|
||||
|
||||
parser.accepts(BtcOptionKeys.BTC_SEED_NODES, description("Custom seed nodes used for BitcoinJ.", ""))
|
||||
.withRequiredArg();
|
||||
parser.accepts(BtcOptionKeys.USE_TOR_FOR_BTC, description("If set to true BitcoinJ is routed over our native Tor instance.", ""))
|
||||
|
7
core/src/main/java/io/bitsquare/app/CoreOptionKeys.java
Normal file
7
core/src/main/java/io/bitsquare/app/CoreOptionKeys.java
Normal file
@ -0,0 +1,7 @@
|
||||
package io.bitsquare.app;
|
||||
|
||||
public class CoreOptionKeys {
|
||||
public static final String IGNORE_DEV_MSG_KEY = "ignoreDevMsg";
|
||||
public static final String DUMP_STATISTICS = "dumpStatistics";
|
||||
|
||||
}
|
@ -23,6 +23,7 @@ import io.bitsquare.p2p.messaging.MailboxMessage;
|
||||
import java.util.UUID;
|
||||
|
||||
public abstract class DisputeMessage implements MailboxMessage {
|
||||
//TODO add serialVersionUID also in superclasses as changes would break compatibility
|
||||
private final int messageVersion = Version.getP2PMessageVersion();
|
||||
private final String uid = UUID.randomUUID().toString();
|
||||
|
||||
|
@ -136,7 +136,7 @@ public class WalletService {
|
||||
useTor = preferences.getUseTorForBitcoinJ();
|
||||
|
||||
storage = new Storage<>(walletDir);
|
||||
Long persisted = storage.initAndGetPersisted("BloomFilterNonce");
|
||||
Long persisted = storage.initAndGetPersistedWithFileName("BloomFilterNonce");
|
||||
if (persisted != null) {
|
||||
bloomFilterTweak = persisted;
|
||||
} else {
|
||||
|
@ -19,7 +19,7 @@ package io.bitsquare.filter;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.name.Named;
|
||||
import io.bitsquare.common.CommonOptionKeys;
|
||||
import io.bitsquare.app.CoreOptionKeys;
|
||||
import io.bitsquare.common.crypto.KeyRing;
|
||||
import io.bitsquare.common.util.Tuple3;
|
||||
import io.bitsquare.common.util.Utilities;
|
||||
@ -59,7 +59,7 @@ public class FilterManager {
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Inject
|
||||
public FilterManager(P2PService p2PService, KeyRing keyRing, User user, @Named(CommonOptionKeys.IGNORE_DEV_MSG_KEY) boolean ignoreDevMsg) {
|
||||
public FilterManager(P2PService p2PService, KeyRing keyRing, User user, @Named(CoreOptionKeys.IGNORE_DEV_MSG_KEY) boolean ignoreDevMsg) {
|
||||
this.p2PService = p2PService;
|
||||
this.keyRing = keyRing;
|
||||
this.user = user;
|
||||
|
@ -19,7 +19,7 @@ package io.bitsquare.filter;
|
||||
|
||||
import com.google.inject.Singleton;
|
||||
import io.bitsquare.app.AppModule;
|
||||
import io.bitsquare.common.CommonOptionKeys;
|
||||
import io.bitsquare.app.CoreOptionKeys;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.env.Environment;
|
||||
@ -36,6 +36,6 @@ public class FilterModule extends AppModule {
|
||||
@Override
|
||||
protected final void configure() {
|
||||
bind(FilterManager.class).in(Singleton.class);
|
||||
bindConstant().annotatedWith(named(CommonOptionKeys.IGNORE_DEV_MSG_KEY)).to(env.getRequiredProperty(CommonOptionKeys.IGNORE_DEV_MSG_KEY));
|
||||
bindConstant().annotatedWith(named(CoreOptionKeys.IGNORE_DEV_MSG_KEY)).to(env.getRequiredProperty(CoreOptionKeys.IGNORE_DEV_MSG_KEY));
|
||||
}
|
||||
}
|
||||
|
@ -64,8 +64,10 @@ import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static io.bitsquare.util.Validator.nonEmptyStringOf;
|
||||
@ -82,7 +84,8 @@ public class TradeManager {
|
||||
private final FailedTradesManager failedTradesManager;
|
||||
private final ArbitratorManager arbitratorManager;
|
||||
private final P2PService p2PService;
|
||||
private FilterManager filterManager;
|
||||
private final FilterManager filterManager;
|
||||
private final TradeStatisticsManager tradeStatisticsManager;
|
||||
|
||||
private final Storage<TradableList<Trade>> tradableListStorage;
|
||||
private final TradableList<Trade> trades;
|
||||
@ -105,6 +108,7 @@ public class TradeManager {
|
||||
P2PService p2PService,
|
||||
PriceFeed priceFeed,
|
||||
FilterManager filterManager,
|
||||
TradeStatisticsManager tradeStatisticsManager,
|
||||
@Named(Storage.DIR_KEY) File storageDir) {
|
||||
this.user = user;
|
||||
this.keyRing = keyRing;
|
||||
@ -116,6 +120,7 @@ public class TradeManager {
|
||||
this.arbitratorManager = arbitratorManager;
|
||||
this.p2PService = p2PService;
|
||||
this.filterManager = filterManager;
|
||||
this.tradeStatisticsManager = tradeStatisticsManager;
|
||||
|
||||
tradableListStorage = new Storage<>(storageDir);
|
||||
trades = new TradableList<>(tradableListStorage, "PendingTrades");
|
||||
@ -194,16 +199,38 @@ public class TradeManager {
|
||||
} else {
|
||||
toRemove.add(trade);
|
||||
}
|
||||
|
||||
addTradeStatistics(trade);
|
||||
}
|
||||
for (Trade trade : toAdd) {
|
||||
for (Trade trade : toAdd)
|
||||
addTradeToFailedTrades(trade);
|
||||
}
|
||||
for (Trade trade : toRemove) {
|
||||
|
||||
for (Trade trade : toRemove)
|
||||
removePreparedTrade(trade);
|
||||
|
||||
for (Tradable tradable : closedTradableManager.getClosedTrades()) {
|
||||
if (tradable instanceof Trade) {
|
||||
Trade trade = (Trade) tradable;
|
||||
addTradeStatistics(trade);
|
||||
}
|
||||
}
|
||||
|
||||
pendingTradesInitialized.set(true);
|
||||
}
|
||||
|
||||
private void addTradeStatistics(Trade trade) {
|
||||
TradeStatistics tradeStatistics = new TradeStatistics(trade.getOffer(),
|
||||
trade.getTradePrice(),
|
||||
trade.getTradeAmount(),
|
||||
trade.getDate(),
|
||||
(trade.getDepositTx() != null ? trade.getDepositTx().getHashAsString() : ""),
|
||||
keyRing.getPubKeyRing());
|
||||
tradeStatisticsManager.add(tradeStatistics);
|
||||
// Only offerer publishes statistic data of trades, only trades from last 20 days
|
||||
if (isMyOffer(trade.getOffer()) && (new Date().getTime() - trade.getDate().getTime()) < TimeUnit.DAYS.toMillis(20))
|
||||
p2PService.addData(tradeStatistics, true);
|
||||
}
|
||||
|
||||
private void handleInitialTakeOfferRequest(TradeMessage message, NodeAddress peerNodeAddress) {
|
||||
log.trace("handleNewMessage: message = " + message.getClass().getSimpleName() + " from " + peerNodeAddress);
|
||||
try {
|
||||
|
@ -19,12 +19,15 @@ package io.bitsquare.trade;
|
||||
|
||||
import com.google.inject.Singleton;
|
||||
import io.bitsquare.app.AppModule;
|
||||
import io.bitsquare.app.CoreOptionKeys;
|
||||
import io.bitsquare.trade.closed.ClosedTradableManager;
|
||||
import io.bitsquare.trade.failed.FailedTradesManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
import static com.google.inject.name.Names.named;
|
||||
|
||||
public class TradeModule extends AppModule {
|
||||
private static final Logger log = LoggerFactory.getLogger(TradeModule.class);
|
||||
|
||||
@ -35,7 +38,9 @@ public class TradeModule extends AppModule {
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(TradeManager.class).in(Singleton.class);
|
||||
bind(TradeStatisticsManager.class).in(Singleton.class);
|
||||
bind(ClosedTradableManager.class).in(Singleton.class);
|
||||
bind(FailedTradesManager.class).in(Singleton.class);
|
||||
bindConstant().annotatedWith(named(CoreOptionKeys.DUMP_STATISTICS)).to(env.getRequiredProperty(CoreOptionKeys.DUMP_STATISTICS));
|
||||
}
|
||||
}
|
||||
|
159
core/src/main/java/io/bitsquare/trade/TradeStatistics.java
Normal file
159
core/src/main/java/io/bitsquare/trade/TradeStatistics.java
Normal file
@ -0,0 +1,159 @@
|
||||
package io.bitsquare.trade;
|
||||
|
||||
import io.bitsquare.app.Version;
|
||||
import io.bitsquare.common.crypto.PubKeyRing;
|
||||
import io.bitsquare.common.util.JsonExclude;
|
||||
import io.bitsquare.p2p.storage.payload.CapabilityRequiringPayload;
|
||||
import io.bitsquare.p2p.storage.payload.StoragePayload;
|
||||
import io.bitsquare.trade.offer.Offer;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.utils.ExchangeRate;
|
||||
import org.bitcoinj.utils.Fiat;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Immutable
|
||||
public final class TradeStatistics implements StoragePayload, CapabilityRequiringPayload {
|
||||
@JsonExclude
|
||||
private static final long serialVersionUID = Version.P2P_NETWORK_VERSION;
|
||||
@JsonExclude
|
||||
public static final long TTL = TimeUnit.DAYS.toMillis(10);
|
||||
|
||||
public final String currency;
|
||||
public final Offer.Direction direction;
|
||||
public final long tradePrice;
|
||||
public final long tradeAmount;
|
||||
public final long tradeDate;
|
||||
public final String paymentMethod;
|
||||
public final long offerDate;
|
||||
public final boolean useMarketBasedPrice;
|
||||
public final double marketPriceMargin;
|
||||
public final long offerAmount;
|
||||
public final long offerMinAmount;
|
||||
public final String offerId;
|
||||
public final String depositTxId;
|
||||
@JsonExclude
|
||||
public final PubKeyRing pubKeyRing;
|
||||
|
||||
public TradeStatistics(Offer offer, Fiat tradePrice, Coin tradeAmount, Date tradeDate, String depositTxId, PubKeyRing pubKeyRing) {
|
||||
this.direction = offer.getDirection();
|
||||
this.currency = offer.getCurrencyCode();
|
||||
this.paymentMethod = offer.getPaymentMethod().getId();
|
||||
this.offerDate = offer.getDate().getTime();
|
||||
this.useMarketBasedPrice = offer.getUseMarketBasedPrice();
|
||||
this.marketPriceMargin = offer.getMarketPriceMargin();
|
||||
this.offerAmount = offer.getAmount().value;
|
||||
this.offerMinAmount = offer.getMinAmount().value;
|
||||
this.offerId = offer.getId();
|
||||
|
||||
this.tradePrice = tradePrice.longValue();
|
||||
this.tradeAmount = tradeAmount.value;
|
||||
this.tradeDate = tradeDate.getTime();
|
||||
this.depositTxId = depositTxId;
|
||||
this.pubKeyRing = pubKeyRing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTTL() {
|
||||
return TTL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey getOwnerPubKey() {
|
||||
return pubKeyRing.getSignaturePubKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Integer> getRequiredCapabilities() {
|
||||
return Arrays.asList(
|
||||
Version.Capability.TRADE_STATISTICS.ordinal()
|
||||
);
|
||||
}
|
||||
|
||||
public Date getTradeDate() {
|
||||
return new Date(tradeDate);
|
||||
}
|
||||
|
||||
public Fiat getTradePrice() {
|
||||
return Fiat.valueOf(currency, tradePrice);
|
||||
}
|
||||
|
||||
public Coin getTradeAmount() {
|
||||
return Coin.valueOf(tradeAmount);
|
||||
}
|
||||
|
||||
public Fiat getTradeVolume() {
|
||||
return new ExchangeRate(getTradePrice()).coinToFiat(getTradeAmount());
|
||||
}
|
||||
|
||||
// We don't include the pubKeyRing as both traders might publish it if the offerer uses an old
|
||||
// version and update later (taker publishes first, then later offerer)
|
||||
// We also don't include the trade date as that is set locally and different for offerer and taker
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof TradeStatistics)) return false;
|
||||
|
||||
TradeStatistics that = (TradeStatistics) o;
|
||||
|
||||
if (tradePrice != that.tradePrice) return false;
|
||||
if (tradeAmount != that.tradeAmount) return false;
|
||||
if (offerDate != that.offerDate) return false;
|
||||
if (useMarketBasedPrice != that.useMarketBasedPrice) return false;
|
||||
if (Double.compare(that.marketPriceMargin, marketPriceMargin) != 0) return false;
|
||||
if (offerAmount != that.offerAmount) return false;
|
||||
if (offerMinAmount != that.offerMinAmount) return false;
|
||||
if (currency != null ? !currency.equals(that.currency) : that.currency != null) return false;
|
||||
if (direction != that.direction) return false;
|
||||
if (paymentMethod != null ? !paymentMethod.equals(that.paymentMethod) : that.paymentMethod != null)
|
||||
return false;
|
||||
if (offerId != null ? !offerId.equals(that.offerId) : that.offerId != null) return false;
|
||||
return !(depositTxId != null ? !depositTxId.equals(that.depositTxId) : that.depositTxId != null);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result;
|
||||
long temp;
|
||||
result = currency != null ? currency.hashCode() : 0;
|
||||
result = 31 * result + (direction != null ? direction.hashCode() : 0);
|
||||
result = 31 * result + (int) (tradePrice ^ (tradePrice >>> 32));
|
||||
result = 31 * result + (int) (tradeAmount ^ (tradeAmount >>> 32));
|
||||
result = 31 * result + (paymentMethod != null ? paymentMethod.hashCode() : 0);
|
||||
result = 31 * result + (int) (offerDate ^ (offerDate >>> 32));
|
||||
result = 31 * result + (useMarketBasedPrice ? 1 : 0);
|
||||
temp = Double.doubleToLongBits(marketPriceMargin);
|
||||
result = 31 * result + (int) (temp ^ (temp >>> 32));
|
||||
result = 31 * result + (int) (offerAmount ^ (offerAmount >>> 32));
|
||||
result = 31 * result + (int) (offerMinAmount ^ (offerMinAmount >>> 32));
|
||||
result = 31 * result + (offerId != null ? offerId.hashCode() : 0);
|
||||
result = 31 * result + (depositTxId != null ? depositTxId.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TradeStatistics{" +
|
||||
"currency='" + currency + '\'' +
|
||||
", direction=" + direction +
|
||||
", tradePrice=" + tradePrice +
|
||||
", tradeAmount=" + tradeAmount +
|
||||
", tradeDate=" + tradeDate +
|
||||
", paymentMethod='" + paymentMethod + '\'' +
|
||||
", offerDate=" + offerDate +
|
||||
", useMarketBasedPrice=" + useMarketBasedPrice +
|
||||
", marketPriceMargin=" + marketPriceMargin +
|
||||
", offerAmount=" + offerAmount +
|
||||
", offerMinAmount=" + offerMinAmount +
|
||||
", offerId='" + offerId + '\'' +
|
||||
", depositTxId='" + depositTxId + '\'' +
|
||||
", pubKeyRing=" + pubKeyRing +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
package io.bitsquare.trade;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.name.Named;
|
||||
import io.bitsquare.app.CoreOptionKeys;
|
||||
import io.bitsquare.common.util.Utilities;
|
||||
import io.bitsquare.p2p.P2PService;
|
||||
import io.bitsquare.p2p.storage.HashMapChangedListener;
|
||||
import io.bitsquare.p2p.storage.payload.StoragePayload;
|
||||
import io.bitsquare.p2p.storage.storageentry.ProtectedStorageEntry;
|
||||
import io.bitsquare.storage.Storage;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableSet;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class TradeStatisticsManager {
|
||||
private static final Logger log = LoggerFactory.getLogger(TradeStatisticsManager.class);
|
||||
private final Storage<HashSet<TradeStatistics>> storage;
|
||||
private Storage<String> jsonStorage;
|
||||
private boolean dumpStatistics;
|
||||
|
||||
private ObservableSet<TradeStatistics> observableTradeStatisticsSet = FXCollections.observableSet();
|
||||
private HashSet<TradeStatistics> tradeStatisticsSet = new HashSet<>();
|
||||
|
||||
@Inject
|
||||
public TradeStatisticsManager(Storage<HashSet<TradeStatistics>> storage, Storage<String> jsonStorage, P2PService p2PService, @Named(CoreOptionKeys.DUMP_STATISTICS) boolean dumpStatistics) {
|
||||
this.storage = storage;
|
||||
this.jsonStorage = jsonStorage;
|
||||
this.dumpStatistics = dumpStatistics;
|
||||
|
||||
if (dumpStatistics)
|
||||
this.jsonStorage.initAndGetPersistedWithFileName("trade_statistics.json");
|
||||
|
||||
HashSet<TradeStatistics> persisted = storage.initAndGetPersistedWithFileName("TradeStatistics");
|
||||
if (persisted != null)
|
||||
persisted.stream().forEach(e -> add(e));
|
||||
|
||||
p2PService.addHashSetChangedListener(new HashMapChangedListener() {
|
||||
@Override
|
||||
public void onAdded(ProtectedStorageEntry data) {
|
||||
final StoragePayload storagePayload = data.getStoragePayload();
|
||||
if (storagePayload instanceof TradeStatistics) {
|
||||
add((TradeStatistics) storagePayload);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoved(ProtectedStorageEntry data) {
|
||||
// We don't remove items
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void add(TradeStatistics tradeStatistics) {
|
||||
if (!tradeStatisticsSet.contains(tradeStatistics)) {
|
||||
boolean itemAlreadyAdded = tradeStatisticsSet.stream().filter(e -> (e.offerId.equals(tradeStatistics.offerId))).findAny().isPresent();
|
||||
if (!itemAlreadyAdded) {
|
||||
tradeStatisticsSet.add(tradeStatistics);
|
||||
observableTradeStatisticsSet.add(tradeStatistics);
|
||||
storage.queueUpForSave(tradeStatisticsSet, 2000);
|
||||
|
||||
if (dumpStatistics) {
|
||||
// We store the statistics as json so it is easy for further processing (e.g. for web based services)
|
||||
// TODO This is just a quick solution for storing to one file.
|
||||
// 1 statistic entry has 500 bytes as json.
|
||||
// Need a more scalable solution later when we get more volume.
|
||||
// The flag will only be activated by dedicated nodes, so it should not be too critical for the moment, but needs to
|
||||
// get improved. Maybe a LevelDB like DB...? Could be impl. in a headless version only.
|
||||
List<TradeStatistics> list = tradeStatisticsSet.stream().collect(Collectors.toList());
|
||||
list.sort((o1, o2) -> (o1.tradeDate < o2.tradeDate ? 1 : (o1.tradeDate == o2.tradeDate ? 0 : -1)));
|
||||
TradeStatistics[] array = new TradeStatistics[tradeStatisticsSet.size()];
|
||||
list.toArray(array);
|
||||
jsonStorage.queueUpForSave(Utilities.objectToJson(array), 5_000);
|
||||
}
|
||||
} else {
|
||||
log.error("We have already an item with the same offer ID. That might happen if both the offerer and the taker published the tradeStatistics");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableSet<TradeStatistics> getObservableTradeStatisticsSet() {
|
||||
return observableTradeStatisticsSet;
|
||||
}
|
||||
}
|
@ -144,6 +144,7 @@ public final class Offer implements StoragePayload, RequiresOwnerIsOnlinePayload
|
||||
transient private OfferAvailabilityProtocol availabilityProtocol;
|
||||
@JsonExclude
|
||||
transient private StringProperty errorMessageProperty = new SimpleStringProperty();
|
||||
@JsonExclude
|
||||
transient private PriceFeed priceFeed;
|
||||
|
||||
|
||||
|
@ -19,8 +19,12 @@ package io.bitsquare.trade.protocol.availability.messages;
|
||||
|
||||
import io.bitsquare.app.Version;
|
||||
import io.bitsquare.common.crypto.PubKeyRing;
|
||||
import io.bitsquare.p2p.messaging.SupportedCapabilitiesMessage;
|
||||
|
||||
public final class OfferAvailabilityRequest extends OfferMessage {
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public final class OfferAvailabilityRequest extends OfferMessage implements SupportedCapabilitiesMessage {
|
||||
// That object is sent over the wire, so we need to take care of version compatibility.
|
||||
private static final long serialVersionUID = Version.P2P_NETWORK_VERSION;
|
||||
|
||||
@ -33,6 +37,15 @@ public final class OfferAvailabilityRequest extends OfferMessage {
|
||||
this.takersTradePrice = takersTradePrice;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private ArrayList<Integer> supportedCapabilities = Version.getCapabilities();
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public ArrayList<Integer> getSupportedCapabilities() {
|
||||
return supportedCapabilities;
|
||||
}
|
||||
|
||||
public PubKeyRing getPubKeyRing() {
|
||||
return pubKeyRing;
|
||||
}
|
||||
|
@ -17,10 +17,15 @@
|
||||
|
||||
package io.bitsquare.trade.protocol.availability.messages;
|
||||
|
||||
|
||||
import io.bitsquare.app.Version;
|
||||
import io.bitsquare.p2p.messaging.SupportedCapabilitiesMessage;
|
||||
import io.bitsquare.trade.protocol.availability.AvailabilityResult;
|
||||
|
||||
public final class OfferAvailabilityResponse extends OfferMessage {
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public final class OfferAvailabilityResponse extends OfferMessage implements SupportedCapabilitiesMessage {
|
||||
// That object is sent over the wire, so we need to take care of version compatibility.
|
||||
private static final long serialVersionUID = Version.P2P_NETWORK_VERSION;
|
||||
|
||||
@ -35,6 +40,15 @@ public final class OfferAvailabilityResponse extends OfferMessage {
|
||||
isAvailable = availabilityResult == AvailabilityResult.AVAILABLE;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private ArrayList<Integer> supportedCapabilities = Version.getCapabilities();
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public ArrayList<Integer> getSupportedCapabilities() {
|
||||
return supportedCapabilities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "OfferAvailabilityResponse{" +
|
||||
|
@ -24,6 +24,7 @@ import javax.annotation.concurrent.Immutable;
|
||||
|
||||
@Immutable
|
||||
public abstract class OfferMessage implements DirectMessage {
|
||||
//TODO add serialVersionUID also in superclasses as changes would break compatibility
|
||||
// That object is sent over the wire, so we need to take care of version compatibility.
|
||||
private static final long serialVersionUID = Version.P2P_NETWORK_VERSION;
|
||||
|
||||
|
@ -132,7 +132,8 @@ public class BuyerAsOffererProtocol extends TradeProtocol implements BuyerProtoc
|
||||
TradeTaskRunner taskRunner = new TradeTaskRunner(buyerAsOffererTrade,
|
||||
() -> handleTaskRunnerSuccess("handle DepositTxPublishedMessage"),
|
||||
this::handleTaskRunnerFault);
|
||||
taskRunner.addTasks(ProcessDepositTxPublishedMessage.class);
|
||||
taskRunner.addTasks(ProcessDepositTxPublishedMessage.class,
|
||||
PublishTradeStatistics.class);
|
||||
taskRunner.run();
|
||||
}
|
||||
|
||||
|
@ -122,7 +122,8 @@ public class BuyerAsTakerProtocol extends TradeProtocol implements BuyerProtocol
|
||||
VerifyOffererAccount.class,
|
||||
VerifyAndSignContract.class,
|
||||
SignAndPublishDepositTxAsBuyer.class,
|
||||
SendDepositTxPublishedMessage.class
|
||||
SendDepositTxPublishedMessage.class,
|
||||
PublishTradeStatistics.class
|
||||
);
|
||||
taskRunner.run();
|
||||
}
|
||||
|
@ -133,7 +133,8 @@ public class SellerAsOffererProtocol extends TradeProtocol implements SellerProt
|
||||
() -> handleTaskRunnerSuccess("DepositTxPublishedMessage"),
|
||||
this::handleTaskRunnerFault);
|
||||
|
||||
taskRunner.addTasks(ProcessDepositTxPublishedMessage.class);
|
||||
taskRunner.addTasks(ProcessDepositTxPublishedMessage.class,
|
||||
PublishTradeStatistics.class);
|
||||
taskRunner.run();
|
||||
}
|
||||
|
||||
|
@ -132,7 +132,8 @@ public class SellerAsTakerProtocol extends TradeProtocol implements SellerProtoc
|
||||
VerifyOffererAccount.class,
|
||||
VerifyAndSignContract.class,
|
||||
SignAndPublishDepositTxAsSeller.class,
|
||||
SendDepositTxPublishedMessage.class
|
||||
SendDepositTxPublishedMessage.class,
|
||||
PublishTradeStatistics.class
|
||||
);
|
||||
taskRunner.run();
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import javax.annotation.concurrent.Immutable;
|
||||
|
||||
@Immutable
|
||||
public abstract class TradeMessage implements DirectMessage {
|
||||
//TODO add serialVersionUID also in superclasses as changes would break compatibility
|
||||
// That object is sent over the wire, so we need to take care of version compatibility.
|
||||
private static final long serialVersionUID = Version.P2P_NETWORK_VERSION;
|
||||
|
||||
|
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* This file is part of Bitsquare.
|
||||
*
|
||||
* Bitsquare is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bitsquare is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.bitsquare.trade.protocol.trade.tasks.offerer;
|
||||
|
||||
import io.bitsquare.common.taskrunner.TaskRunner;
|
||||
import io.bitsquare.trade.Trade;
|
||||
import io.bitsquare.trade.TradeStatistics;
|
||||
import io.bitsquare.trade.protocol.trade.tasks.TradeTask;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class PublishTradeStatistics extends TradeTask {
|
||||
private static final Logger log = LoggerFactory.getLogger(PublishTradeStatistics.class);
|
||||
|
||||
public PublishTradeStatistics(TaskRunner taskHandler, Trade trade) {
|
||||
super(taskHandler, trade);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run() {
|
||||
try {
|
||||
runInterceptHook();
|
||||
// Offerer publishes directly
|
||||
TradeStatistics tradeStatistics = new TradeStatistics(trade.getOffer(),
|
||||
trade.getTradePrice(),
|
||||
trade.getTradeAmount(),
|
||||
trade.getDate(),
|
||||
(trade.getDepositTx() != null ? trade.getDepositTx().getHashAsString() : ""),
|
||||
processModel.getPubKeyRing());
|
||||
processModel.getP2PService().addData(tradeStatistics, true);
|
||||
complete();
|
||||
} catch (Throwable t) {
|
||||
failed(t);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
/*
|
||||
* This file is part of Bitsquare.
|
||||
*
|
||||
* Bitsquare is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bitsquare is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.bitsquare.trade.protocol.trade.tasks.taker;
|
||||
|
||||
import io.bitsquare.common.taskrunner.TaskRunner;
|
||||
import io.bitsquare.trade.Trade;
|
||||
import io.bitsquare.trade.TradeStatistics;
|
||||
import io.bitsquare.trade.protocol.trade.tasks.TradeTask;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class PublishTradeStatistics extends TradeTask {
|
||||
private static final Logger log = LoggerFactory.getLogger(PublishTradeStatistics.class);
|
||||
|
||||
public PublishTradeStatistics(TaskRunner taskHandler, Trade trade) {
|
||||
super(taskHandler, trade);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run() {
|
||||
try {
|
||||
runInterceptHook();
|
||||
// taker only publishes if the offerer uses an old version
|
||||
processModel.getP2PService().getNetworkNode().getConfirmedConnections()
|
||||
.stream()
|
||||
.filter(c -> c.getPeersNodeAddressOptional().isPresent() && c.getPeersNodeAddressOptional().get().equals(trade.getTradingPeerNodeAddress()))
|
||||
.findAny()
|
||||
.ifPresent(c -> {
|
||||
TradeStatistics tradeStatistics = new TradeStatistics(trade.getOffer(),
|
||||
trade.getTradePrice(),
|
||||
trade.getTradeAmount(),
|
||||
trade.getDate(),
|
||||
(trade.getDepositTx() != null ? trade.getDepositTx().getHashAsString() : ""),
|
||||
processModel.getPubKeyRing());
|
||||
|
||||
final List<Integer> requiredCapabilities = tradeStatistics.getRequiredCapabilities();
|
||||
final List<Integer> supportedCapabilities = c.getSupportedCapabilities();
|
||||
boolean matches = false;
|
||||
if (supportedCapabilities != null) {
|
||||
for (int messageCapability : requiredCapabilities) {
|
||||
for (int connectionCapability : supportedCapabilities) {
|
||||
if (messageCapability == connectionCapability) {
|
||||
matches = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!matches) {
|
||||
log.error("We publish tradeStatistics because the offerer does use an old version.");
|
||||
processModel.getP2PService().addData(tradeStatistics, true);
|
||||
} else {
|
||||
log.error("We do not publish tradeStatistics because the offerer support the capabilities.");
|
||||
}
|
||||
});
|
||||
complete();
|
||||
} catch (Throwable t) {
|
||||
failed(t);
|
||||
}
|
||||
}
|
||||
}
|
@ -111,7 +111,8 @@ public final class Preferences implements Persistable {
|
||||
private boolean autoSelectArbitrators = true;
|
||||
private final Map<String, Boolean> dontShowAgainMap;
|
||||
private boolean tacAccepted;
|
||||
private boolean useTorForBitcoinJ = true;
|
||||
//TODO we set it to false for now as it is not ready yet
|
||||
private boolean useTorForBitcoinJ = false;
|
||||
private boolean showOwnOffersInOfferBook = true;
|
||||
private Locale preferredLocale;
|
||||
private TradeCurrency preferredTradeCurrency;
|
||||
@ -119,8 +120,11 @@ public final class Preferences implements Persistable {
|
||||
private double maxPriceDistanceInPercent;
|
||||
private boolean useInvertedMarketPrice;
|
||||
private String marketScreenCurrencyCode = CurrencyUtil.getDefaultTradeCurrency().getCode();
|
||||
private String tradeStatisticsScreenCurrencyCode = CurrencyUtil.getDefaultTradeCurrency().getCode();
|
||||
private String buyScreenCurrencyCode = CurrencyUtil.getDefaultTradeCurrency().getCode();
|
||||
private String sellScreenCurrencyCode = CurrencyUtil.getDefaultTradeCurrency().getCode();
|
||||
private int tradeStatisticsTickUnitIndex = 0;
|
||||
|
||||
private boolean useStickyMarketPrice = false;
|
||||
private boolean usePercentageBasedPrice = false;
|
||||
private Map<String, String> peerTagMap = new HashMap<>();
|
||||
@ -205,6 +209,8 @@ public final class Preferences implements Persistable {
|
||||
marketScreenCurrencyCode = persisted.getMarketScreenCurrencyCode();
|
||||
buyScreenCurrencyCode = persisted.getBuyScreenCurrencyCode();
|
||||
sellScreenCurrencyCode = persisted.getSellScreenCurrencyCode();
|
||||
tradeStatisticsScreenCurrencyCode = persisted.getTradeStatisticsScreenCurrencyCode();
|
||||
tradeStatisticsTickUnitIndex = persisted.getTradeStatisticsTickUnitIndex();
|
||||
|
||||
if (persisted.getIgnoreTradersList() != null)
|
||||
ignoreTradersList = persisted.getIgnoreTradersList();
|
||||
@ -455,6 +461,16 @@ public final class Preferences implements Persistable {
|
||||
storage.queueUpForSave();
|
||||
}
|
||||
|
||||
public void setTradeStatisticsScreenCurrencyCode(String tradeStatisticsScreenCurrencyCode) {
|
||||
this.tradeStatisticsScreenCurrencyCode = tradeStatisticsScreenCurrencyCode;
|
||||
storage.queueUpForSave();
|
||||
}
|
||||
|
||||
public void setTradeStatisticsTickUnitIndex(int tradeStatisticsTickUnitIndex) {
|
||||
this.tradeStatisticsTickUnitIndex = tradeStatisticsTickUnitIndex;
|
||||
storage.queueUpForSave();
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Getter
|
||||
@ -607,6 +623,15 @@ public final class Preferences implements Persistable {
|
||||
public String getDefaultPath() {
|
||||
return defaultPath;
|
||||
}
|
||||
|
||||
public String getTradeStatisticsScreenCurrencyCode() {
|
||||
return tradeStatisticsScreenCurrencyCode;
|
||||
}
|
||||
|
||||
public int getTradeStatisticsTickUnitIndex() {
|
||||
return tradeStatisticsTickUnitIndex;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Private
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -636,4 +661,5 @@ public final class Preferences implements Persistable {
|
||||
this.blockChainExplorerMainNet = blockChainExplorerMainNet;
|
||||
storage.queueUpForSave(2000);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -174,7 +174,8 @@ public class BitsquareApp extends Application {
|
||||
Font.loadFont(getClass().getResource("/fonts/VerdanaBoldItalic.ttf").toExternalForm(), 13);
|
||||
scene.getStylesheets().setAll(
|
||||
"/io/bitsquare/gui/bitsquare.css",
|
||||
"/io/bitsquare/gui/images.css");
|
||||
"/io/bitsquare/gui/images.css",
|
||||
"/io/bitsquare/gui/CandleStickChart.css");
|
||||
|
||||
// configure the system tray
|
||||
SystemTray.create(primaryStage, shutDownHandler);
|
||||
|
120
gui/src/main/java/io/bitsquare/gui/CandleStickChart.css
Normal file
120
gui/src/main/java/io/bitsquare/gui/CandleStickChart.css
Normal file
@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright (c) 2008, 2013 Oracle and/or its affiliates.
|
||||
* All rights reserved. Use is subject to license terms.
|
||||
*
|
||||
* This file is available and licensed under the following license:
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
*
|
||||
* - Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* - Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in
|
||||
* the documentation and/or other materials provided with the distribution.
|
||||
* - Neither the name of Oracle Corporation nor the names of its
|
||||
* contributors may be used to endorse or promote products derived
|
||||
* from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
/* ====== CANDLE STICK CHART =========================================================== */
|
||||
|
||||
.candlestick-tooltip-label {
|
||||
-fx-font-size: 0.75em;
|
||||
-fx-font-weight: bold;
|
||||
-fx-text-fill: #666666;
|
||||
-fx-padding: 2 5 2 0;
|
||||
}
|
||||
|
||||
.candlestick-average-line {
|
||||
-fx-stroke: #00b2ff;
|
||||
-fx-stroke-width: 2px;
|
||||
}
|
||||
|
||||
.candlestick-candle {
|
||||
-fx-effect: dropshadow(two-pass-box, rgba(0, 0, 0, 0.4), 10, 0.0, 2, 4);
|
||||
}
|
||||
|
||||
.candlestick-line {
|
||||
-fx-stroke: #666666;
|
||||
-fx-stroke-width: 3px;
|
||||
}
|
||||
|
||||
.candlestick-bar {
|
||||
-fx-padding: 5;
|
||||
-demo-bar-fill: #e81a00;
|
||||
-fx-background-color: linear-gradient(derive(-demo-bar-fill, -30%), derive(-demo-bar-fill, -40%)),
|
||||
linear-gradient(derive(-demo-bar-fill, 100%), derive(-demo-bar-fill, 10%)),
|
||||
linear-gradient(derive(-demo-bar-fill, 30%), derive(-demo-bar-fill, -10%));
|
||||
-fx-background-insets: 0, 1, 2;
|
||||
}
|
||||
|
||||
.candlestick-bar.close-above-open {
|
||||
-demo-bar-fill: #1bff06;
|
||||
}
|
||||
|
||||
.candlestick-bar.open-above-close {
|
||||
-demo-bar-fill: #e81a00;
|
||||
}
|
||||
|
||||
.candlestick-bar.empty {
|
||||
-demo-bar-fill: #cccccc;
|
||||
}
|
||||
|
||||
.volume-bar {
|
||||
-fx-padding: 5;
|
||||
-demo-bar-fill: #91b1cc;
|
||||
-fx-background-color: #91b1cc;
|
||||
-fx-background-insets: 0, 1, 2;
|
||||
}
|
||||
|
||||
.volume-bar.bg {
|
||||
-demo-bar-fill: #70bfc6;
|
||||
}
|
||||
|
||||
.volume-bar {
|
||||
-fx-effect: dropshadow(two-pass-box, rgba(0, 0, 0, 0.4), 10, 0.0, 2, 4);
|
||||
}
|
||||
|
||||
/*.default-color0.chart-series-line {
|
||||
-fx-stroke: blue;
|
||||
}
|
||||
|
||||
.default-color0.chart-line-symbol {
|
||||
-fx-background-color: red, green;
|
||||
}*/
|
||||
|
||||
|
||||
|
||||
.chart-alternative-row-fill {
|
||||
-fx-fill: transparent;
|
||||
-fx-stroke: transparent;
|
||||
-fx-stroke-width: 0;
|
||||
}
|
||||
|
||||
.chart-plot-background {
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
|
||||
/*
|
||||
.chart-legend {
|
||||
-fx-background-color: transparent;
|
||||
-fx-padding: 20px;
|
||||
}
|
||||
|
||||
.chart-legend-item-symbol {
|
||||
-fx-background-radius: 0;
|
||||
}
|
||||
*/
|
@ -1069,4 +1069,35 @@ textfield */
|
||||
-fx-alignment: center;
|
||||
-fx-font-size: 10;
|
||||
-fx-text-fill: white;
|
||||
}
|
||||
}
|
||||
|
||||
#toggle-left {
|
||||
-fx-border-radius: 4 0 0 4;
|
||||
-fx-padding: 4 4 4 4;
|
||||
-fx-border-color: #aaa;
|
||||
-fx-border-style: solid solid solid solid;
|
||||
-fx-border-insets: 0 -2 0 0;
|
||||
-fx-background-insets: 0 -2 0 0;
|
||||
-fx-background-radius: 4 0 0 4;
|
||||
}
|
||||
|
||||
#toggle-center {
|
||||
-fx-border-radius: 0 0 0 0;
|
||||
-fx-padding: 4 4 4 4;
|
||||
-fx-border-color: #aaa;
|
||||
-fx-border-style: solid solid solid solid;
|
||||
-fx-border-insets: 0 0 0 0;
|
||||
-fx-background-insets: 0 0 0 0;
|
||||
-fx-background-radius: 0 0 0 0;
|
||||
}
|
||||
|
||||
#toggle-right {
|
||||
-fx-border-radius: 0 4 4 0;
|
||||
-fx-padding: 4 4 4 4;
|
||||
-fx-border-color: #aaa;
|
||||
-fx-border-style: solid solid solid solid;
|
||||
-fx-border-insets: 0 0 0 -2;
|
||||
-fx-background-insets: 0 0 0 -2;
|
||||
-fx-background-radius: 0 4 4 0;
|
||||
}
|
||||
|
||||
|
@ -168,36 +168,16 @@ public class AltCoinAccountsView extends ActivatableViewAndModel<GridPane, AltCo
|
||||
.closeButtonText("I understand")
|
||||
.show();
|
||||
} else if (code.equals("ETHC")) {
|
||||
//TODO remove after JULY, 21
|
||||
if (new Date().before(new Date(2016 - 1900, Calendar.JULY, 21))) {
|
||||
new Popup().information("You cannot use EtherClassic before the hard fork gets activated.\n" +
|
||||
"It is planned for July, 20 2016, but please check on their project web page for detailed information.\n\n" +
|
||||
"The EHT/ETHC fork situation carries considerable risks.\n" +
|
||||
"Be sure you fully understand the situation and check out the information on the \"Ethereum Classic\" and \"Ethereum\" project web pages.")
|
||||
.closeButtonText("I understand")
|
||||
.onAction(() -> Utilities.openWebPage("https://ethereumclassic.github.io/"))
|
||||
.actionButtonText("Open Ethereum Classic web page")
|
||||
.show();
|
||||
} else if (new Date().before(new Date(2016 - 1900, Calendar.AUGUST, 30))) {
|
||||
//TODO remove after AUGUST, 30
|
||||
new Popup().information("The EHT/ETHC fork situation carries considerable risks.\n" +
|
||||
"Be sure you fully understand the situation and check out the information on the \"Ethereum Classic\" and \"Ethereum\" project web pages.")
|
||||
.closeButtonText("I understand")
|
||||
.onAction(() -> Utilities.openWebPage("https://ethereumclassic.github.io/"))
|
||||
.actionButtonText("Open Ethereum Classic web page")
|
||||
.show();
|
||||
}
|
||||
} else if (code.equals("ETH")) {
|
||||
//TODO remove after AUGUST, 30
|
||||
if (new Date().before(new Date(2016 - 1900, Calendar.AUGUST, 30))) {
|
||||
new Popup().information("The EHT/ETHC fork situation carries considerable risks.\n" +
|
||||
new Popup().information("The EHT/ETC fork situation carries considerable risks.\n" +
|
||||
"Be sure you fully understand the situation and check out the information on the \"Ethereum Classic\" and \"Ethereum\" project web pages.")
|
||||
.closeButtonText("I understand")
|
||||
.onAction(() -> Utilities.openWebPage("https://www.ethereum.org/"))
|
||||
.actionButtonText("Open Ethereum web page")
|
||||
.onAction(() -> Utilities.openWebPage("https://ethereumclassic.github.io/"))
|
||||
.actionButtonText("Open Ethereum Classic web page")
|
||||
.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!model.getPaymentAccounts().stream().filter(e -> {
|
||||
if (e.getAccountName() != null)
|
||||
|
@ -27,4 +27,5 @@
|
||||
|
||||
<Tab fx:id="chartsTab" text="Offer book" closable="false"/>
|
||||
<Tab fx:id="statisticsTab" text="Spreads" closable="false"/>
|
||||
<Tab fx:id="tradesTab" text="Trades" closable="false"/>
|
||||
</TabPane>
|
||||
|
@ -23,6 +23,7 @@ import io.bitsquare.gui.common.view.*;
|
||||
import io.bitsquare.gui.main.MainView;
|
||||
import io.bitsquare.gui.main.markets.charts.MarketsChartsView;
|
||||
import io.bitsquare.gui.main.markets.statistics.MarketsStatisticsView;
|
||||
import io.bitsquare.gui.main.markets.trades.TradesChartsView;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Tab;
|
||||
@ -33,7 +34,7 @@ import javax.inject.Inject;
|
||||
@FxmlView
|
||||
public class MarketView extends ActivatableViewAndModel<TabPane, Activatable> {
|
||||
@FXML
|
||||
Tab chartsTab, statisticsTab;
|
||||
Tab chartsTab, tradesTab, statisticsTab;
|
||||
private final ViewLoader viewLoader;
|
||||
private final Navigation navigation;
|
||||
private Navigation.Listener navigationListener;
|
||||
@ -55,6 +56,8 @@ public class MarketView extends ActivatableViewAndModel<TabPane, Activatable> {
|
||||
tabChangeListener = (ov, oldValue, newValue) -> {
|
||||
if (newValue == chartsTab)
|
||||
navigation.navigateTo(MainView.class, MarketView.class, MarketsChartsView.class);
|
||||
else if (newValue == tradesTab)
|
||||
navigation.navigateTo(MainView.class, MarketView.class, TradesChartsView.class);
|
||||
else if (newValue == statisticsTab)
|
||||
navigation.navigateTo(MainView.class, MarketView.class, MarketsStatisticsView.class);
|
||||
};
|
||||
@ -67,6 +70,8 @@ public class MarketView extends ActivatableViewAndModel<TabPane, Activatable> {
|
||||
|
||||
if (root.getSelectionModel().getSelectedItem() == chartsTab)
|
||||
navigation.navigateTo(MainView.class, MarketView.class, MarketsChartsView.class);
|
||||
else if (root.getSelectionModel().getSelectedItem() == tradesTab)
|
||||
navigation.navigateTo(MainView.class, MarketView.class, TradesChartsView.class);
|
||||
else
|
||||
navigation.navigateTo(MainView.class, MarketView.class, MarketsStatisticsView.class);
|
||||
}
|
||||
@ -82,6 +87,7 @@ public class MarketView extends ActivatableViewAndModel<TabPane, Activatable> {
|
||||
View view = viewLoader.load(viewClass);
|
||||
|
||||
if (view instanceof MarketsChartsView) tab = chartsTab;
|
||||
else if (view instanceof TradesChartsView) tab = tradesTab;
|
||||
else if (view instanceof MarketsStatisticsView) tab = statisticsTab;
|
||||
else throw new IllegalArgumentException("Navigation to " + viewClass + " is not supported");
|
||||
|
||||
|
@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!--
|
||||
~ This file is part of Bitsquare.
|
||||
~
|
||||
~ Bitsquare is free software: you can redistribute it and/or modify it
|
||||
~ under the terms of the GNU Affero General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or (at
|
||||
~ your option) any later version.
|
||||
~
|
||||
~ Bitsquare is distributed in the hope that it will be useful, but WITHOUT
|
||||
~ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
~ FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
~ License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU Affero General Public License
|
||||
~ along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<VBox fx:id="root" fx:controller="io.bitsquare.gui.main.markets.trades.TradesChartsView"
|
||||
spacing="10.0" fillWidth="true"
|
||||
AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0"
|
||||
AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0"
|
||||
xmlns:fx="http://javafx.com/fxml">
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="20.0" top="10.0" right="20"/>
|
||||
</padding>
|
||||
|
||||
</VBox>
|
@ -0,0 +1,505 @@
|
||||
/*
|
||||
* This file is part of Bitsquare.
|
||||
*
|
||||
* Bitsquare is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bitsquare is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.bitsquare.gui.main.markets.trades;
|
||||
|
||||
import io.bitsquare.common.UserThread;
|
||||
import io.bitsquare.gui.common.view.ActivatableViewAndModel;
|
||||
import io.bitsquare.gui.common.view.FxmlView;
|
||||
import io.bitsquare.gui.main.markets.trades.charts.price.CandleStickChart;
|
||||
import io.bitsquare.gui.main.markets.trades.charts.volume.VolumeChart;
|
||||
import io.bitsquare.gui.util.BSFormatter;
|
||||
import io.bitsquare.locale.CryptoCurrency;
|
||||
import io.bitsquare.locale.FiatCurrency;
|
||||
import io.bitsquare.locale.TradeCurrency;
|
||||
import io.bitsquare.trade.TradeStatistics;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.transformation.SortedList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.chart.NumberAxis;
|
||||
import javafx.scene.chart.XYChart;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.Callback;
|
||||
import javafx.util.StringConverter;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.utils.Fiat;
|
||||
import org.fxmisc.easybind.EasyBind;
|
||||
import org.fxmisc.easybind.Subscription;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.util.Date;
|
||||
|
||||
@FxmlView
|
||||
public class TradesChartsView extends ActivatableViewAndModel<VBox, TradesChartsViewModel> {
|
||||
private static final Logger log = LoggerFactory.getLogger(TradesChartsView.class);
|
||||
|
||||
private final BSFormatter formatter;
|
||||
|
||||
private TableView<TradeStatistics> tableView;
|
||||
private ComboBox<TradeCurrency> currencyComboBox;
|
||||
private VolumeChart volumeChart;
|
||||
private CandleStickChart priceChart;
|
||||
private NumberAxis priceAxisX, priceAxisY, volumeAxisY, volumeAxisX;
|
||||
private XYChart.Series<Number, Number> priceSeries;
|
||||
private XYChart.Series<Number, Number> volumeSeries;
|
||||
private ChangeListener<Number> priceAxisYWidthListener;
|
||||
private ChangeListener<Number> volumeAxisYWidthListener;
|
||||
private double priceAxisYWidth;
|
||||
private double volumeAxisYWidth;
|
||||
private final StringProperty priceColumnLabel = new SimpleStringProperty();
|
||||
private ChangeListener<Toggle> toggleChangeListener;
|
||||
private ToggleGroup toggleGroup;
|
||||
|
||||
private final ListChangeListener<XYChart.Data<Number, Number>> itemsChangeListener;
|
||||
private Subscription tradeCurrencySubscriber;
|
||||
|
||||
private SortedList<TradeStatistics> sortedList;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor, lifecycle
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Inject
|
||||
public TradesChartsView(TradesChartsViewModel model, BSFormatter formatter) {
|
||||
super(model);
|
||||
this.formatter = formatter;
|
||||
|
||||
// Need to render on next frame as otherwise there are issues in the chart rendering
|
||||
itemsChangeListener = c -> UserThread.execute(this::updateChartData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
HBox toolBox = getToolBox();
|
||||
createCharts();
|
||||
createTable();
|
||||
root.getChildren().addAll(toolBox, priceChart, volumeChart, tableView);
|
||||
|
||||
toggleChangeListener = (observable, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
model.setTickUnit((TradesChartsViewModel.TickUnit) newValue.getUserData());
|
||||
priceAxisX.setTickLabelFormatter(getTimeAxisStringConverter());
|
||||
}
|
||||
};
|
||||
priceAxisYWidthListener = (observable, oldValue, newValue) -> {
|
||||
priceAxisYWidth = (double) newValue;
|
||||
layoutChart();
|
||||
};
|
||||
volumeAxisYWidthListener = (observable, oldValue, newValue) -> {
|
||||
volumeAxisYWidth = (double) newValue;
|
||||
layoutChart();
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void activate() {
|
||||
currencyComboBox.setItems(model.getTradeCurrencies());
|
||||
currencyComboBox.getSelectionModel().select(model.getTradeCurrency());
|
||||
currencyComboBox.setVisibleRowCount(Math.min(currencyComboBox.getItems().size(), 25));
|
||||
currencyComboBox.setOnAction(e -> model.onSetTradeCurrency(currencyComboBox.getSelectionModel().getSelectedItem()));
|
||||
|
||||
toggleGroup.getToggles().get(model.tickUnit.ordinal()).setSelected(true);
|
||||
|
||||
model.priceItems.addListener(itemsChangeListener);
|
||||
toggleGroup.selectedToggleProperty().addListener(toggleChangeListener);
|
||||
priceAxisY.widthProperty().addListener(priceAxisYWidthListener);
|
||||
volumeAxisY.widthProperty().addListener(volumeAxisYWidthListener);
|
||||
|
||||
tradeCurrencySubscriber = EasyBind.subscribe(model.tradeCurrencyProperty,
|
||||
tradeCurrency -> {
|
||||
String code = tradeCurrency.getCode();
|
||||
String tradeCurrencyName = tradeCurrency.getName();
|
||||
|
||||
priceSeries.setName(tradeCurrencyName);
|
||||
final String currencyPair = formatter.getCurrencyPair(code);
|
||||
priceColumnLabel.set("Price (" + currencyPair + ")");
|
||||
});
|
||||
|
||||
sortedList = new SortedList<>(model.tradeStatisticsByCurrency);
|
||||
sortedList.comparatorProperty().bind(tableView.comparatorProperty());
|
||||
tableView.setItems(sortedList);
|
||||
|
||||
priceChart.setAnimated(model.preferences.getUseAnimations());
|
||||
volumeChart.setAnimated(model.preferences.getUseAnimations());
|
||||
updateChartData();
|
||||
priceAxisX.setTickLabelFormatter(getTimeAxisStringConverter());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deactivate() {
|
||||
model.priceItems.removeListener(itemsChangeListener);
|
||||
toggleGroup.selectedToggleProperty().removeListener(toggleChangeListener);
|
||||
priceAxisY.widthProperty().removeListener(priceAxisYWidthListener);
|
||||
volumeAxisY.widthProperty().removeListener(volumeAxisYWidthListener);
|
||||
tradeCurrencySubscriber.unsubscribe();
|
||||
currencyComboBox.setOnAction(null);
|
||||
priceAxisY.labelProperty().unbind();
|
||||
priceSeries.getData().clear();
|
||||
priceChart.getData().clear();
|
||||
|
||||
sortedList.comparatorProperty().unbind();
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Chart
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void createCharts() {
|
||||
priceSeries = new XYChart.Series<>();
|
||||
|
||||
priceAxisX = new NumberAxis(0, model.maxTicks + 1, 1);
|
||||
priceAxisX.setTickUnit(1);
|
||||
priceAxisX.setMinorTickCount(0);
|
||||
priceAxisX.setForceZeroInRange(false);
|
||||
priceAxisX.setTickLabelFormatter(getTimeAxisStringConverter());
|
||||
|
||||
priceAxisY = new NumberAxis();
|
||||
priceAxisY.setForceZeroInRange(false);
|
||||
priceAxisY.setAutoRanging(true);
|
||||
priceAxisY.labelProperty().bind(priceColumnLabel);
|
||||
priceAxisY.setTickLabelFormatter(new StringConverter<Number>() {
|
||||
@Override
|
||||
public String toString(Number object) {
|
||||
return formatter.formatFiat(Fiat.valueOf(model.getCurrencyCode(), new Double((double) object).longValue()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
priceChart = new CandleStickChart(priceAxisX, priceAxisY);
|
||||
priceChart.setMinHeight(250);
|
||||
priceChart.setLegendVisible(false);
|
||||
priceChart.setData(FXCollections.observableArrayList(priceSeries));
|
||||
priceChart.setToolTipStringConverter(new StringConverter<Number>() {
|
||||
@Override
|
||||
public String toString(Number object) {
|
||||
return formatter.formatFiatWithCode(Fiat.valueOf(model.getCurrencyCode(), (long) object));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
volumeSeries = new XYChart.Series<>();
|
||||
|
||||
volumeAxisX = new NumberAxis(0, model.maxTicks + 1, 1);
|
||||
volumeAxisX.setTickUnit(1);
|
||||
volumeAxisX.setMinorTickCount(0);
|
||||
volumeAxisX.setForceZeroInRange(false);
|
||||
volumeAxisX.setTickLabelFormatter(getTimeAxisStringConverter());
|
||||
|
||||
volumeAxisY = new NumberAxis();
|
||||
volumeAxisY.setForceZeroInRange(true);
|
||||
volumeAxisY.setAutoRanging(true);
|
||||
volumeAxisY.setLabel("Volume (BTC)");
|
||||
volumeAxisY.setTickLabelFormatter(new StringConverter<Number>() {
|
||||
@Override
|
||||
public String toString(Number object) {
|
||||
return formatter.formatCoin(Coin.valueOf(new Double((double) object).longValue()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
volumeChart = new VolumeChart(volumeAxisX, volumeAxisY);
|
||||
volumeChart.setData(FXCollections.observableArrayList(volumeSeries));
|
||||
volumeChart.setMinHeight(140);
|
||||
volumeChart.setLegendVisible(false);
|
||||
volumeChart.setToolTipStringConverter(new StringConverter<Number>() {
|
||||
@Override
|
||||
public String toString(Number object) {
|
||||
return formatter.formatCoinWithCode(Coin.valueOf(new Double((double) object).longValue()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateChartData() {
|
||||
volumeSeries.getData().setAll(model.volumeItems);
|
||||
|
||||
// At price chart we need to set the priceSeries new otherwise the lines are not rendered correctly
|
||||
// TODO should be fixed in candle chart
|
||||
priceSeries.getData().clear();
|
||||
priceSeries = new XYChart.Series<>();
|
||||
priceSeries.getData().setAll(model.priceItems);
|
||||
priceChart.getData().clear();
|
||||
priceChart.setData(FXCollections.observableArrayList(priceSeries));
|
||||
}
|
||||
|
||||
private void layoutChart() {
|
||||
UserThread.execute(() -> {
|
||||
if (volumeAxisYWidth > priceAxisYWidth) {
|
||||
priceChart.setPadding(new Insets(0, 0, 0, volumeAxisYWidth - priceAxisYWidth));
|
||||
volumeChart.setPadding(new Insets(0, 0, 0, 0));
|
||||
} else if (volumeAxisYWidth < priceAxisYWidth) {
|
||||
priceChart.setPadding(new Insets(0, 0, 0, 0));
|
||||
volumeChart.setPadding(new Insets(0, 0, 0, priceAxisYWidth - volumeAxisYWidth));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private StringConverter<Number> getTimeAxisStringConverter() {
|
||||
return new StringConverter<Number>() {
|
||||
@Override
|
||||
public String toString(Number object) {
|
||||
long index = new Double((double) object).longValue();
|
||||
long time = model.getTimeFromTickIndex(index);
|
||||
if (model.tickUnit.ordinal() <= TradesChartsViewModel.TickUnit.DAY.ordinal())
|
||||
return index % 4 == 0 ? formatter.formatDate(new Date(time)) : "";
|
||||
else
|
||||
return index % 3 == 0 ? formatter.formatTime(new Date(time)) : "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// CurrencyComboBox
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private HBox getToolBox() {
|
||||
Label currencyLabel = new Label("Currency:");
|
||||
currencyLabel.setPadding(new Insets(0, 4, 0, 0));
|
||||
|
||||
currencyComboBox = new ComboBox<>();
|
||||
currencyComboBox.setPromptText("Select currency");
|
||||
currencyComboBox.setConverter(new StringConverter<TradeCurrency>() {
|
||||
@Override
|
||||
public String toString(TradeCurrency tradeCurrency) {
|
||||
// http://boschista.deviantart.com/journal/Cool-ASCII-Symbols-214218618
|
||||
if (tradeCurrency instanceof FiatCurrency)
|
||||
return "★ " + tradeCurrency.getNameAndCode();
|
||||
else if (tradeCurrency instanceof CryptoCurrency)
|
||||
return "✦ " + tradeCurrency.getNameAndCode();
|
||||
else
|
||||
return "-";
|
||||
}
|
||||
|
||||
@Override
|
||||
public TradeCurrency fromString(String s) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
Pane spacer = new Pane();
|
||||
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||
|
||||
Label label = new Label("Interval:");
|
||||
label.setPadding(new Insets(0, 4, 0, 0));
|
||||
|
||||
toggleGroup = new ToggleGroup();
|
||||
ToggleButton month = getToggleButton("Month", TradesChartsViewModel.TickUnit.MONTH, toggleGroup, "toggle-left");
|
||||
ToggleButton week = getToggleButton("Week", TradesChartsViewModel.TickUnit.WEEK, toggleGroup, "toggle-center");
|
||||
ToggleButton day = getToggleButton("Day", TradesChartsViewModel.TickUnit.DAY, toggleGroup, "toggle-center");
|
||||
ToggleButton hour = getToggleButton("Hour", TradesChartsViewModel.TickUnit.HOUR, toggleGroup, "toggle-center");
|
||||
ToggleButton minute10 = getToggleButton("10 Minutes", TradesChartsViewModel.TickUnit.MINUTE_10, toggleGroup, "toggle-center");
|
||||
ToggleButton minute = getToggleButton("Minute", TradesChartsViewModel.TickUnit.MINUTE, toggleGroup, "toggle-right");
|
||||
|
||||
HBox hBox = new HBox();
|
||||
hBox.setSpacing(0);
|
||||
hBox.setPadding(new Insets(5, 9, -10, 10));
|
||||
hBox.setAlignment(Pos.CENTER_LEFT);
|
||||
hBox.getChildren().addAll(currencyLabel, currencyComboBox, spacer, label, month, week, day, hour, minute10, minute);
|
||||
return hBox;
|
||||
}
|
||||
|
||||
private ToggleButton getToggleButton(String label, TradesChartsViewModel.TickUnit tickUnit, ToggleGroup toggleGroup, String style) {
|
||||
ToggleButton toggleButton = new ToggleButton(label);
|
||||
toggleButton.setPadding(new Insets(0, 5, 0, 5));
|
||||
toggleButton.setUserData(tickUnit);
|
||||
toggleButton.setToggleGroup(toggleGroup);
|
||||
toggleButton.setId(style);
|
||||
return toggleButton;
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Table
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void createTable() {
|
||||
tableView = new TableView<>();
|
||||
tableView.setMinHeight(120);
|
||||
|
||||
// date
|
||||
TableColumn<TradeStatistics, TradeStatistics> dateColumn = new TableColumn<>("Date/Time");
|
||||
dateColumn.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue()));
|
||||
dateColumn.setCellFactory(
|
||||
new Callback<TableColumn<TradeStatistics, TradeStatistics>, TableCell<TradeStatistics,
|
||||
TradeStatistics>>() {
|
||||
@Override
|
||||
public TableCell<TradeStatistics, TradeStatistics> call(
|
||||
TableColumn<TradeStatistics, TradeStatistics> column) {
|
||||
return new TableCell<TradeStatistics, TradeStatistics>() {
|
||||
@Override
|
||||
public void updateItem(final TradeStatistics item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (item != null)
|
||||
setText(formatter.formatDateTime(item.getTradeDate()));
|
||||
else
|
||||
setText("");
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
dateColumn.setComparator((o1, o2) -> o1.getTradeDate().compareTo(o2.getTradeDate()));
|
||||
tableView.getColumns().add(dateColumn);
|
||||
|
||||
// amount
|
||||
TableColumn<TradeStatistics, TradeStatistics> amountColumn = new TableColumn<>("Amount in BTC");
|
||||
amountColumn.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue()));
|
||||
amountColumn.setCellFactory(
|
||||
new Callback<TableColumn<TradeStatistics, TradeStatistics>, TableCell<TradeStatistics,
|
||||
TradeStatistics>>() {
|
||||
@Override
|
||||
public TableCell<TradeStatistics, TradeStatistics> call(
|
||||
TableColumn<TradeStatistics, TradeStatistics> column) {
|
||||
return new TableCell<TradeStatistics, TradeStatistics>() {
|
||||
@Override
|
||||
public void updateItem(final TradeStatistics item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (item != null)
|
||||
setText(formatter.formatCoinWithCode(item.getTradeAmount()));
|
||||
else
|
||||
setText("");
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
amountColumn.setComparator((o1, o2) -> o1.getTradeAmount().compareTo(o2.getTradeAmount()));
|
||||
tableView.getColumns().add(amountColumn);
|
||||
|
||||
// price
|
||||
TableColumn<TradeStatistics, TradeStatistics> priceColumn = new TableColumn<>();
|
||||
priceColumn.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue()));
|
||||
priceColumn.textProperty().bind(priceColumnLabel);
|
||||
priceColumn.setCellFactory(
|
||||
new Callback<TableColumn<TradeStatistics, TradeStatistics>, TableCell<TradeStatistics,
|
||||
TradeStatistics>>() {
|
||||
@Override
|
||||
public TableCell<TradeStatistics, TradeStatistics> call(
|
||||
TableColumn<TradeStatistics, TradeStatistics> column) {
|
||||
return new TableCell<TradeStatistics, TradeStatistics>() {
|
||||
@Override
|
||||
public void updateItem(final TradeStatistics item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (item != null)
|
||||
setText(formatter.formatFiat(item.getTradePrice()));
|
||||
else
|
||||
setText("");
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
priceColumn.setComparator((o1, o2) -> o1.getTradePrice().compareTo(o2.getTradePrice()));
|
||||
tableView.getColumns().add(priceColumn);
|
||||
|
||||
// volume
|
||||
TableColumn<TradeStatistics, TradeStatistics> volumeColumn = new TableColumn<>();
|
||||
volumeColumn.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue()));
|
||||
volumeColumn.setText("Volume (BTC)");
|
||||
volumeColumn.setCellFactory(
|
||||
new Callback<TableColumn<TradeStatistics, TradeStatistics>, TableCell<TradeStatistics,
|
||||
TradeStatistics>>() {
|
||||
@Override
|
||||
public TableCell<TradeStatistics, TradeStatistics> call(
|
||||
TableColumn<TradeStatistics, TradeStatistics> column) {
|
||||
return new TableCell<TradeStatistics, TradeStatistics>() {
|
||||
@Override
|
||||
public void updateItem(final TradeStatistics item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (item != null)
|
||||
setText(formatter.formatFiatWithCode(item.getTradeVolume()));
|
||||
else
|
||||
setText("");
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
volumeColumn.setComparator((o1, o2) -> {
|
||||
final Fiat tradeVolume1 = o1.getTradeVolume();
|
||||
final Fiat tradeVolume2 = o2.getTradeVolume();
|
||||
return tradeVolume1 != null && tradeVolume2 != null ? tradeVolume1.compareTo(tradeVolume2) : 0;
|
||||
});
|
||||
tableView.getColumns().add(volumeColumn);
|
||||
|
||||
// direction
|
||||
TableColumn<TradeStatistics, TradeStatistics> directionColumn = new TableColumn<>("Trade type");
|
||||
directionColumn.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue()));
|
||||
directionColumn.setCellFactory(
|
||||
new Callback<TableColumn<TradeStatistics, TradeStatistics>, TableCell<TradeStatistics,
|
||||
TradeStatistics>>() {
|
||||
@Override
|
||||
public TableCell<TradeStatistics, TradeStatistics> call(
|
||||
TableColumn<TradeStatistics, TradeStatistics> column) {
|
||||
return new TableCell<TradeStatistics, TradeStatistics>() {
|
||||
@Override
|
||||
public void updateItem(final TradeStatistics item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (item != null)
|
||||
setText(formatter.getDirection(item.direction));
|
||||
else
|
||||
setText("");
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
directionColumn.setComparator((o1, o2) -> o1.direction.compareTo(o2.direction));
|
||||
tableView.getColumns().add(directionColumn);
|
||||
|
||||
tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
|
||||
Label placeholder = new Label("There is no data available");
|
||||
placeholder.setWrapText(true);
|
||||
tableView.setPlaceholder(placeholder);
|
||||
dateColumn.setSortType(TableColumn.SortType.DESCENDING);
|
||||
tableView.getSortOrder().add(dateColumn);
|
||||
}
|
||||
}
|
@ -0,0 +1,260 @@
|
||||
/*
|
||||
* This file is part of Bitsquare.
|
||||
*
|
||||
* Bitsquare is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bitsquare is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.bitsquare.gui.main.markets.trades;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.inject.Inject;
|
||||
import io.bitsquare.gui.common.model.ActivatableViewModel;
|
||||
import io.bitsquare.gui.main.markets.trades.charts.CandleData;
|
||||
import io.bitsquare.locale.CurrencyUtil;
|
||||
import io.bitsquare.locale.TradeCurrency;
|
||||
import io.bitsquare.trade.TradeStatistics;
|
||||
import io.bitsquare.trade.TradeStatisticsManager;
|
||||
import io.bitsquare.user.Preferences;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.SetChangeListener;
|
||||
import javafx.scene.chart.XYChart;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
class TradesChartsViewModel extends ActivatableViewModel {
|
||||
private static final Logger log = LoggerFactory.getLogger(TradesChartsViewModel.class);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Enum
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public enum TickUnit {
|
||||
MONTH,
|
||||
WEEK,
|
||||
DAY,
|
||||
HOUR,
|
||||
MINUTE_10,
|
||||
MINUTE
|
||||
}
|
||||
|
||||
private final TradeStatisticsManager tradeStatisticsManager;
|
||||
final Preferences preferences;
|
||||
|
||||
private final SetChangeListener<TradeStatistics> setChangeListener;
|
||||
final ObjectProperty<TradeCurrency> tradeCurrencyProperty = new SimpleObjectProperty<>();
|
||||
|
||||
final ObservableList<TradeStatistics> tradeStatisticsByCurrency = FXCollections.observableArrayList();
|
||||
ObservableList<XYChart.Data<Number, Number>> priceItems = FXCollections.observableArrayList();
|
||||
ObservableList<XYChart.Data<Number, Number>> volumeItems = FXCollections.observableArrayList();
|
||||
|
||||
TickUnit tickUnit = TickUnit.MONTH;
|
||||
int maxTicks = 30;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor, lifecycle
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Inject
|
||||
public TradesChartsViewModel(TradeStatisticsManager tradeStatisticsManager, Preferences preferences) {
|
||||
this.tradeStatisticsManager = tradeStatisticsManager;
|
||||
this.preferences = preferences;
|
||||
|
||||
setChangeListener = change -> updateChartData();
|
||||
|
||||
Optional<TradeCurrency> tradeCurrencyOptional = CurrencyUtil.getTradeCurrency(preferences.getTradeStatisticsScreenCurrencyCode());
|
||||
if (tradeCurrencyOptional.isPresent())
|
||||
tradeCurrencyProperty.set(tradeCurrencyOptional.get());
|
||||
else {
|
||||
tradeCurrencyProperty.set(CurrencyUtil.getDefaultTradeCurrency());
|
||||
}
|
||||
|
||||
tickUnit = TickUnit.values()[preferences.getTradeStatisticsTickUnitIndex()];
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
TradesChartsViewModel() {
|
||||
setChangeListener = null;
|
||||
preferences = null;
|
||||
tradeStatisticsManager = null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void activate() {
|
||||
tradeStatisticsManager.getObservableTradeStatisticsSet().addListener(setChangeListener);
|
||||
updateChartData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deactivate() {
|
||||
tradeStatisticsManager.getObservableTradeStatisticsSet().removeListener(setChangeListener);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// UI actions
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void onSetTradeCurrency(TradeCurrency tradeCurrency) {
|
||||
this.tradeCurrencyProperty.set(tradeCurrency);
|
||||
preferences.setTradeStatisticsScreenCurrencyCode(tradeCurrency.getCode());
|
||||
updateChartData();
|
||||
}
|
||||
|
||||
public void setTickUnit(TickUnit tickUnit) {
|
||||
this.tickUnit = tickUnit;
|
||||
preferences.setTradeStatisticsTickUnitIndex(tickUnit.ordinal());
|
||||
updateChartData();
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Getters
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public String getCurrencyCode() {
|
||||
return tradeCurrencyProperty.get().getCode();
|
||||
}
|
||||
|
||||
public ObservableList<TradeCurrency> getTradeCurrencies() {
|
||||
return preferences.getTradeCurrenciesAsObservable();
|
||||
}
|
||||
|
||||
public TradeCurrency getTradeCurrency() {
|
||||
return tradeCurrencyProperty.get();
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Private
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
private void updateChartData() {
|
||||
tradeStatisticsByCurrency.setAll(tradeStatisticsManager.getObservableTradeStatisticsSet().stream()
|
||||
.filter(e -> e.currency.equals(getCurrencyCode()))
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
// Get all entries for the defined time interval
|
||||
Map<Long, Set<TradeStatistics>> itemsPerInterval = new HashMap<>();
|
||||
tradeStatisticsByCurrency.stream().forEach(e -> {
|
||||
Set<TradeStatistics> set;
|
||||
final long time = getTickFromTime(e.tradeDate, tickUnit);
|
||||
final long now = getTickFromTime(new Date().getTime(), tickUnit);
|
||||
long index = maxTicks - (now - time);
|
||||
if (itemsPerInterval.containsKey(index)) {
|
||||
set = itemsPerInterval.get(index);
|
||||
} else {
|
||||
set = new HashSet<>();
|
||||
itemsPerInterval.put(index, set);
|
||||
}
|
||||
set.add(e);
|
||||
});
|
||||
|
||||
// create CandleData for defined time interval
|
||||
List<CandleData> candleDataList = itemsPerInterval.entrySet().stream()
|
||||
.map(entry -> getCandleData(entry.getKey(), entry.getValue()))
|
||||
.collect(Collectors.toList());
|
||||
candleDataList.sort((o1, o2) -> (o1.tick < o2.tick ? -1 : (o1.tick == o2.tick ? 0 : 1)));
|
||||
|
||||
priceItems.setAll(candleDataList.stream()
|
||||
.map(e -> new XYChart.Data<Number, Number>(e.tick, e.open, e))
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
volumeItems.setAll(candleDataList.stream()
|
||||
.map(e -> new XYChart.Data<Number, Number>(e.tick, e.accumulatedAmount, e))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
CandleData getCandleData(long tick, Set<TradeStatistics> set) {
|
||||
long open = 0;
|
||||
long close = 0;
|
||||
long high = 0;
|
||||
long low = 0;
|
||||
long accumulatedVolume = 0;
|
||||
long accumulatedAmount = 0;
|
||||
|
||||
for (TradeStatistics item : set) {
|
||||
final long tradePriceAsLong = item.tradePrice;
|
||||
low = (low != 0) ? Math.min(low, tradePriceAsLong) : tradePriceAsLong;
|
||||
high = (high != 0) ? Math.max(high, tradePriceAsLong) : tradePriceAsLong;
|
||||
accumulatedVolume += (item.getTradeVolume() != null) ? item.getTradeVolume().value : 0;
|
||||
accumulatedAmount += item.tradeAmount;
|
||||
}
|
||||
long averagePrice = Math.round(accumulatedVolume * Coin.COIN.value / accumulatedAmount);
|
||||
|
||||
List<TradeStatistics> list = new ArrayList<>(set);
|
||||
list.sort((o1, o2) -> (o1.tradeDate < o2.tradeDate ? -1 : (o1.tradeDate == o2.tradeDate ? 0 : 1)));
|
||||
if (list.size() > 0) {
|
||||
open = list.get(0).tradePrice;
|
||||
close = list.get(list.size() - 1).tradePrice;
|
||||
}
|
||||
boolean isBullish = close > open;
|
||||
return new CandleData(tick, open, close, high, low, averagePrice, accumulatedAmount, accumulatedVolume, isBullish);
|
||||
}
|
||||
|
||||
long getTickFromTime(long tradeDateAsTime, TickUnit tickUnit) {
|
||||
switch (tickUnit) {
|
||||
case MONTH:
|
||||
return TimeUnit.MILLISECONDS.toDays(tradeDateAsTime) / 31;
|
||||
case WEEK:
|
||||
return TimeUnit.MILLISECONDS.toDays(tradeDateAsTime) / 7;
|
||||
case DAY:
|
||||
return TimeUnit.MILLISECONDS.toDays(tradeDateAsTime);
|
||||
case HOUR:
|
||||
return TimeUnit.MILLISECONDS.toHours(tradeDateAsTime);
|
||||
case MINUTE_10:
|
||||
return TimeUnit.MILLISECONDS.toMinutes(tradeDateAsTime) / 10;
|
||||
case MINUTE:
|
||||
return TimeUnit.MILLISECONDS.toMinutes(tradeDateAsTime);
|
||||
default:
|
||||
return tradeDateAsTime;
|
||||
}
|
||||
}
|
||||
|
||||
long getTimeFromTick(long tick, TickUnit tickUnit) {
|
||||
switch (tickUnit) {
|
||||
case MONTH:
|
||||
return TimeUnit.DAYS.toMillis(tick) * 31;
|
||||
case WEEK:
|
||||
return TimeUnit.DAYS.toMillis(tick) * 7;
|
||||
case DAY:
|
||||
return TimeUnit.DAYS.toMillis(tick);
|
||||
case HOUR:
|
||||
return TimeUnit.HOURS.toMillis(tick);
|
||||
case MINUTE_10:
|
||||
return TimeUnit.MINUTES.toMillis(tick) * 10;
|
||||
case MINUTE:
|
||||
return TimeUnit.MINUTES.toMillis(tick);
|
||||
default:
|
||||
return tick;
|
||||
}
|
||||
}
|
||||
|
||||
long getTimeFromTickIndex(long index) {
|
||||
long now = getTickFromTime(new Date().getTime(), tickUnit);
|
||||
long tick = now - (maxTicks - index);
|
||||
return getTimeFromTick(tick, tickUnit);
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* This file is part of Bitsquare.
|
||||
*
|
||||
* Bitsquare is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bitsquare is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package io.bitsquare.gui.main.markets.trades.charts;
|
||||
|
||||
public class CandleData {
|
||||
public final long tick; // Is the time tick in the chosen time interval
|
||||
public final long open;
|
||||
public final long close;
|
||||
public final long high;
|
||||
public final long low;
|
||||
public final long average;
|
||||
public final long accumulatedAmount;
|
||||
public final long accumulatedVolume;
|
||||
public final boolean isBullish;
|
||||
|
||||
public CandleData(long tick, long open, long close, long high, long low, long average, long accumulatedAmount, long accumulatedVolume, boolean isBullish) {
|
||||
this.tick = tick;
|
||||
this.open = open;
|
||||
this.close = close;
|
||||
this.high = high;
|
||||
this.low = low;
|
||||
this.average = average;
|
||||
this.accumulatedAmount = accumulatedAmount;
|
||||
this.accumulatedVolume = accumulatedVolume;
|
||||
this.isBullish = isBullish;
|
||||
}
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright (c) 2008, 2014, Oracle and/or its affiliates.
|
||||
* All rights reserved. Use is subject to license terms.
|
||||
*
|
||||
* This file is available and licensed under the following license:
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
*
|
||||
* - Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* - Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in
|
||||
* the documentation and/or other materials provided with the distribution.
|
||||
* - Neither the name of Oracle Corporation nor the names of its
|
||||
* contributors may be used to endorse or promote products derived
|
||||
* from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package io.bitsquare.gui.main.markets.trades.charts.price;
|
||||
|
||||
import io.bitsquare.gui.main.markets.trades.charts.CandleData;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.shape.Line;
|
||||
import javafx.util.StringConverter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Candle node used for drawing a candle
|
||||
*/
|
||||
public class Candle extends Group {
|
||||
private static final Logger log = LoggerFactory.getLogger(Candle.class);
|
||||
|
||||
private String seriesStyleClass;
|
||||
private String dataStyleClass;
|
||||
private final TooltipContent tooltipContent;
|
||||
private final Line highLowLine = new Line();
|
||||
private final Region bar = new Region();
|
||||
|
||||
private boolean openAboveClose = true;
|
||||
private Tooltip tooltip = new Tooltip();
|
||||
private double closeOffset;
|
||||
|
||||
Candle(String seriesStyleClass, String dataStyleClass, StringConverter<Number> priceStringConverter) {
|
||||
this.seriesStyleClass = seriesStyleClass;
|
||||
this.dataStyleClass = dataStyleClass;
|
||||
|
||||
setAutoSizeChildren(false);
|
||||
getChildren().addAll(highLowLine, bar);
|
||||
getStyleClass().setAll("candlestick-candle", seriesStyleClass, dataStyleClass);
|
||||
updateStyleClasses();
|
||||
|
||||
tooltipContent = new TooltipContent(priceStringConverter);
|
||||
tooltip.setGraphic(tooltipContent);
|
||||
Tooltip.install(this, tooltip);
|
||||
}
|
||||
|
||||
public void setSeriesAndDataStyleClasses(String seriesStyleClass, String dataStyleClass) {
|
||||
this.seriesStyleClass = seriesStyleClass;
|
||||
this.dataStyleClass = dataStyleClass;
|
||||
getStyleClass().setAll("candlestick-candle", seriesStyleClass, dataStyleClass);
|
||||
updateStyleClasses();
|
||||
}
|
||||
|
||||
public void update(double closeOffset, double highOffset, double lowOffset, double candleWidth) {
|
||||
this.closeOffset = closeOffset;
|
||||
openAboveClose = closeOffset > 0;
|
||||
updateStyleClasses();
|
||||
highLowLine.setStartY(highOffset);
|
||||
highLowLine.setEndY(lowOffset);
|
||||
if (openAboveClose) {
|
||||
bar.resizeRelocate(-candleWidth / 2, 0, candleWidth, Math.max(5, closeOffset));
|
||||
} else {
|
||||
bar.resizeRelocate(-candleWidth / 2, closeOffset, candleWidth, Math.max(5, closeOffset * -1));
|
||||
}
|
||||
}
|
||||
|
||||
public void updateTooltip(CandleData candleData) {
|
||||
tooltipContent.update(candleData);
|
||||
}
|
||||
|
||||
private void updateStyleClasses() {
|
||||
String style = openAboveClose ? "open-above-close" : "close-above-open";
|
||||
if (closeOffset == 0)
|
||||
style = "empty";
|
||||
|
||||
highLowLine.getStyleClass().setAll("candlestick-line", seriesStyleClass, dataStyleClass,
|
||||
style);
|
||||
|
||||
bar.getStyleClass().setAll("candlestick-bar", seriesStyleClass, dataStyleClass,
|
||||
style);
|
||||
}
|
||||
}
|
@ -0,0 +1,316 @@
|
||||
/*
|
||||
* Copyright (c) 2008, 2014, Oracle and/or its affiliates.
|
||||
* All rights reserved. Use is subject to license terms.
|
||||
*
|
||||
* This file is available and licensed under the following license:
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
*
|
||||
* - Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* - Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in
|
||||
* the documentation and/or other materials provided with the distribution.
|
||||
* - Neither the name of Oracle Corporation nor the names of its
|
||||
* contributors may be used to endorse or promote products derived
|
||||
* from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package io.bitsquare.gui.main.markets.trades.charts.price;
|
||||
|
||||
import io.bitsquare.gui.main.markets.trades.charts.CandleData;
|
||||
import javafx.animation.FadeTransition;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.chart.Axis;
|
||||
import javafx.scene.chart.NumberAxis;
|
||||
import javafx.scene.chart.XYChart;
|
||||
import javafx.scene.shape.LineTo;
|
||||
import javafx.scene.shape.MoveTo;
|
||||
import javafx.scene.shape.Path;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A candlestick chart is a style of bar-chart used primarily to describe price movements of a security, derivative,
|
||||
* or currency over time.
|
||||
* <p>
|
||||
* The Data Y value is used for the opening price and then the close, high and low values are stored in the Data's
|
||||
* extra value property using a CandleStickExtraValues object.
|
||||
*/
|
||||
public class CandleStickChart extends XYChart<Number, Number> {
|
||||
private static final Logger log = LoggerFactory.getLogger(CandleStickChart.class);
|
||||
|
||||
private StringConverter<Number> priceStringConverter;
|
||||
|
||||
// -------------- CONSTRUCTORS ----------------------------------------------
|
||||
|
||||
/**
|
||||
* Construct a new CandleStickChart with the given axis.
|
||||
*
|
||||
* @param xAxis The x axis to use
|
||||
* @param yAxis The y axis to use
|
||||
*/
|
||||
public CandleStickChart(Axis<Number> xAxis, Axis<Number> yAxis) {
|
||||
super(xAxis, yAxis);
|
||||
}
|
||||
|
||||
// -------------- METHODS ------------------------------------------------------------------------------------------
|
||||
|
||||
public final void setToolTipStringConverter(StringConverter<Number> priceStringConverter) {
|
||||
this.priceStringConverter = priceStringConverter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to update and layout the content for the plot
|
||||
*/
|
||||
@Override
|
||||
protected void layoutPlotChildren() {
|
||||
// we have nothing to layout if no data is present
|
||||
if (getData() == null) {
|
||||
return;
|
||||
}
|
||||
// update candle positions
|
||||
for (int seriesIndex = 0; seriesIndex < getData().size(); seriesIndex++) {
|
||||
XYChart.Series<Number, Number> series = getData().get(seriesIndex);
|
||||
Iterator<XYChart.Data<Number, Number>> iter = getDisplayedDataIterator(series);
|
||||
Path seriesPath = null;
|
||||
if (series.getNode() instanceof Path) {
|
||||
seriesPath = (Path) series.getNode();
|
||||
seriesPath.getElements().clear();
|
||||
}
|
||||
while (iter.hasNext()) {
|
||||
XYChart.Data<Number, Number> item = iter.next();
|
||||
double x = getXAxis().getDisplayPosition(getCurrentDisplayedXValue(item));
|
||||
double y = getYAxis().getDisplayPosition(getCurrentDisplayedYValue(item));
|
||||
Node itemNode = item.getNode();
|
||||
CandleData candleData = (CandleData) item.getExtraValue();
|
||||
if (itemNode instanceof Candle && candleData != null) {
|
||||
Candle candle = (Candle) itemNode;
|
||||
|
||||
double close = getYAxis().getDisplayPosition(candleData.close);
|
||||
double high = getYAxis().getDisplayPosition(candleData.high);
|
||||
double low = getYAxis().getDisplayPosition(candleData.low);
|
||||
// calculate candle width
|
||||
double candleWidth = -1;
|
||||
if (getXAxis() instanceof NumberAxis) {
|
||||
NumberAxis xa = (NumberAxis) getXAxis();
|
||||
candleWidth = xa.getDisplayPosition(xa.getTickUnit()) * 0.90; // use 90% width between ticks
|
||||
}
|
||||
// update candle
|
||||
candle.update(close - y, high - y, low - y, candleWidth);
|
||||
candle.updateTooltip(candleData);
|
||||
|
||||
// position the candle
|
||||
candle.setLayoutX(x);
|
||||
candle.setLayoutY(y);
|
||||
}
|
||||
if (seriesPath != null && candleData != null) {
|
||||
final double displayPosition = getYAxis().getDisplayPosition(candleData.average);
|
||||
if (seriesPath.getElements().isEmpty())
|
||||
seriesPath.getElements().add(new MoveTo(x, displayPosition));
|
||||
else
|
||||
seriesPath.getElements().add(new LineTo(x, displayPosition));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dataItemChanged(XYChart.Data<Number, Number> item) {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dataItemAdded(XYChart.Series<Number, Number> series, int itemIndex, XYChart.Data<Number, Number> item) {
|
||||
Node candle = createCandle(getData().indexOf(series), item, itemIndex);
|
||||
if (getPlotChildren().contains(candle))
|
||||
getPlotChildren().remove(candle);
|
||||
|
||||
if (shouldAnimate()) {
|
||||
candle.setOpacity(0);
|
||||
getPlotChildren().add(candle);
|
||||
// fade in new candle
|
||||
FadeTransition ft = new FadeTransition(Duration.millis(500), candle);
|
||||
ft.setToValue(1);
|
||||
ft.play();
|
||||
} else {
|
||||
getPlotChildren().add(candle);
|
||||
}
|
||||
// always draw average line on top
|
||||
|
||||
if (series.getNode() instanceof Path) {
|
||||
Path seriesPath = (Path) series.getNode();
|
||||
seriesPath.toFront();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dataItemRemoved(XYChart.Data<Number, Number> item, XYChart.Series<Number, Number> series) {
|
||||
if (series.getNode() instanceof Path) {
|
||||
Path seriesPath = (Path) series.getNode();
|
||||
seriesPath.getElements().clear();
|
||||
}
|
||||
|
||||
final Node node = item.getNode();
|
||||
if (shouldAnimate()) {
|
||||
// fade out old candle
|
||||
FadeTransition ft = new FadeTransition(Duration.millis(500), node);
|
||||
ft.setToValue(0);
|
||||
ft.setOnFinished((ActionEvent actionEvent) -> {
|
||||
getPlotChildren().remove(node);
|
||||
});
|
||||
ft.play();
|
||||
} else {
|
||||
getPlotChildren().remove(node);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void seriesAdded(XYChart.Series<Number, Number> series, int seriesIndex) {
|
||||
// handle any data already in series
|
||||
for (int j = 0; j < series.getData().size(); j++) {
|
||||
XYChart.Data item = series.getData().get(j);
|
||||
Node candle = createCandle(seriesIndex, item, j);
|
||||
|
||||
if (!getPlotChildren().contains(candle)) {
|
||||
getPlotChildren().add(candle);
|
||||
if (shouldAnimate()) {
|
||||
candle.setOpacity(0);
|
||||
FadeTransition ft = new FadeTransition(Duration.millis(500), candle);
|
||||
ft.setToValue(1);
|
||||
ft.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
Path seriesPath = new Path();
|
||||
seriesPath.getStyleClass().setAll("candlestick-average-line", "series" + seriesIndex);
|
||||
series.setNode(seriesPath);
|
||||
|
||||
if (!getPlotChildren().contains(seriesPath)) {
|
||||
getPlotChildren().add(seriesPath);
|
||||
if (shouldAnimate()) {
|
||||
seriesPath.setOpacity(0);
|
||||
FadeTransition ft = new FadeTransition(Duration.millis(500), seriesPath);
|
||||
ft.setToValue(1);
|
||||
ft.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void seriesRemoved(XYChart.Series<Number, Number> series) {
|
||||
// remove all candle nodes
|
||||
for (XYChart.Data<Number, Number> d : series.getData()) {
|
||||
final Node candle = d.getNode();
|
||||
if (shouldAnimate()) {
|
||||
FadeTransition ft = new FadeTransition(Duration.millis(500), candle);
|
||||
ft.setToValue(0);
|
||||
ft.setOnFinished((ActionEvent actionEvent) -> {
|
||||
getPlotChildren().remove(candle);
|
||||
});
|
||||
ft.play();
|
||||
} else {
|
||||
getPlotChildren().remove(candle);
|
||||
}
|
||||
}
|
||||
if (series.getNode() instanceof Path) {
|
||||
Path seriesPath = (Path) series.getNode();
|
||||
if (shouldAnimate()) {
|
||||
FadeTransition ft = new FadeTransition(Duration.millis(500), seriesPath);
|
||||
ft.setToValue(0);
|
||||
ft.setOnFinished((ActionEvent actionEvent) -> {
|
||||
getPlotChildren().remove(seriesPath);
|
||||
seriesPath.getElements().clear();
|
||||
});
|
||||
ft.play();
|
||||
} else {
|
||||
getPlotChildren().remove(seriesPath);
|
||||
seriesPath.getElements().clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Candle node to represent a single data item
|
||||
*
|
||||
* @param seriesIndex The index of the series the data item is in
|
||||
* @param item The data item to create node for
|
||||
* @param itemIndex The index of the data item in the series
|
||||
* @return New candle node to represent the give data item
|
||||
*/
|
||||
private Node createCandle(int seriesIndex, final XYChart.Data item, int itemIndex) {
|
||||
Node candle = item.getNode();
|
||||
// check if candle has already been created
|
||||
if (candle instanceof Candle) {
|
||||
((Candle) candle).setSeriesAndDataStyleClasses("series" + seriesIndex, "data" + itemIndex);
|
||||
} else {
|
||||
candle = new Candle("series" + seriesIndex, "data" + itemIndex, priceStringConverter);
|
||||
item.setNode(candle);
|
||||
}
|
||||
return candle;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called when the range has been invalidated and we need to update it. If the axis are auto
|
||||
* ranging then we compile a list of all data that the given axis has to plot and call invalidateRange() on the
|
||||
* axis passing it that data.
|
||||
*/
|
||||
@Override
|
||||
protected void updateAxisRange() {
|
||||
// For candle stick chart we need to override this method as we need to let the axis know that they need to be able
|
||||
// to cover the whole area occupied by the high to low range not just its center data value
|
||||
final Axis<Number> xa = getXAxis();
|
||||
final Axis<Number> ya = getYAxis();
|
||||
List<Number> xData = null;
|
||||
List<Number> yData = null;
|
||||
if (xa.isAutoRanging()) {
|
||||
xData = new ArrayList<>();
|
||||
}
|
||||
if (ya.isAutoRanging()) {
|
||||
yData = new ArrayList<>();
|
||||
}
|
||||
if (xData != null || yData != null) {
|
||||
for (XYChart.Series<Number, Number> series : getData()) {
|
||||
for (XYChart.Data<Number, Number> data : series.getData()) {
|
||||
if (xData != null) {
|
||||
xData.add(data.getXValue());
|
||||
}
|
||||
if (yData != null) {
|
||||
if (data.getExtraValue() instanceof CandleData) {
|
||||
CandleData candleData = (CandleData) data.getExtraValue();
|
||||
yData.add(candleData.high);
|
||||
yData.add(candleData.low);
|
||||
} else {
|
||||
yData.add(data.getYValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (xData != null) {
|
||||
xa.invalidateRange(xData);
|
||||
}
|
||||
if (yData != null) {
|
||||
ya.invalidateRange(yData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (c) 2008, 2014, Oracle and/or its affiliates.
|
||||
* All rights reserved. Use is subject to license terms.
|
||||
*
|
||||
* This file is available and licensed under the following license:
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
*
|
||||
* - Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* - Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in
|
||||
* the documentation and/or other materials provided with the distribution.
|
||||
* - Neither the name of Oracle Corporation nor the names of its
|
||||
* contributors may be used to endorse or promote products derived
|
||||
* from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package io.bitsquare.gui.main.markets.trades.charts.price;
|
||||
|
||||
import io.bitsquare.gui.main.markets.trades.charts.CandleData;
|
||||
import io.bitsquare.gui.util.Layout;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
/**
|
||||
* The content for Candle tool tips
|
||||
*/
|
||||
public class TooltipContent extends GridPane {
|
||||
private final StringConverter<Number> priceStringConverter;
|
||||
private final Label openValue = new Label();
|
||||
private final Label closeValue = new Label();
|
||||
private final Label highValue = new Label();
|
||||
private final Label lowValue = new Label();
|
||||
private final Label averageValue = new Label();
|
||||
|
||||
TooltipContent(StringConverter<Number> priceStringConverter) {
|
||||
this.priceStringConverter = priceStringConverter;
|
||||
|
||||
setHgap(Layout.GRID_GAP);
|
||||
setVgap(2);
|
||||
|
||||
Label open = new Label("Open:");
|
||||
Label close = new Label("Close:");
|
||||
Label high = new Label("High:");
|
||||
Label low = new Label("Low:");
|
||||
Label average = new Label("Average:");
|
||||
/* open.getStyleClass().add("candlestick-tooltip-label");
|
||||
close.getStyleClass().add("candlestick-tooltip-label");
|
||||
high.getStyleClass().add("candlestick-tooltip-label");
|
||||
low.getStyleClass().add("candlestick-tooltip-label");*/
|
||||
setConstraints(open, 0, 0);
|
||||
setConstraints(openValue, 1, 0);
|
||||
setConstraints(close, 0, 1);
|
||||
setConstraints(closeValue, 1, 1);
|
||||
setConstraints(high, 0, 2);
|
||||
setConstraints(highValue, 1, 2);
|
||||
setConstraints(low, 0, 3);
|
||||
setConstraints(lowValue, 1, 3);
|
||||
setConstraints(average, 0, 4);
|
||||
setConstraints(averageValue, 1, 4);
|
||||
getChildren().addAll(open, openValue, close, closeValue, high, highValue, low, lowValue, average, averageValue);
|
||||
}
|
||||
|
||||
public void update(CandleData candleData) {
|
||||
openValue.setText(priceStringConverter.toString(candleData.open));
|
||||
closeValue.setText(priceStringConverter.toString(candleData.close));
|
||||
highValue.setText(priceStringConverter.toString(candleData.high));
|
||||
lowValue.setText(priceStringConverter.toString(candleData.low));
|
||||
averageValue.setText(priceStringConverter.toString(candleData.average));
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* This file is part of Bitsquare.
|
||||
*
|
||||
* Bitsquare is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bitsquare is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.bitsquare.gui.main.markets.trades.charts.volume;
|
||||
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.util.StringConverter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class VolumeBar extends Group {
|
||||
private static final Logger log = LoggerFactory.getLogger(VolumeBar.class);
|
||||
|
||||
private String seriesStyleClass;
|
||||
private String dataStyleClass;
|
||||
private final StringConverter<Number> volumeStringConverter;
|
||||
|
||||
private final Region bar = new Region();
|
||||
private final Tooltip tooltip;
|
||||
|
||||
VolumeBar(String seriesStyleClass, String dataStyleClass, StringConverter<Number> volumeStringConverter) {
|
||||
this.seriesStyleClass = seriesStyleClass;
|
||||
this.dataStyleClass = dataStyleClass;
|
||||
this.volumeStringConverter = volumeStringConverter;
|
||||
|
||||
setAutoSizeChildren(false);
|
||||
getChildren().add(bar);
|
||||
updateStyleClasses();
|
||||
tooltip = new Tooltip();
|
||||
Tooltip.install(this, tooltip);
|
||||
}
|
||||
|
||||
public void setSeriesAndDataStyleClasses(String seriesStyleClass, String dataStyleClass) {
|
||||
this.seriesStyleClass = seriesStyleClass;
|
||||
this.dataStyleClass = dataStyleClass;
|
||||
updateStyleClasses();
|
||||
}
|
||||
|
||||
public void update(double height, double candleWidth, double accumulatedAmount) {
|
||||
bar.resizeRelocate(-candleWidth / 2, 0, candleWidth, height);
|
||||
tooltip.setText("Volume: " + volumeStringConverter.toString(accumulatedAmount));
|
||||
}
|
||||
|
||||
private void updateStyleClasses() {
|
||||
bar.getStyleClass().setAll("volume-bar", seriesStyleClass, dataStyleClass, "bg");
|
||||
}
|
||||
}
|
@ -0,0 +1,195 @@
|
||||
/*
|
||||
* This file is part of Bitsquare.
|
||||
*
|
||||
* Bitsquare is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bitsquare is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package io.bitsquare.gui.main.markets.trades.charts.volume;
|
||||
|
||||
import io.bitsquare.gui.main.markets.trades.charts.CandleData;
|
||||
import io.bitsquare.gui.main.markets.trades.charts.price.CandleStickChart;
|
||||
import javafx.animation.FadeTransition;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.chart.Axis;
|
||||
import javafx.scene.chart.NumberAxis;
|
||||
import javafx.scene.chart.XYChart;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class VolumeChart extends XYChart<Number, Number> {
|
||||
private static final Logger log = LoggerFactory.getLogger(CandleStickChart.class);
|
||||
|
||||
private StringConverter<Number> toolTipStringConverter;
|
||||
|
||||
public VolumeChart(Axis<Number> xAxis, Axis<Number> yAxis) {
|
||||
super(xAxis, yAxis);
|
||||
}
|
||||
|
||||
|
||||
public final void setToolTipStringConverter(StringConverter<Number> toolTipStringConverter) {
|
||||
this.toolTipStringConverter = toolTipStringConverter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutPlotChildren() {
|
||||
if (getData() == null) {
|
||||
return;
|
||||
}
|
||||
for (int seriesIndex = 0; seriesIndex < getData().size(); seriesIndex++) {
|
||||
XYChart.Series<Number, Number> series = getData().get(seriesIndex);
|
||||
Iterator<XYChart.Data<Number, Number>> iter = getDisplayedDataIterator(series);
|
||||
while (iter.hasNext()) {
|
||||
XYChart.Data<Number, Number> item = iter.next();
|
||||
double x = getXAxis().getDisplayPosition(getCurrentDisplayedXValue(item));
|
||||
double y = getYAxis().getDisplayPosition(getCurrentDisplayedYValue(item));
|
||||
Node itemNode = item.getNode();
|
||||
CandleData candleData = (CandleData) item.getExtraValue();
|
||||
if (itemNode instanceof VolumeBar && candleData != null) {
|
||||
VolumeBar volumeBar = (VolumeBar) itemNode;
|
||||
double candleWidth = -1;
|
||||
if (getXAxis() instanceof NumberAxis) {
|
||||
NumberAxis xa = (NumberAxis) getXAxis();
|
||||
candleWidth = xa.getDisplayPosition(xa.getTickUnit()) * 0.90; // use 90% width between ticks
|
||||
}
|
||||
|
||||
// 97 is visible chart data height if chart height is 140.
|
||||
// So we subtract 43 form the height to get the height for the bar to the bottom.
|
||||
// Did not find a way how to request the chart data height
|
||||
final double height = getHeight() - 43;
|
||||
double upperYPos = Math.min(height - 5, y); // We want min 5px height to allow tooltips
|
||||
volumeBar.update(height - upperYPos, candleWidth, candleData.accumulatedAmount);
|
||||
volumeBar.setLayoutX(x);
|
||||
volumeBar.setLayoutY(upperYPos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dataItemChanged(XYChart.Data<Number, Number> item) {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dataItemAdded(XYChart.Series<Number, Number> series, int itemIndex, XYChart.Data<Number, Number> item) {
|
||||
Node volumeBar = createCandle(getData().indexOf(series), item, itemIndex);
|
||||
if (getPlotChildren().contains(volumeBar))
|
||||
getPlotChildren().remove(volumeBar);
|
||||
|
||||
if (shouldAnimate()) {
|
||||
volumeBar.setOpacity(0);
|
||||
getPlotChildren().add(volumeBar);
|
||||
FadeTransition ft = new FadeTransition(Duration.millis(500), volumeBar);
|
||||
ft.setToValue(1);
|
||||
ft.play();
|
||||
} else {
|
||||
getPlotChildren().add(volumeBar);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void dataItemRemoved(XYChart.Data<Number, Number> item, XYChart.Series<Number, Number> series) {
|
||||
final Node node = item.getNode();
|
||||
if (shouldAnimate()) {
|
||||
FadeTransition ft = new FadeTransition(Duration.millis(500), node);
|
||||
ft.setToValue(0);
|
||||
ft.setOnFinished((ActionEvent actionEvent) -> {
|
||||
getPlotChildren().remove(node);
|
||||
});
|
||||
ft.play();
|
||||
} else {
|
||||
getPlotChildren().remove(node);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void seriesAdded(XYChart.Series<Number, Number> series, int seriesIndex) {
|
||||
for (int j = 0; j < series.getData().size(); j++) {
|
||||
XYChart.Data item = series.getData().get(j);
|
||||
Node volumeBar = createCandle(seriesIndex, item, j);
|
||||
if (shouldAnimate()) {
|
||||
volumeBar.setOpacity(0);
|
||||
getPlotChildren().add(volumeBar);
|
||||
FadeTransition ft = new FadeTransition(Duration.millis(500), volumeBar);
|
||||
ft.setToValue(1);
|
||||
ft.play();
|
||||
} else {
|
||||
getPlotChildren().add(volumeBar);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void seriesRemoved(XYChart.Series<Number, Number> series) {
|
||||
for (XYChart.Data<Number, Number> d : series.getData()) {
|
||||
final Node volumeBar = d.getNode();
|
||||
if (shouldAnimate()) {
|
||||
FadeTransition ft = new FadeTransition(Duration.millis(500), volumeBar);
|
||||
ft.setToValue(0);
|
||||
ft.setOnFinished((ActionEvent actionEvent) -> {
|
||||
getPlotChildren().remove(volumeBar);
|
||||
});
|
||||
ft.play();
|
||||
} else {
|
||||
getPlotChildren().remove(volumeBar);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Node createCandle(int seriesIndex, final XYChart.Data item, int itemIndex) {
|
||||
Node volumeBar = item.getNode();
|
||||
if (volumeBar instanceof VolumeBar) {
|
||||
((VolumeBar) volumeBar).setSeriesAndDataStyleClasses("series" + seriesIndex, "data" + itemIndex);
|
||||
} else {
|
||||
volumeBar = new VolumeBar("series" + seriesIndex, "data" + itemIndex, toolTipStringConverter);
|
||||
item.setNode(volumeBar);
|
||||
}
|
||||
return volumeBar;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateAxisRange() {
|
||||
final Axis<Number> xa = getXAxis();
|
||||
final Axis<Number> ya = getYAxis();
|
||||
List<Number> xData = null;
|
||||
List<Number> yData = null;
|
||||
if (xa.isAutoRanging()) {
|
||||
xData = new ArrayList<>();
|
||||
}
|
||||
if (ya.isAutoRanging())
|
||||
yData = new ArrayList<>();
|
||||
if (xData != null || yData != null) {
|
||||
for (XYChart.Series<Number, Number> series : getData()) {
|
||||
for (XYChart.Data<Number, Number> data : series.getData()) {
|
||||
if (xData != null) {
|
||||
xData.add(data.getXValue());
|
||||
}
|
||||
if (yData != null)
|
||||
yData.add(data.getYValue());
|
||||
}
|
||||
}
|
||||
if (xData != null) {
|
||||
xa.invalidateRange(xData);
|
||||
}
|
||||
if (yData != null) {
|
||||
ya.invalidateRange(yData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -208,8 +208,8 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
|
||||
|
||||
balanceTextField.setTargetAmount(model.dataModel.totalToPayAsCoin.get());
|
||||
|
||||
if (DevFlags.STRESS_TEST_MODE)
|
||||
UserThread.runAfter(this::onShowPayFundsScreen, 200, TimeUnit.MILLISECONDS);
|
||||
// if (DevFlags.STRESS_TEST_MODE)
|
||||
// UserThread.runAfter(this::onShowPayFundsScreen, 200, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -165,8 +165,8 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
|
||||
if (DevFlags.DEV_MODE) {
|
||||
amount.set("0.0001");
|
||||
minAmount.set(amount.get());
|
||||
price.set("0.02");
|
||||
volume.set("0.04");
|
||||
price.set("600");
|
||||
volume.set("0.12");
|
||||
|
||||
setAmountToModel();
|
||||
setMinAmountToModel();
|
||||
@ -352,18 +352,10 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
|
||||
private void applyCurrencyCode(String newValue) {
|
||||
String key = "ETH-ETHC-Warning";
|
||||
if (preferences.showAgain(key) && new Date().before(new Date(2016 - 1900, Calendar.AUGUST, 30))) {
|
||||
if (newValue.equals("ETH")) {
|
||||
new Popup().information("The EHT/ETHC fork situation carries considerable risks.\n" +
|
||||
"Be sure you fully understand the situation and check out the information on the \"Ethereum Classic\" and \"Ethereum\" project web pages.")
|
||||
.closeButtonText("I understand")
|
||||
.onAction(() -> Utilities.openWebPage("https://www.ethereum.org/"))
|
||||
.actionButtonText("Open Ethereum web page")
|
||||
.dontShowAgainId(key, preferences)
|
||||
.show();
|
||||
} else if (newValue.equals("ETHC")) {
|
||||
new Popup().information("The EHT/ETHC fork situation carries considerable risks.\n" +
|
||||
if (newValue.equals("ETHC")) {
|
||||
new Popup().information("The EHT/ETC fork situation carries considerable risks.\n" +
|
||||
"Be sure you fully understand the situation and check out the information on the \"Ethereum Classic\" and \"Ethereum\" project web pages.\n\n" +
|
||||
"Please note, that the price is denominated as ETHC/BTC not BTC/ETHC!")
|
||||
"Please note, that the price is denominated as ETC/BTC not BTC/ETC!")
|
||||
.closeButtonText("I understand")
|
||||
.onAction(() -> Utilities.openWebPage("https://ethereumclassic.github.io/"))
|
||||
.actionButtonText("Open Ethereum Classic web page")
|
||||
|
@ -268,7 +268,7 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
|
||||
// called form parent as the view does not get notified when the tab is closed
|
||||
public void onClose() {
|
||||
Coin balance = model.dataModel.balance.get();
|
||||
if (balance != null && balance.isPositive() && !model.takeOfferCompleted.get()) {
|
||||
if (balance != null && balance.isPositive() && !model.takeOfferCompleted.get() && !DevFlags.DEV_MODE) {
|
||||
model.dataModel.swapTradeToSavings();
|
||||
new Popup().information("You had already funded that offer.\n" +
|
||||
"Your funds have been moved to your local Bitsquare wallet and are available for " +
|
||||
@ -523,7 +523,6 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
|
||||
showTransactionPublishedScreenSubscription = EasyBind.subscribe(model.showTransactionPublishedScreen, newValue -> {
|
||||
if (newValue && DevFlags.DEV_MODE) {
|
||||
close();
|
||||
navigation.navigateTo(MainView.class, PortfolioView.class, PendingTradesView.class);
|
||||
} else if (newValue && model.getTrade() != null && model.getTrade().errorMessageProperty().get() == null) {
|
||||
String key = "takeOfferSuccessInfo";
|
||||
if (preferences.showAgain(key)) {
|
||||
|
@ -143,18 +143,10 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
|
||||
//TODO remove after AUGUST, 30
|
||||
String key = "ETH-ETHC-Warning";
|
||||
if (dataModel.getPreferences().showAgain(key) && new Date().before(new Date(2016 - 1900, Calendar.AUGUST, 30))) {
|
||||
if (dataModel.getCurrencyCode().equals("ETH")) {
|
||||
new Popup().information("The EHT/ETHC fork situation carries considerable risks.\n" +
|
||||
"Be sure you fully understand the situation and check out the information on the \"Ethereum Classic\" and \"Ethereum\" project web pages.")
|
||||
.closeButtonText("I understand")
|
||||
.onAction(() -> Utilities.openWebPage("https://www.ethereum.org/"))
|
||||
.actionButtonText("Open Ethereum web page")
|
||||
.dontShowAgainId(key, dataModel.getPreferences())
|
||||
.show();
|
||||
} else if (dataModel.getCurrencyCode().equals("ETHC")) {
|
||||
new Popup().information("The EHT/ETHC fork situation carries considerable risks.\n" +
|
||||
if (dataModel.getCurrencyCode().equals("ETHC")) {
|
||||
new Popup().information("The EHT/ETC fork situation carries considerable risks.\n" +
|
||||
"Be sure you fully understand the situation and check out the information on the \"Ethereum Classic\" and \"Ethereum\" project web pages.\n\n" +
|
||||
"Please note, that the price is denominated as ETHC/BTC not BTC/ETHC!")
|
||||
"Please note, that the price is denominated as ETC/BTC not BTC/ETC!")
|
||||
.closeButtonText("I understand")
|
||||
.onAction(() -> Utilities.openWebPage("https://ethereumclassic.github.io/"))
|
||||
.actionButtonText("Open Ethereum Classic web page")
|
||||
|
@ -128,6 +128,8 @@ public class NetworkSettingsView extends ActivatableViewAndModel<GridPane, Activ
|
||||
|
||||
@Override
|
||||
public void activate() {
|
||||
// TODO we deactive atm as its not ready now
|
||||
useTorCheckBox.setDisable(true);
|
||||
useTorCheckBox.setSelected(preferences.getUseTorForBitcoinJ());
|
||||
useTorCheckBox.setOnAction(event -> {
|
||||
boolean selected = useTorCheckBox.isSelected();
|
||||
|
@ -318,6 +318,15 @@ public class BSFormatter {
|
||||
}
|
||||
}
|
||||
|
||||
public String formatTime(Date date) {
|
||||
if (date != null) {
|
||||
DateFormat timeFormatter = DateFormat.getTimeInstance(DateFormat.DEFAULT, locale);
|
||||
return timeFormatter.format(date);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public String formatDate(Date date) {
|
||||
if (date != null) {
|
||||
DateFormat dateFormatter = DateFormat.getDateInstance(DateFormat.DEFAULT, locale);
|
||||
|
@ -99,7 +99,7 @@ public class GUIUtil {
|
||||
String directory = Paths.get(path).getParent().toString();
|
||||
preferences.setDefaultPath(directory);
|
||||
Storage<ArrayList<PaymentAccount>> paymentAccountsStorage = new Storage<>(new File(directory));
|
||||
ArrayList<PaymentAccount> persisted = paymentAccountsStorage.initAndGetPersisted(fileName);
|
||||
ArrayList<PaymentAccount> persisted = paymentAccountsStorage.initAndGetPersistedWithFileName(fileName);
|
||||
if (persisted != null) {
|
||||
final StringBuilder msg = new StringBuilder();
|
||||
persisted.stream().forEach(paymentAccount -> {
|
||||
|
@ -0,0 +1,69 @@
|
||||
package io.bitsquare.gui.main.markets.trades;
|
||||
|
||||
import io.bitsquare.gui.main.markets.trades.charts.CandleData;
|
||||
import io.bitsquare.trade.TradeStatistics;
|
||||
import io.bitsquare.trade.offer.Offer;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.utils.Fiat;
|
||||
import org.junit.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class TradesChartsViewModelTest {
|
||||
private static final Logger log = LoggerFactory.getLogger(TradesChartsViewModelTest.class);
|
||||
|
||||
@Test
|
||||
public void testGetCandleData() {
|
||||
TradesChartsViewModel model = new TradesChartsViewModel();
|
||||
|
||||
long low = Fiat.parseFiat("EUR", "500").value;
|
||||
long open = Fiat.parseFiat("EUR", "520").value;
|
||||
long close = Fiat.parseFiat("EUR", "580").value;
|
||||
long high = Fiat.parseFiat("EUR", "600").value;
|
||||
long average = Fiat.parseFiat("EUR", "550").value;
|
||||
long amount = Coin.parseCoin("4").value;
|
||||
long volume = Fiat.parseFiat("EUR", "2200").value;
|
||||
boolean isBullish = true;
|
||||
|
||||
Set<TradeStatistics> set = new HashSet<>();
|
||||
final Date now = new Date();
|
||||
Offer offer = new Offer(null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
"EUR",
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null);
|
||||
set.add(new TradeStatistics(offer, Fiat.parseFiat("EUR", "520"), Coin.parseCoin("1"), new Date(now.getTime()), null, null));
|
||||
set.add(new TradeStatistics(offer, Fiat.parseFiat("EUR", "500"), Coin.parseCoin("1"), new Date(now.getTime() + 100), null, null));
|
||||
set.add(new TradeStatistics(offer, Fiat.parseFiat("EUR", "600"), Coin.parseCoin("1"), new Date(now.getTime() + 200), null, null));
|
||||
set.add(new TradeStatistics(offer, Fiat.parseFiat("EUR", "580"), Coin.parseCoin("1"), new Date(now.getTime() + 300), null, null));
|
||||
|
||||
CandleData candleData = model.getCandleData(model.getTickFromTime(now.getTime(), TradesChartsViewModel.TickUnit.DAY), set);
|
||||
assertEquals(open, candleData.open);
|
||||
assertEquals(close, candleData.close);
|
||||
assertEquals(high, candleData.high);
|
||||
assertEquals(low, candleData.low);
|
||||
assertEquals(average, candleData.average);
|
||||
assertEquals(amount, candleData.accumulatedAmount);
|
||||
assertEquals(volume, candleData.accumulatedVolume);
|
||||
assertEquals(isBullish, candleData.isBullish);
|
||||
}
|
||||
}
|
@ -306,7 +306,7 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis
|
||||
Log.traceCall();
|
||||
|
||||
requestDataManager.requestPreliminaryData();
|
||||
keepAliveManager.restart();
|
||||
keepAliveManager.start();
|
||||
p2pServiceListeners.stream().forEach(SetupListener::onTorNodeReady);
|
||||
}
|
||||
|
||||
@ -810,6 +810,10 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis
|
||||
p2PDataStorage.addHashMapChangedListener(hashMapChangedListener);
|
||||
}
|
||||
|
||||
public void removeHashMapChangedListener(HashMapChangedListener hashMapChangedListener) {
|
||||
p2PDataStorage.removeHashMapChangedListener(hashMapChangedListener);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Getters
|
||||
|
@ -0,0 +1,11 @@
|
||||
package io.bitsquare.p2p.messaging;
|
||||
|
||||
import io.bitsquare.p2p.Message;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public interface SupportedCapabilitiesMessage extends Message {
|
||||
@Nullable
|
||||
ArrayList<Integer> getSupportedCapabilities();
|
||||
}
|
@ -8,9 +8,11 @@ import io.bitsquare.app.Version;
|
||||
import io.bitsquare.common.ByteArrayUtils;
|
||||
import io.bitsquare.common.UserThread;
|
||||
import io.bitsquare.common.util.Tuple2;
|
||||
import io.bitsquare.common.util.Utilities;
|
||||
import io.bitsquare.p2p.Message;
|
||||
import io.bitsquare.p2p.NodeAddress;
|
||||
import io.bitsquare.p2p.messaging.PrefixedSealedAndSignedMessage;
|
||||
import io.bitsquare.p2p.messaging.SupportedCapabilitiesMessage;
|
||||
import io.bitsquare.p2p.network.messages.CloseConnectionMessage;
|
||||
import io.bitsquare.p2p.network.messages.SendersNodeAddressMessage;
|
||||
import io.bitsquare.p2p.peers.BanList;
|
||||
@ -18,11 +20,13 @@ import io.bitsquare.p2p.peers.getdata.messages.GetDataResponse;
|
||||
import io.bitsquare.p2p.peers.keepalive.messages.KeepAliveMessage;
|
||||
import io.bitsquare.p2p.peers.keepalive.messages.Ping;
|
||||
import io.bitsquare.p2p.peers.keepalive.messages.Pong;
|
||||
import io.bitsquare.p2p.storage.messages.AddDataMessage;
|
||||
import io.bitsquare.p2p.storage.messages.RefreshTTLMessage;
|
||||
import io.bitsquare.p2p.storage.payload.CapabilityRequiringPayload;
|
||||
import io.bitsquare.p2p.storage.payload.StoragePayload;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -106,7 +110,7 @@ public class Connection implements MessageListener {
|
||||
private final List<Tuple2<Long, Serializable>> messageTimeStamps = new ArrayList<>();
|
||||
private final CopyOnWriteArraySet<MessageListener> messageListeners = new CopyOnWriteArraySet<>();
|
||||
private volatile long lastSendTimeStamp = 0;
|
||||
;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
@ -168,73 +172,118 @@ public class Connection implements MessageListener {
|
||||
// Called form various threads
|
||||
public void sendMessage(Message message) {
|
||||
if (!stopped) {
|
||||
try {
|
||||
log.info("sendMessage message=" + getTruncatedMessage(message));
|
||||
Log.traceCall();
|
||||
// Throttle outbound messages
|
||||
long now = System.currentTimeMillis();
|
||||
long elapsed = now - lastSendTimeStamp;
|
||||
if (elapsed < 20) {
|
||||
log.info("We got 2 sendMessage requests in less than 20 ms. We set the thread to sleep " +
|
||||
"for 50 ms to avoid flooding our peer. lastSendTimeStamp={}, now={}, elapsed={}",
|
||||
lastSendTimeStamp, now, elapsed);
|
||||
Thread.sleep(50);
|
||||
if (!isCapabilityRequired(message) || isCapabilitySupported(message)) {
|
||||
try {
|
||||
log.info("sendMessage message=" + Utilities.toTruncatedString(message));
|
||||
Log.traceCall();
|
||||
|
||||
// Throttle outbound messages
|
||||
long now = System.currentTimeMillis();
|
||||
long elapsed = now - lastSendTimeStamp;
|
||||
if (elapsed < 20) {
|
||||
log.info("We got 2 sendMessage requests in less than 20 ms. We set the thread to sleep " +
|
||||
"for 50 ms to avoid flooding our peer. lastSendTimeStamp={}, now={}, elapsed={}",
|
||||
lastSendTimeStamp, now, elapsed);
|
||||
Thread.sleep(50);
|
||||
}
|
||||
|
||||
lastSendTimeStamp = now;
|
||||
String peersNodeAddress = peersNodeAddressOptional.isPresent() ? peersNodeAddressOptional.get().toString() : "null";
|
||||
int size = ByteArrayUtils.objectToByteArray(message).length;
|
||||
|
||||
if (message instanceof Ping || message instanceof RefreshTTLMessage) {
|
||||
// pings and offer refresh msg we dont want to log in production
|
||||
log.trace("\n\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n" +
|
||||
"Sending direct message to peer" +
|
||||
"Write object to outputStream to peer: {} (uid={})\ntruncated message={} / size={}" +
|
||||
"\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n",
|
||||
peersNodeAddress, uid, Utilities.toTruncatedString(message), size);
|
||||
} else if (message instanceof PrefixedSealedAndSignedMessage && peersNodeAddressOptional.isPresent()) {
|
||||
setPeerType(Connection.PeerType.DIRECT_MSG_PEER);
|
||||
|
||||
log.info("\n\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n" +
|
||||
"Sending direct message to peer" +
|
||||
"Write object to outputStream to peer: {} (uid={})\ntruncated message={} / size={}" +
|
||||
"\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n",
|
||||
peersNodeAddress, uid, Utilities.toTruncatedString(message), size);
|
||||
} else {
|
||||
log.info("\n\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n" +
|
||||
"Write object to outputStream to peer: {} (uid={})\ntruncated message={} / size={}" +
|
||||
"\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n",
|
||||
peersNodeAddress, uid, Utilities.toTruncatedString(message), size);
|
||||
}
|
||||
|
||||
if (!stopped) {
|
||||
objectOutputStreamLock.lock();
|
||||
objectOutputStream.writeObject(message);
|
||||
objectOutputStream.flush();
|
||||
|
||||
statistic.addSentBytes(size);
|
||||
statistic.addSentMessage(message);
|
||||
|
||||
// We don't want to get the activity ts updated by ping/pong msg
|
||||
if (!(message instanceof KeepAliveMessage))
|
||||
statistic.updateLastActivityTimestamp();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// an exception lead to a shutdown
|
||||
sharedModel.handleConnectionException(e);
|
||||
} catch (Throwable t) {
|
||||
log.error(t.getMessage());
|
||||
t.printStackTrace();
|
||||
sharedModel.handleConnectionException(t);
|
||||
} finally {
|
||||
if (objectOutputStreamLock.isLocked())
|
||||
objectOutputStreamLock.unlock();
|
||||
}
|
||||
|
||||
lastSendTimeStamp = now;
|
||||
String peersNodeAddress = peersNodeAddressOptional.isPresent() ? peersNodeAddressOptional.get().toString() : "null";
|
||||
int size = ByteArrayUtils.objectToByteArray(message).length;
|
||||
|
||||
if (message instanceof Ping || message instanceof RefreshTTLMessage) {
|
||||
// pings and offer refresh msg we dont want to log in production
|
||||
log.trace("\n\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n" +
|
||||
"Sending direct message to peer" +
|
||||
"Write object to outputStream to peer: {} (uid={})\ntruncated message={} / size={}" +
|
||||
"\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n",
|
||||
peersNodeAddress, uid, getTruncatedMessage(message), size);
|
||||
} else if (message instanceof PrefixedSealedAndSignedMessage && peersNodeAddressOptional.isPresent()) {
|
||||
setPeerType(Connection.PeerType.DIRECT_MSG_PEER);
|
||||
|
||||
log.info("\n\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n" +
|
||||
"Sending direct message to peer" +
|
||||
"Write object to outputStream to peer: {} (uid={})\ntruncated message={} / size={}" +
|
||||
"\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n",
|
||||
peersNodeAddress, uid, getTruncatedMessage(message), size);
|
||||
} else {
|
||||
log.info("\n\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n" +
|
||||
"Write object to outputStream to peer: {} (uid={})\ntruncated message={} / size={}" +
|
||||
"\n>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n",
|
||||
peersNodeAddress, uid, getTruncatedMessage(message), size);
|
||||
}
|
||||
|
||||
if (!stopped) {
|
||||
objectOutputStreamLock.lock();
|
||||
objectOutputStream.writeObject(message);
|
||||
objectOutputStream.flush();
|
||||
|
||||
statistic.addSentBytes(size);
|
||||
statistic.addSentMessage(message);
|
||||
|
||||
// We don't want to get the activity ts updated by ping/pong msg
|
||||
if (!(message instanceof KeepAliveMessage))
|
||||
statistic.updateLastActivityTimestamp();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// an exception lead to a shutdown
|
||||
sharedModel.handleConnectionException(e);
|
||||
} catch (Throwable t) {
|
||||
log.error(t.getMessage());
|
||||
t.printStackTrace();
|
||||
sharedModel.handleConnectionException(t);
|
||||
} finally {
|
||||
if (objectOutputStreamLock.isLocked())
|
||||
objectOutputStreamLock.unlock();
|
||||
}
|
||||
} else {
|
||||
log.debug("called sendMessage but was already stopped");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isCapabilitySupported(Message message) {
|
||||
if (message instanceof AddDataMessage) {
|
||||
final StoragePayload storagePayload = (((AddDataMessage) message).protectedStorageEntry).getStoragePayload();
|
||||
if (storagePayload instanceof CapabilityRequiringPayload) {
|
||||
final List<Integer> requiredCapabilities = ((CapabilityRequiringPayload) storagePayload).getRequiredCapabilities();
|
||||
final List<Integer> supportedCapabilities = sharedModel.getSupportedCapabilities();
|
||||
if (supportedCapabilities != null) {
|
||||
for (int messageCapability : requiredCapabilities) {
|
||||
for (int connectionCapability : supportedCapabilities) {
|
||||
if (messageCapability == connectionCapability)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
log.debug("We do not send the message to the peer because he does not support the required capability for that message type.\n" +
|
||||
"Required capabilities is: " + requiredCapabilities.toString() + "\n" +
|
||||
"Supported capabilities is: " + supportedCapabilities.toString() + "\n" +
|
||||
"connection: " + this.toString() + "\n" +
|
||||
"storagePayload is: " + Utilities.toTruncatedString(storagePayload));
|
||||
return false;
|
||||
} else {
|
||||
log.warn("We do not send the message to the peer because he uses an old version which does not support capabilities.\n" +
|
||||
"Required capabilities is: " + requiredCapabilities.toString() + "\n" +
|
||||
"connection: " + this.toString() + "\n" +
|
||||
"storagePayload is: " + Utilities.toTruncatedString(storagePayload));
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isCapabilityRequired(Message message) {
|
||||
return message instanceof AddDataMessage && (((AddDataMessage) message).protectedStorageEntry).getStoragePayload() instanceof CapabilityRequiringPayload;
|
||||
}
|
||||
|
||||
public List<Integer> getSupportedCapabilities() {
|
||||
return sharedModel.getSupportedCapabilities();
|
||||
}
|
||||
|
||||
public void addMessageListener(MessageListener messageListener) {
|
||||
boolean isNewEntry = messageListeners.add(messageListener);
|
||||
if (!isNewEntry)
|
||||
@ -451,10 +500,6 @@ public class Connection implements MessageListener {
|
||||
}
|
||||
}
|
||||
|
||||
private String getTruncatedMessage(Message message) {
|
||||
return StringUtils.abbreviate(message.toString(), 100).replace("\n", "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
@ -512,6 +557,9 @@ public class Connection implements MessageListener {
|
||||
private volatile boolean stopped;
|
||||
private CloseConnectionReason closeConnectionReason;
|
||||
private RuleViolation ruleViolation;
|
||||
@Nullable
|
||||
private List<Integer> supportedCapabilities;
|
||||
|
||||
|
||||
public SharedModel(Connection connection, Socket socket) {
|
||||
this.connection = connection;
|
||||
@ -549,6 +597,15 @@ public class Connection implements MessageListener {
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public List<Integer> getSupportedCapabilities() {
|
||||
return supportedCapabilities;
|
||||
}
|
||||
|
||||
public void setSupportedCapabilities(List<Integer> supportedCapabilities) {
|
||||
this.supportedCapabilities = supportedCapabilities;
|
||||
}
|
||||
|
||||
public void handleConnectionException(Throwable e) {
|
||||
Log.traceCall(e.toString());
|
||||
if (e instanceof SocketException) {
|
||||
@ -675,7 +732,7 @@ public class Connection implements MessageListener {
|
||||
"Received object (truncated)={} / size={}"
|
||||
+ "\n<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n",
|
||||
connection,
|
||||
StringUtils.abbreviate(rawInputObject.toString(), 100),
|
||||
Utilities.toTruncatedString(rawInputObject),
|
||||
size);
|
||||
} else if (rawInputObject instanceof Message) {
|
||||
// We want to log all incoming messages (except Pong and RefreshTTLMessage)
|
||||
@ -685,7 +742,7 @@ public class Connection implements MessageListener {
|
||||
"Received object (truncated)={} / size={}"
|
||||
+ "\n<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n",
|
||||
connection,
|
||||
StringUtils.abbreviate(rawInputObject.toString(), 100),
|
||||
Utilities.toTruncatedString(rawInputObject),
|
||||
size);
|
||||
} else {
|
||||
log.error("\n\n<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n" +
|
||||
@ -761,6 +818,9 @@ public class Connection implements MessageListener {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sharedModel.getSupportedCapabilities() == null && message instanceof SupportedCapabilitiesMessage)
|
||||
sharedModel.setSupportedCapabilities(((SupportedCapabilitiesMessage) message).getSupportedCapabilities());
|
||||
|
||||
if (message instanceof CloseConnectionMessage) {
|
||||
// If we get a CloseConnectionMessage we shut down
|
||||
log.info("CloseConnectionMessage received. Reason={}\n\t" +
|
||||
|
@ -10,7 +10,6 @@ import io.bitsquare.p2p.NodeAddress;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
@ -66,7 +65,7 @@ public abstract class NetworkNode implements MessageListener {
|
||||
abstract public void start(boolean useBridges, @Nullable SetupListener setupListener);
|
||||
|
||||
public SettableFuture<Connection> sendMessage(@NotNull NodeAddress peersNodeAddress, Message message) {
|
||||
Log.traceCall("peersNodeAddress=" + peersNodeAddress + "\n\tmessage=" + StringUtils.abbreviate(message.toString(), 100));
|
||||
Log.traceCall("peersNodeAddress=" + peersNodeAddress + "\n\tmessage=" + Utilities.toTruncatedString(message));
|
||||
checkNotNull(peersNodeAddress, "peerAddress must not be null");
|
||||
|
||||
Connection connection = getOutboundConnection(peersNodeAddress);
|
||||
@ -218,10 +217,10 @@ public abstract class NetworkNode implements MessageListener {
|
||||
public Socks5Proxy getSocksProxy() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public SettableFuture<Connection> sendMessage(Connection connection, Message message) {
|
||||
Log.traceCall("\n\tmessage=" + StringUtils.abbreviate(message.toString(), 100) + "\n\tconnection=" + connection);
|
||||
Log.traceCall("\n\tmessage=" + Utilities.toTruncatedString(message) + "\n\tconnection=" + connection);
|
||||
// connection.sendMessage might take a bit (compression, write to stream), so we use a thread to not block
|
||||
ListenableFuture<Connection> future = executorService.submit(() -> {
|
||||
Thread.currentThread().setName("NetworkNode:SendMessage-to-" + connection.getUid());
|
||||
|
@ -6,18 +6,17 @@ import com.google.common.util.concurrent.SettableFuture;
|
||||
import io.bitsquare.app.Log;
|
||||
import io.bitsquare.common.Timer;
|
||||
import io.bitsquare.common.UserThread;
|
||||
import io.bitsquare.common.util.Utilities;
|
||||
import io.bitsquare.p2p.NodeAddress;
|
||||
import io.bitsquare.p2p.network.Connection;
|
||||
import io.bitsquare.p2p.network.NetworkNode;
|
||||
import io.bitsquare.p2p.storage.messages.BroadcastMessage;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -67,7 +66,6 @@ public class BroadcastHandler implements PeerManager.Listener {
|
||||
private Listener listener;
|
||||
private int numOfPeers;
|
||||
private Timer timeoutTimer;
|
||||
private Set<String> broadcastQueue = new CopyOnWriteArraySet<>();
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -98,7 +96,7 @@ public class BroadcastHandler implements PeerManager.Listener {
|
||||
this.listener = listener;
|
||||
|
||||
Log.traceCall("Sender=" + sender + "\n\t" +
|
||||
"Message=" + StringUtils.abbreviate(message.toString(), 100));
|
||||
"Message=" + Utilities.toTruncatedString(message));
|
||||
Set<Connection> connectedPeersSet = networkNode.getConfirmedConnections()
|
||||
.stream()
|
||||
.filter(connection -> !connection.getPeersNodeAddressOptional().get().equals(sender))
|
||||
@ -124,9 +122,7 @@ public class BroadcastHandler implements PeerManager.Listener {
|
||||
"numOfPeers=" + numOfPeers + "\n\t" +
|
||||
"numOfCompletedBroadcasts=" + numOfCompletedBroadcasts + "\n\t" +
|
||||
"numOfCompletedBroadcasts=" + numOfCompletedBroadcasts + "\n\t" +
|
||||
"numOfFailedBroadcasts=" + numOfFailedBroadcasts + "\n\t" +
|
||||
"broadcastQueue.size()=" + broadcastQueue.size() + "\n\t" +
|
||||
"broadcastQueue=" + broadcastQueue);
|
||||
"numOfFailedBroadcasts=" + numOfFailedBroadcasts);
|
||||
onFault(errorMessage, false);
|
||||
}, timeoutDelay);
|
||||
|
||||
@ -141,60 +137,59 @@ public class BroadcastHandler implements PeerManager.Listener {
|
||||
}
|
||||
} else {
|
||||
onFault("Message not broadcasted because we have no available peers yet.\n\t" +
|
||||
"message = " + StringUtils.abbreviate(message.toString(), 100), false);
|
||||
"message = " + Utilities.toTruncatedString(message), false);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendToPeer(Connection connection, BroadcastMessage message) {
|
||||
String errorMessage = "Message not broadcasted because we have stopped the handler already.\n\t" +
|
||||
"message = " + StringUtils.abbreviate(message.toString(), 100);
|
||||
"message = " + Utilities.toTruncatedString(message);
|
||||
if (!stopped) {
|
||||
if (!connection.isStopped()) {
|
||||
NodeAddress nodeAddress = connection.getPeersNodeAddressOptional().get();
|
||||
log.trace("Broadcast message to " + nodeAddress + ".");
|
||||
broadcastQueue.add(nodeAddress.getFullAddress());
|
||||
SettableFuture<Connection> future = networkNode.sendMessage(connection, message);
|
||||
Futures.addCallback(future, new FutureCallback<Connection>() {
|
||||
@Override
|
||||
public void onSuccess(Connection connection) {
|
||||
numOfCompletedBroadcasts++;
|
||||
broadcastQueue.remove(nodeAddress.getFullAddress());
|
||||
if (!stopped) {
|
||||
log.trace("Broadcast to " + nodeAddress + " succeeded.");
|
||||
if (!connection.isCapabilityRequired(message) || connection.isCapabilitySupported(message)) {
|
||||
NodeAddress nodeAddress = connection.getPeersNodeAddressOptional().get();
|
||||
log.trace("Broadcast message to " + nodeAddress + ".");
|
||||
SettableFuture<Connection> future = networkNode.sendMessage(connection, message);
|
||||
Futures.addCallback(future, new FutureCallback<Connection>() {
|
||||
@Override
|
||||
public void onSuccess(Connection connection) {
|
||||
numOfCompletedBroadcasts++;
|
||||
if (!stopped) {
|
||||
log.trace("Broadcast to " + nodeAddress + " succeeded.");
|
||||
|
||||
if (listener != null)
|
||||
listener.onBroadcasted(message, numOfCompletedBroadcasts);
|
||||
|
||||
if (listener != null && numOfCompletedBroadcasts == 1)
|
||||
listener.onBroadcastedToFirstPeer(message);
|
||||
|
||||
if (numOfCompletedBroadcasts + numOfFailedBroadcasts == numOfPeers) {
|
||||
if (listener != null)
|
||||
listener.onBroadcastCompleted(message, numOfCompletedBroadcasts, numOfFailedBroadcasts);
|
||||
listener.onBroadcasted(message, numOfCompletedBroadcasts);
|
||||
|
||||
cleanup();
|
||||
resultHandler.onCompleted(BroadcastHandler.this);
|
||||
if (listener != null && numOfCompletedBroadcasts == 1)
|
||||
listener.onBroadcastedToFirstPeer(message);
|
||||
|
||||
if (numOfCompletedBroadcasts + numOfFailedBroadcasts == numOfPeers) {
|
||||
if (listener != null)
|
||||
listener.onBroadcastCompleted(message, numOfCompletedBroadcasts, numOfFailedBroadcasts);
|
||||
|
||||
cleanup();
|
||||
resultHandler.onCompleted(BroadcastHandler.this);
|
||||
}
|
||||
} else {
|
||||
// TODO investigate why that is called very often at seed nodes
|
||||
onFault("stopped at onSuccess: " + errorMessage, false);
|
||||
}
|
||||
} else {
|
||||
// TODO investigate why that is called very often at seed nodes
|
||||
onFault("stopped at onSuccess: " + errorMessage, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NotNull Throwable throwable) {
|
||||
numOfFailedBroadcasts++;
|
||||
broadcastQueue.remove(nodeAddress.getFullAddress());
|
||||
if (!stopped) {
|
||||
log.info("Broadcast to " + nodeAddress + " failed.\n\t" +
|
||||
"ErrorMessage=" + throwable.getMessage());
|
||||
if (numOfCompletedBroadcasts + numOfFailedBroadcasts == numOfPeers)
|
||||
@Override
|
||||
public void onFailure(@NotNull Throwable throwable) {
|
||||
numOfFailedBroadcasts++;
|
||||
if (!stopped) {
|
||||
log.info("Broadcast to " + nodeAddress + " failed.\n\t" +
|
||||
"ErrorMessage=" + throwable.getMessage());
|
||||
if (numOfCompletedBroadcasts + numOfFailedBroadcasts == numOfPeers)
|
||||
onFault("stopped at onFailure: " + errorMessage);
|
||||
} else {
|
||||
onFault("stopped at onFailure: " + errorMessage);
|
||||
} else {
|
||||
onFault("stopped at onFailure: " + errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
onFault("Connection stopped already", false);
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
package io.bitsquare.p2p.peers;
|
||||
|
||||
import io.bitsquare.app.Log;
|
||||
import io.bitsquare.common.util.Utilities;
|
||||
import io.bitsquare.p2p.NodeAddress;
|
||||
import io.bitsquare.p2p.network.NetworkNode;
|
||||
import io.bitsquare.p2p.storage.messages.BroadcastMessage;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -42,7 +42,7 @@ public class Broadcaster implements BroadcastHandler.ResultHandler {
|
||||
public void broadcast(BroadcastMessage message, @Nullable NodeAddress sender,
|
||||
@Nullable BroadcastHandler.Listener listener, boolean isDataOwner) {
|
||||
Log.traceCall("Sender=" + sender + "\n\t" +
|
||||
"Message=" + StringUtils.abbreviate(message.toString(), 100));
|
||||
"Message=" + Utilities.toTruncatedString(message));
|
||||
|
||||
BroadcastHandler broadcastHandler = new BroadcastHandler(networkNode, peerManager);
|
||||
broadcastHandler.broadcast(message, sender, this, listener, isDataOwner);
|
||||
|
@ -95,7 +95,7 @@ public class PeerManager implements ConnectionListener {
|
||||
this.seedNodeAddresses = new HashSet<>(seedNodeAddresses);
|
||||
networkNode.addConnectionListener(this);
|
||||
dbStorage = new Storage<>(storageDir);
|
||||
HashSet<Peer> persistedPeers = dbStorage.initAndGetPersisted("PersistedPeers");
|
||||
HashSet<Peer> persistedPeers = dbStorage.initAndGetPersistedWithFileName("PersistedPeers");
|
||||
if (persistedPeers != null) {
|
||||
log.info("We have persisted reported peers. persistedPeers.size()=" + persistedPeers.size());
|
||||
this.persistedPeers.addAll(persistedPeers);
|
||||
|
@ -6,17 +6,22 @@ import com.google.common.util.concurrent.SettableFuture;
|
||||
import io.bitsquare.app.Log;
|
||||
import io.bitsquare.common.Timer;
|
||||
import io.bitsquare.common.UserThread;
|
||||
import io.bitsquare.common.util.Utilities;
|
||||
import io.bitsquare.p2p.network.CloseConnectionReason;
|
||||
import io.bitsquare.p2p.network.Connection;
|
||||
import io.bitsquare.p2p.network.NetworkNode;
|
||||
import io.bitsquare.p2p.peers.getdata.messages.GetDataRequest;
|
||||
import io.bitsquare.p2p.peers.getdata.messages.GetDataResponse;
|
||||
import io.bitsquare.p2p.storage.P2PDataStorage;
|
||||
import io.bitsquare.p2p.storage.payload.CapabilityRequiringPayload;
|
||||
import io.bitsquare.p2p.storage.payload.StoragePayload;
|
||||
import io.bitsquare.p2p.storage.storageentry.ProtectedStorageEntry;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class GetDataRequestHandler {
|
||||
@ -64,8 +69,41 @@ public class GetDataRequestHandler {
|
||||
|
||||
public void handle(GetDataRequest getDataRequest, final Connection connection) {
|
||||
Log.traceCall(getDataRequest + "\n\tconnection=" + connection);
|
||||
GetDataResponse getDataResponse = new GetDataResponse(new HashSet<>(dataStorage.getMap().values()),
|
||||
getDataRequest.getNonce());
|
||||
|
||||
final HashSet<ProtectedStorageEntry> filteredDataSet = new HashSet<>();
|
||||
for (ProtectedStorageEntry protectedStorageEntry : dataStorage.getMap().values()) {
|
||||
final StoragePayload storagePayload = protectedStorageEntry.getStoragePayload();
|
||||
boolean doAdd = false;
|
||||
if (storagePayload instanceof CapabilityRequiringPayload) {
|
||||
final List<Integer> requiredCapabilities = ((CapabilityRequiringPayload) storagePayload).getRequiredCapabilities();
|
||||
final List<Integer> supportedCapabilities = connection.getSupportedCapabilities();
|
||||
if (supportedCapabilities != null) {
|
||||
for (int messageCapability : requiredCapabilities) {
|
||||
for (int connectionCapability : supportedCapabilities) {
|
||||
if (messageCapability == connectionCapability) {
|
||||
doAdd = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!doAdd)
|
||||
log.debug("We do not send the message to the peer because he does not support the required capability for that message type.\n" +
|
||||
"Required capabilities is: " + requiredCapabilities.toString() + "\n" +
|
||||
"Supported capabilities is: " + supportedCapabilities.toString() + "\n" +
|
||||
"storagePayload is: " + Utilities.toTruncatedString(storagePayload));
|
||||
} else {
|
||||
log.debug("We do not send the message to the peer because he uses an old version which does not support capabilities.\n" +
|
||||
"Required capabilities is: " + requiredCapabilities.toString() + "\n" +
|
||||
"storagePayload is: " + Utilities.toTruncatedString(storagePayload));
|
||||
}
|
||||
} else {
|
||||
doAdd = true;
|
||||
}
|
||||
if (doAdd)
|
||||
filteredDataSet.add(protectedStorageEntry);
|
||||
}
|
||||
|
||||
GetDataResponse getDataResponse = new GetDataResponse(filteredDataSet, getDataRequest.getNonce());
|
||||
|
||||
if (timeoutTimer == null) {
|
||||
timeoutTimer = UserThread.runAfter(() -> { // setup before sending to avoid race conditions
|
||||
|
@ -1,12 +1,14 @@
|
||||
package io.bitsquare.p2p.peers.getdata.messages;
|
||||
|
||||
import io.bitsquare.app.Version;
|
||||
import io.bitsquare.p2p.Message;
|
||||
import io.bitsquare.p2p.messaging.SupportedCapabilitiesMessage;
|
||||
import io.bitsquare.p2p.storage.storageentry.ProtectedStorageEntry;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
|
||||
public final class GetDataResponse implements Message {
|
||||
public final class GetDataResponse implements SupportedCapabilitiesMessage {
|
||||
// That object is sent over the wire, so we need to take care of version compatibility.
|
||||
private static final long serialVersionUID = Version.P2P_NETWORK_VERSION;
|
||||
private final int messageVersion = Version.getP2PMessageVersion();
|
||||
@ -19,6 +21,15 @@ public final class GetDataResponse implements Message {
|
||||
this.requestNonce = requestNonce;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private ArrayList<Integer> supportedCapabilities = Version.getCapabilities();
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public ArrayList<Integer> getSupportedCapabilities() {
|
||||
return supportedCapabilities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMessageVersion() {
|
||||
return messageVersion;
|
||||
|
@ -1,9 +1,13 @@
|
||||
package io.bitsquare.p2p.peers.getdata.messages;
|
||||
|
||||
import io.bitsquare.app.Version;
|
||||
import io.bitsquare.p2p.messaging.SupportedCapabilitiesMessage;
|
||||
import io.bitsquare.p2p.network.messages.AnonymousMessage;
|
||||
|
||||
public final class PreliminaryGetDataRequest implements AnonymousMessage, GetDataRequest {
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public final class PreliminaryGetDataRequest implements AnonymousMessage, GetDataRequest, SupportedCapabilitiesMessage {
|
||||
// That object is sent over the wire, so we need to take care of version compatibility.
|
||||
private static final long serialVersionUID = Version.P2P_NETWORK_VERSION;
|
||||
|
||||
@ -14,6 +18,15 @@ public final class PreliminaryGetDataRequest implements AnonymousMessage, GetDat
|
||||
this.nonce = nonce;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private ArrayList<Integer> supportedCapabilities = Version.getCapabilities();
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public ArrayList<Integer> getSupportedCapabilities() {
|
||||
return supportedCapabilities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNonce() {
|
||||
return nonce;
|
||||
|
@ -60,12 +60,8 @@ public class KeepAliveManager implements MessageListener, ConnectionListener, Pe
|
||||
// API
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void restart() {
|
||||
if (keepAliveTimer == null)
|
||||
keepAliveTimer = UserThread.runPeriodically(() -> {
|
||||
stopped = false;
|
||||
keepAlive();
|
||||
}, INTERVAL_SEC);
|
||||
public void start() {
|
||||
restart();
|
||||
}
|
||||
|
||||
|
||||
@ -166,6 +162,14 @@ public class KeepAliveManager implements MessageListener, ConnectionListener, Pe
|
||||
// Private
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void restart() {
|
||||
if (keepAliveTimer == null)
|
||||
keepAliveTimer = UserThread.runPeriodically(() -> {
|
||||
stopped = false;
|
||||
keepAlive();
|
||||
}, INTERVAL_SEC);
|
||||
}
|
||||
|
||||
private void keepAlive() {
|
||||
if (!stopped) {
|
||||
Log.traceCall();
|
||||
|
@ -4,6 +4,7 @@ import io.bitsquare.app.Version;
|
||||
import io.bitsquare.p2p.Message;
|
||||
|
||||
public abstract class KeepAliveMessage implements Message {
|
||||
//TODO add serialVersionUID also in superclasses as changes would break compatibility
|
||||
@Override
|
||||
public int getMessageVersion() {
|
||||
return Version.getP2PMessageVersion();
|
||||
|
@ -311,7 +311,7 @@ public class PeerExchangeManager implements MessageListener, ConnectionListener,
|
||||
requestWithAvailablePeers();
|
||||
}, RETRY_DELAY_AFTER_ALL_CON_LOST_SEC);
|
||||
} else {
|
||||
log.warn("retryTimer already started");
|
||||
log.debug("retryTimer already started");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,14 +2,17 @@ package io.bitsquare.p2p.peers.peerexchange.messages;
|
||||
|
||||
import io.bitsquare.app.Version;
|
||||
import io.bitsquare.p2p.NodeAddress;
|
||||
import io.bitsquare.p2p.messaging.SupportedCapabilitiesMessage;
|
||||
import io.bitsquare.p2p.network.messages.SendersNodeAddressMessage;
|
||||
import io.bitsquare.p2p.peers.peerexchange.Peer;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
public final class GetPeersRequest extends PeerExchangeMessage implements SendersNodeAddressMessage {
|
||||
public final class GetPeersRequest extends PeerExchangeMessage implements SendersNodeAddressMessage, SupportedCapabilitiesMessage {
|
||||
// That object is sent over the wire, so we need to take care of version compatibility.
|
||||
private static final long serialVersionUID = Version.P2P_NETWORK_VERSION;
|
||||
|
||||
@ -24,6 +27,15 @@ public final class GetPeersRequest extends PeerExchangeMessage implements Sender
|
||||
this.reportedPeers = reportedPeers;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private ArrayList<Integer> supportedCapabilities = Version.getCapabilities();
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public ArrayList<Integer> getSupportedCapabilities() {
|
||||
return supportedCapabilities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NodeAddress getSenderNodeAddress() {
|
||||
return senderNodeAddress;
|
||||
|
@ -1,11 +1,14 @@
|
||||
package io.bitsquare.p2p.peers.peerexchange.messages;
|
||||
|
||||
import io.bitsquare.app.Version;
|
||||
import io.bitsquare.p2p.messaging.SupportedCapabilitiesMessage;
|
||||
import io.bitsquare.p2p.peers.peerexchange.Peer;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
|
||||
public final class GetPeersResponse extends PeerExchangeMessage {
|
||||
public final class GetPeersResponse extends PeerExchangeMessage implements SupportedCapabilitiesMessage {
|
||||
// That object is sent over the wire, so we need to take care of version compatibility.
|
||||
private static final long serialVersionUID = Version.P2P_NETWORK_VERSION;
|
||||
|
||||
@ -17,6 +20,15 @@ public final class GetPeersResponse extends PeerExchangeMessage {
|
||||
this.reportedPeers = reportedPeers;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private ArrayList<Integer> supportedCapabilities = Version.getCapabilities();
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public ArrayList<Integer> getSupportedCapabilities() {
|
||||
return supportedCapabilities;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "GetPeersResponse{" +
|
||||
|
@ -4,6 +4,7 @@ import io.bitsquare.app.Version;
|
||||
import io.bitsquare.p2p.Message;
|
||||
|
||||
abstract class PeerExchangeMessage implements Message {
|
||||
//TODO add serialVersionUID also in superclasses as changes would break compatibility
|
||||
private final int messageVersion = Version.getP2PMessageVersion();
|
||||
|
||||
@Override
|
||||
|
@ -35,9 +35,12 @@ public class SeedNodesRepository {
|
||||
new NodeAddress("b66vnevaljo6xt5a.onion:8000"),*/
|
||||
|
||||
// v0.4.2
|
||||
// ...128
|
||||
DevFlags.STRESS_TEST_MODE ? new NodeAddress("hlitt7z4bec4kdh4.onion:8000") : new NodeAddress("uadzuib66jupaept.onion:8000"),
|
||||
DevFlags.STRESS_TEST_MODE ? new NodeAddress("hlitt7z4bec4kdh4.onion:8000") : new NodeAddress("hbma455xxbqhcuqh.onion:8000"),
|
||||
DevFlags.STRESS_TEST_MODE ? new NodeAddress("hlitt7z4bec4kdh4.onion:8000") : new NodeAddress("wgthuiqn3aoiovbm.onion:8000"),
|
||||
|
||||
// ...188
|
||||
DevFlags.STRESS_TEST_MODE ? new NodeAddress("hlitt7z4bec4kdh4.onion:8000") : new NodeAddress("hbma455xxbqhcuqh.onion:8000"),
|
||||
DevFlags.STRESS_TEST_MODE ? new NodeAddress("hlitt7z4bec4kdh4.onion:8000") : new NodeAddress("2zxtnprnx5wqr7a3.onion:8000"),
|
||||
|
||||
// testnet
|
||||
@ -50,9 +53,9 @@ public class SeedNodesRepository {
|
||||
// 3. Shut down the seed node
|
||||
// 4. Rename the directory with your local onion address
|
||||
// 5. Edit here your found onion address (new NodeAddress("YOUR_ONION.onion:8002")
|
||||
new NodeAddress("rxdkppp3vicnbgqt.onion:8002"),
|
||||
new NodeAddress("brmbf6mf67d2hlm4.onion:8002"),
|
||||
new NodeAddress("mfla72c4igh5ta2t.onion:8002")
|
||||
DevFlags.STRESS_TEST_MODE ? new NodeAddress("hlitt7z4bec4kdh4.onion:8002") : new NodeAddress("rxdkppp3vicnbgqt.onion:8002"),
|
||||
DevFlags.STRESS_TEST_MODE ? new NodeAddress("hlitt7z4bec4kdh4.onion:8002") : new NodeAddress("brmbf6mf67d2hlm4.onion:8002"),
|
||||
DevFlags.STRESS_TEST_MODE ? new NodeAddress("hlitt7z4bec4kdh4.onion:8002") : new NodeAddress("mfla72c4igh5ta2t.onion:8002")
|
||||
);
|
||||
|
||||
// Addresses are used if the last digit of their port match the network id:
|
||||
|
@ -10,6 +10,7 @@ import io.bitsquare.common.crypto.Hash;
|
||||
import io.bitsquare.common.crypto.Sig;
|
||||
import io.bitsquare.common.persistance.Persistable;
|
||||
import io.bitsquare.common.util.Tuple2;
|
||||
import io.bitsquare.common.util.Utilities;
|
||||
import io.bitsquare.common.wire.Payload;
|
||||
import io.bitsquare.p2p.Message;
|
||||
import io.bitsquare.p2p.NodeAddress;
|
||||
@ -71,7 +72,7 @@ public class P2PDataStorage implements MessageListener, ConnectionListener {
|
||||
|
||||
storage = new Storage<>(storageDir);
|
||||
|
||||
HashMap<ByteArray, MapValue> persisted = storage.initAndGetPersisted("SequenceNumberMap");
|
||||
HashMap<ByteArray, MapValue> persisted = storage.initAndGetPersistedWithFileName("SequenceNumberMap");
|
||||
if (persisted != null)
|
||||
sequenceNumberMap = getPurgedSequenceNumberMap(persisted);
|
||||
}
|
||||
@ -97,7 +98,7 @@ public class P2PDataStorage implements MessageListener, ConnectionListener {
|
||||
ByteArray hashOfPayload = entry.getKey();
|
||||
ProtectedStorageEntry protectedStorageEntry = map.get(hashOfPayload);
|
||||
toRemoveSet.add(protectedStorageEntry);
|
||||
log.info("We found an expired data entry. We remove the protectedData:\n\t" + StringUtils.abbreviate(protectedStorageEntry.toString().replace("\n", ""), 100));
|
||||
log.info("We found an expired data entry. We remove the protectedData:\n\t" + Utilities.toTruncatedString(protectedStorageEntry));
|
||||
map.remove(hashOfPayload);
|
||||
});
|
||||
|
||||
@ -118,7 +119,7 @@ public class P2PDataStorage implements MessageListener, ConnectionListener {
|
||||
@Override
|
||||
public void onMessage(Message message, Connection connection) {
|
||||
if (message instanceof BroadcastMessage) {
|
||||
Log.traceCall(StringUtils.abbreviate(message.toString(), 100) + "\n\tconnection=" + connection);
|
||||
Log.traceCall(Utilities.toTruncatedString(message) + "\n\tconnection=" + connection);
|
||||
connection.getPeersNodeAddressOptional().ifPresent(peersNodeAddress -> {
|
||||
if (message instanceof AddDataMessage) {
|
||||
add(((AddDataMessage) message).protectedStorageEntry, peersNodeAddress, null, false);
|
||||
@ -387,6 +388,10 @@ public class P2PDataStorage implements MessageListener, ConnectionListener {
|
||||
hashMapChangedListeners.add(hashMapChangedListener);
|
||||
}
|
||||
|
||||
public void removeHashMapChangedListener(HashMapChangedListener hashMapChangedListener) {
|
||||
hashMapChangedListeners.remove(hashMapChangedListener);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Private
|
||||
@ -571,7 +576,7 @@ public class P2PDataStorage implements MessageListener, ConnectionListener {
|
||||
.append(" / ")
|
||||
.append(mapValue != null ? mapValue.timeStamp : "null")
|
||||
.append("; Payload=")
|
||||
.append(StringUtils.abbreviate(storagePayload.toString(), 100).replace("\n", ""));
|
||||
.append(Utilities.toTruncatedString(storagePayload));
|
||||
});
|
||||
sb.append("\n------------------------------------------------------------\n");
|
||||
log.debug(sb.toString());
|
||||
|
@ -4,6 +4,7 @@ import io.bitsquare.app.Version;
|
||||
import io.bitsquare.p2p.Message;
|
||||
|
||||
public abstract class BroadcastMessage implements Message {
|
||||
//TODO add serialVersionUID also in superclasses as changes would break compatibility
|
||||
private final int messageVersion = Version.getP2PMessageVersion();
|
||||
|
||||
@Override
|
||||
|
@ -0,0 +1,18 @@
|
||||
package io.bitsquare.p2p.storage.payload;
|
||||
|
||||
import io.bitsquare.common.wire.Payload;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Used for payloads which requires certain capability.
|
||||
* <p>
|
||||
* This is used for TradeStatistics to be able to support old versions which don't know about that class.
|
||||
* We only send the data to nodes which are capable to handle that data (e.g. TradeStatistics supported from v. 0.4.9.1 on).
|
||||
*/
|
||||
public interface CapabilityRequiringPayload extends Payload {
|
||||
/**
|
||||
* @return Capabilities the other node need to support to receive that message
|
||||
*/
|
||||
List<Integer> getRequiredCapabilities();
|
||||
}
|
@ -14,6 +14,7 @@ import io.bitsquare.common.handlers.ResultHandler;
|
||||
import io.bitsquare.common.util.Utilities;
|
||||
import io.bitsquare.p2p.P2PService;
|
||||
import io.bitsquare.p2p.P2PServiceListener;
|
||||
import io.bitsquare.trade.TradeStatisticsManager;
|
||||
import io.bitsquare.trade.offer.OpenOfferManager;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.bitcoinj.store.BlockStoreException;
|
||||
@ -30,6 +31,7 @@ public class SeedNode {
|
||||
private static Environment env;
|
||||
private final Injector injector;
|
||||
private final SeedNodeModule seedNodeModule;
|
||||
private final TradeStatisticsManager tradeStatisticsManager;
|
||||
|
||||
private P2PService p2pService;
|
||||
|
||||
@ -116,6 +118,9 @@ public class SeedNode {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
// Wee want to persist trade statistics so we need to instantiate the tradeStatisticsManager
|
||||
tradeStatisticsManager = injector.getInstance(TradeStatisticsManager.class);
|
||||
}
|
||||
|
||||
public void shutDown() {
|
||||
|
Loading…
Reference in New Issue
Block a user