Merge branch 'LiveStats' into Development

This commit is contained in:
Manfred Karrer 2016-07-25 17:04:20 +02:00
commit 2240daf9c0
74 changed files with 2758 additions and 231 deletions

View File

@ -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;
}
}

View File

@ -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";
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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));
}
}

View File

@ -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;

View File

@ -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);

View File

@ -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.", ""))

View 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";
}

View File

@ -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();

View File

@ -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 {

View File

@ -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;

View File

@ -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));
}
}

View File

@ -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 {

View File

@ -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));
}
}

View 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 +
'}';
}
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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{" +

View File

@ -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;

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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);

View 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;
}
*/

View File

@ -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;
}

View File

@ -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)

View File

@ -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>

View File

@ -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");

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}
}

View File

@ -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));
}
}

View File

@ -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");
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}

View File

@ -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")

View File

@ -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)) {

View File

@ -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")

View File

@ -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();

View File

@ -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);

View File

@ -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 -> {

View File

@ -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);
}
}

View File

@ -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

View File

@ -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();
}

View File

@ -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" +

View File

@ -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());

View File

@ -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);
}

View File

@ -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);

View File

@ -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);

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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();

View File

@ -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();

View File

@ -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");
}
}

View File

@ -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;

View File

@ -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{" +

View File

@ -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

View File

@ -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:

View File

@ -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());

View File

@ -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

View File

@ -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();
}

View File

@ -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() {