diff --git a/common/pom.xml b/common/pom.xml index 8779b16d33..a9a8726a3a 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -5,7 +5,7 @@ parent io.bitsquare - 0.4.9 + 0.4.9.1 4.0.0 diff --git a/common/src/main/java/io/bitsquare/app/Capabilities.java b/common/src/main/java/io/bitsquare/app/Capabilities.java new file mode 100644 index 0000000000..2967789af5 --- /dev/null +++ b/common/src/main/java/io/bitsquare/app/Capabilities.java @@ -0,0 +1,35 @@ +package io.bitsquare.app; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Arrays; + +public class Capabilities { + private static final Logger log = LoggerFactory.getLogger(Capabilities.class); + + // 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 { + TRADE_STATISTICS + } + + public static void setCapabilities(ArrayList capabilities) { + Capabilities.capabilities = capabilities; + } + + private static ArrayList capabilities = new ArrayList<>(Arrays.asList( + Capability.TRADE_STATISTICS.ordinal() + )); + + /** + * @return The Capabilities as ordinal integer the client supports. + */ + public static ArrayList getCapabilities() { + return capabilities; + } +} diff --git a/common/src/main/java/io/bitsquare/app/Log.java b/common/src/main/java/io/bitsquare/app/Log.java index 2994c6dbb2..10a96bc590 100644 --- a/common/src/main/java/io/bitsquare/app/Log.java +++ b/common/src/main/java/io/bitsquare/app/Log.java @@ -34,6 +34,10 @@ public class Log { private static SizeBasedTriggeringPolicy triggeringPolicy; private static Logger logbackLogger; + public static void setLevel(Level logLevel) { + logbackLogger.setLevel(logLevel); + } + public static void setup(String fileName) { LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); @@ -84,10 +88,6 @@ public class Log { logbackLogger.addAppender(errorAppender);*/ } - public static void setLevel(Level logLevel) { - logbackLogger.setLevel(logLevel); - } - public static void traceCall() { if (LoggerFactory.getLogger(Log.class).isTraceEnabled()) { StackTraceElement stackTraceElement = new Throwable().getStackTrace()[1]; diff --git a/common/src/main/java/io/bitsquare/app/Version.java b/common/src/main/java/io/bitsquare/app/Version.java index 9b75b3d334..72255cbaba 100644 --- a/common/src/main/java/io/bitsquare/app/Version.java +++ b/common/src/main/java/io/bitsquare/app/Version.java @@ -24,7 +24,7 @@ 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 +39,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 diff --git a/common/src/main/java/io/bitsquare/common/Clock.java b/common/src/main/java/io/bitsquare/common/Clock.java index 30548e4f65..e178952a7e 100644 --- a/common/src/main/java/io/bitsquare/common/Clock.java +++ b/common/src/main/java/io/bitsquare/common/Clock.java @@ -12,7 +12,7 @@ import java.util.concurrent.TimeUnit; public class Clock { private static final Logger log = LoggerFactory.getLogger(Clock.class); - public static final int IDLE_TOLERANCE = 5000; + public static final int IDLE_TOLERANCE = 20000; public interface Listener { void onSecondTick(); diff --git a/common/src/main/java/io/bitsquare/common/CommonOptionKeys.java b/common/src/main/java/io/bitsquare/common/CommonOptionKeys.java new file mode 100644 index 0000000000..d20f191be0 --- /dev/null +++ b/common/src/main/java/io/bitsquare/common/CommonOptionKeys.java @@ -0,0 +1,5 @@ +package io.bitsquare.common; + +public class CommonOptionKeys { + public static final String LOG_LEVEL_KEY = "logLevel"; +} diff --git a/common/src/main/java/io/bitsquare/common/OptionKeys.java b/common/src/main/java/io/bitsquare/common/OptionKeys.java deleted file mode 100644 index 50c66cd6b8..0000000000 --- a/common/src/main/java/io/bitsquare/common/OptionKeys.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.bitsquare.common; - -public class OptionKeys { - public static final String LOG_LEVEL_KEY = "logLevel"; - public static final String IGNORE_DEV_MSG_KEY = "ignoreDevMsg"; -} diff --git a/common/src/main/java/io/bitsquare/common/util/RestartUtil.java b/common/src/main/java/io/bitsquare/common/util/RestartUtil.java index a826db8934..f62d85b4fa 100644 --- a/common/src/main/java/io/bitsquare/common/util/RestartUtil.java +++ b/common/src/main/java/io/bitsquare/common/util/RestartUtil.java @@ -52,7 +52,10 @@ public class RestartUtil { try { final String command = "nohup " + cmd.toString() + " >/dev/null 2>" + logPath + " &"; - log.warn("Executing cmd for restart:\n" + command); + log.warn("\n\n############################################################\n" + + "Executing cmd for restart: {}" + + "\n############################################################\n\n", + command); Runtime.getRuntime().exec(command); } catch (IOException e) { e.printStackTrace(); diff --git a/common/src/main/java/io/bitsquare/common/util/Utilities.java b/common/src/main/java/io/bitsquare/common/util/Utilities.java index 110f8a5dfa..1a067e2c80 100644 --- a/common/src/main/java/io/bitsquare/common/util/Utilities.java +++ b/common/src/main/java/io/bitsquare/common/util/Utilities.java @@ -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); + } } diff --git a/common/src/main/java/io/bitsquare/storage/Storage.java b/common/src/main/java/io/bitsquare/storage/Storage.java index c7e5f9e9be..4621144e79 100644 --- a/common/src/main/java/io/bitsquare/storage/Storage.java +++ b/common/src/main/java/io/bitsquare/storage/Storage.java @@ -77,7 +77,7 @@ public class Storage { } @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); diff --git a/core/pom.xml b/core/pom.xml index 8714a63e30..7f9476b11a 100755 --- a/core/pom.xml +++ b/core/pom.xml @@ -6,7 +6,7 @@ parent io.bitsquare - 0.4.9 + 0.4.9.1 core diff --git a/core/src/main/java/io/bitsquare/alert/AlertManager.java b/core/src/main/java/io/bitsquare/alert/AlertManager.java index 6bcb59f4ca..dc0d55bdca 100644 --- a/core/src/main/java/io/bitsquare/alert/AlertManager.java +++ b/core/src/main/java/io/bitsquare/alert/AlertManager.java @@ -19,7 +19,7 @@ package io.bitsquare.alert; import com.google.inject.Inject; import com.google.inject.name.Named; -import io.bitsquare.common.OptionKeys; +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(OptionKeys.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; diff --git a/core/src/main/java/io/bitsquare/alert/AlertModule.java b/core/src/main/java/io/bitsquare/alert/AlertModule.java index 98f13f1ecc..18ad9c03e6 100644 --- a/core/src/main/java/io/bitsquare/alert/AlertModule.java +++ b/core/src/main/java/io/bitsquare/alert/AlertModule.java @@ -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)); } } diff --git a/core/src/main/java/io/bitsquare/alert/PrivateNotificationManager.java b/core/src/main/java/io/bitsquare/alert/PrivateNotificationManager.java index d653bd2d76..65bbe468e1 100644 --- a/core/src/main/java/io/bitsquare/alert/PrivateNotificationManager.java +++ b/core/src/main/java/io/bitsquare/alert/PrivateNotificationManager.java @@ -19,7 +19,7 @@ package io.bitsquare.alert; import com.google.inject.Inject; import com.google.inject.name.Named; -import io.bitsquare.common.OptionKeys; +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(OptionKeys.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; diff --git a/core/src/main/java/io/bitsquare/app/BitsquareEnvironment.java b/core/src/main/java/io/bitsquare/app/BitsquareEnvironment.java index 52a8e918d8..574d9fc44e 100644 --- a/core/src/main/java/io/bitsquare/app/BitsquareEnvironment.java +++ b/core/src/main/java/io/bitsquare/app/BitsquareEnvironment.java @@ -20,11 +20,12 @@ package io.bitsquare.app; import ch.qos.logback.classic.Level; import io.bitsquare.BitsquareException; import io.bitsquare.btc.BitcoinNetwork; +import io.bitsquare.btc.BtcOptionKeys; import io.bitsquare.btc.UserAgent; -import io.bitsquare.btc.WalletService; +import io.bitsquare.common.CommonOptionKeys; import io.bitsquare.common.crypto.KeyStorage; import io.bitsquare.common.util.Utilities; -import io.bitsquare.network.OptionKeys; +import io.bitsquare.network.NetworkOptionKeys; import io.bitsquare.storage.Storage; import io.bitsquare.util.spring.JOptCommandLinePropertySource; import joptsimple.OptionSet; @@ -51,11 +52,6 @@ public class BitsquareEnvironment extends StandardEnvironment { private static final Logger log = LoggerFactory.getLogger(BitsquareEnvironment.class); private static final String BITCOIN_NETWORK_PROP = "bitcoinNetwork.properties"; - public static final String USER_DATA_DIR_KEY = "userDataDir"; - - public static final String DEFAULT_USER_DATA_DIR = defaultUserDataDir(); - - public static final String APP_NAME_KEY = "appName"; public static void setDefaultAppName(String defaultAppName) { DEFAULT_APP_NAME = defaultAppName; @@ -63,7 +59,7 @@ public class BitsquareEnvironment extends StandardEnvironment { public static String DEFAULT_APP_NAME = "Bitsquare"; - public static final String APP_DATA_DIR_KEY = "appDataDir"; + public static final String DEFAULT_USER_DATA_DIR = defaultUserDataDir(); public static final String DEFAULT_APP_DATA_DIR = appDataDir(DEFAULT_USER_DATA_DIR, DEFAULT_APP_NAME); public static final String LOG_LEVEL_DEFAULT = (DevFlags.STRESS_TEST_MODE || DevFlags.DEV_MODE) ? Level.TRACE.levelStr : Level.WARN.levelStr; @@ -78,13 +74,12 @@ public class BitsquareEnvironment extends StandardEnvironment { private final String appName; private final String userDataDir; - private final String appDataDir; private final String btcNetworkDir; private final String logLevel; private BitcoinNetwork bitcoinNetwork; - private final String seedNodes, ignoreDevMsg; - private final String myAddress, banList; + private final String btcSeedNodes, seedNodes, ignoreDevMsg, useTorForBtc, useTorForHttp, + myAddress, banList, dumpStatistics, socks5ProxyBtcAddress, socks5ProxyHttpAddress; public BitsquareEnvironment(OptionSet options) { this(new JOptCommandLinePropertySource(BITSQUARE_COMMANDLINE_PROPERTY_SOURCE_NAME, checkNotNull( @@ -108,7 +103,7 @@ public class BitsquareEnvironment extends StandardEnvironment { log.warn("propertiesObject not instance of Properties"); } } - properties.setProperty(BitcoinNetwork.KEY, bitcoinNetwork.name()); + properties.setProperty(BtcOptionKeys.BTC_NETWORK, bitcoinNetwork.name()); try (FileOutputStream fileOutputStream = new FileOutputStream(file)) { properties.store(fileOutputStream, null); @@ -125,35 +120,54 @@ public class BitsquareEnvironment extends StandardEnvironment { } protected BitsquareEnvironment(PropertySource commandLineProperties) { - userDataDir = commandLineProperties.containsProperty(USER_DATA_DIR_KEY) ? - (String) commandLineProperties.getProperty(USER_DATA_DIR_KEY) : - DEFAULT_USER_DATA_DIR; - - appName = commandLineProperties.containsProperty(APP_NAME_KEY) ? - (String) commandLineProperties.getProperty(APP_NAME_KEY) : - DEFAULT_APP_NAME; - - appDataDir = commandLineProperties.containsProperty(APP_DATA_DIR_KEY) ? - (String) commandLineProperties.getProperty(APP_DATA_DIR_KEY) : - appDataDir(userDataDir, appName); - - logLevel = commandLineProperties.containsProperty(io.bitsquare.common.OptionKeys.LOG_LEVEL_KEY) ? - (String) commandLineProperties.getProperty(io.bitsquare.common.OptionKeys.LOG_LEVEL_KEY) : + logLevel = commandLineProperties.containsProperty(CommonOptionKeys.LOG_LEVEL_KEY) ? + (String) commandLineProperties.getProperty(CommonOptionKeys.LOG_LEVEL_KEY) : LOG_LEVEL_DEFAULT; - seedNodes = commandLineProperties.containsProperty(OptionKeys.SEED_NODES_KEY) ? - (String) commandLineProperties.getProperty(OptionKeys.SEED_NODES_KEY) : + userDataDir = commandLineProperties.containsProperty(CoreOptionKeys.USER_DATA_DIR_KEY) ? + (String) commandLineProperties.getProperty(CoreOptionKeys.USER_DATA_DIR_KEY) : + DEFAULT_USER_DATA_DIR; + + appName = commandLineProperties.containsProperty(CoreOptionKeys.APP_NAME_KEY) ? + (String) commandLineProperties.getProperty(CoreOptionKeys.APP_NAME_KEY) : + DEFAULT_APP_NAME; + + appDataDir = commandLineProperties.containsProperty(CoreOptionKeys.APP_DATA_DIR_KEY) ? + (String) commandLineProperties.getProperty(CoreOptionKeys.APP_DATA_DIR_KEY) : + appDataDir(userDataDir, appName); + ignoreDevMsg = commandLineProperties.containsProperty(CoreOptionKeys.IGNORE_DEV_MSG_KEY) ? + (String) commandLineProperties.getProperty(CoreOptionKeys.IGNORE_DEV_MSG_KEY) : + ""; + dumpStatistics = commandLineProperties.containsProperty(CoreOptionKeys.DUMP_STATISTICS) ? + (String) commandLineProperties.getProperty(CoreOptionKeys.DUMP_STATISTICS) : ""; - myAddress = commandLineProperties.containsProperty(OptionKeys.MY_ADDRESS) ? - (String) commandLineProperties.getProperty(OptionKeys.MY_ADDRESS) : - ""; - banList = commandLineProperties.containsProperty(OptionKeys.BAN_LIST) ? - (String) commandLineProperties.getProperty(OptionKeys.BAN_LIST) : + seedNodes = commandLineProperties.containsProperty(NetworkOptionKeys.SEED_NODES_KEY) ? + (String) commandLineProperties.getProperty(NetworkOptionKeys.SEED_NODES_KEY) : ""; - ignoreDevMsg = commandLineProperties.containsProperty(io.bitsquare.common.OptionKeys.IGNORE_DEV_MSG_KEY) ? - (String) commandLineProperties.getProperty(io.bitsquare.common.OptionKeys.IGNORE_DEV_MSG_KEY) : + myAddress = commandLineProperties.containsProperty(NetworkOptionKeys.MY_ADDRESS) ? + (String) commandLineProperties.getProperty(NetworkOptionKeys.MY_ADDRESS) : + ""; + banList = commandLineProperties.containsProperty(NetworkOptionKeys.BAN_LIST) ? + (String) commandLineProperties.getProperty(NetworkOptionKeys.BAN_LIST) : + ""; + socks5ProxyBtcAddress = commandLineProperties.containsProperty(NetworkOptionKeys.SOCKS_5_PROXY_BTC_ADDRESS) ? + (String) commandLineProperties.getProperty(NetworkOptionKeys.SOCKS_5_PROXY_BTC_ADDRESS) : + ""; + socks5ProxyHttpAddress = commandLineProperties.containsProperty(NetworkOptionKeys.SOCKS_5_PROXY_HTTP_ADDRESS) ? + (String) commandLineProperties.getProperty(NetworkOptionKeys.SOCKS_5_PROXY_HTTP_ADDRESS) : + ""; + useTorForHttp = commandLineProperties.containsProperty(NetworkOptionKeys.USE_TOR_FOR_HTTP) ? + (String) commandLineProperties.getProperty(NetworkOptionKeys.USE_TOR_FOR_HTTP) : + ""; + + btcSeedNodes = commandLineProperties.containsProperty(BtcOptionKeys.BTC_SEED_NODES) ? + (String) commandLineProperties.getProperty(BtcOptionKeys.BTC_SEED_NODES) : + ""; + + useTorForBtc = commandLineProperties.containsProperty(BtcOptionKeys.USE_TOR_FOR_BTC) ? + (String) commandLineProperties.getProperty(BtcOptionKeys.USE_TOR_FOR_BTC) : ""; MutablePropertySources propertySources = this.getPropertySources(); @@ -163,7 +177,7 @@ public class BitsquareEnvironment extends StandardEnvironment { propertySources.addLast(homeDirProperties()); propertySources.addLast(classpathProperties()); - bitcoinNetwork = BitcoinNetwork.valueOf(getProperty(BitcoinNetwork.KEY, BitcoinNetwork.DEFAULT.name()).toUpperCase()); + bitcoinNetwork = BitcoinNetwork.valueOf(getProperty(BtcOptionKeys.BTC_NETWORK, BitcoinNetwork.DEFAULT.name()).toUpperCase()); btcNetworkDir = Paths.get(appDataDir, bitcoinNetwork.name().toLowerCase()).toString(); File btcNetworkDirFile = new File(btcNetworkDir); if (!btcNetworkDirFile.exists()) @@ -210,27 +224,31 @@ public class BitsquareEnvironment extends StandardEnvironment { private static final long serialVersionUID = -8478089705207326165L; { - setProperty(APP_DATA_DIR_KEY, appDataDir); - setProperty(io.bitsquare.common.OptionKeys.LOG_LEVEL_KEY, logLevel); + setProperty(CommonOptionKeys.LOG_LEVEL_KEY, logLevel); - setProperty(OptionKeys.SEED_NODES_KEY, seedNodes); - setProperty(OptionKeys.MY_ADDRESS, myAddress); - setProperty(OptionKeys.BAN_LIST, banList); - setProperty(io.bitsquare.common.OptionKeys.IGNORE_DEV_MSG_KEY, ignoreDevMsg); + setProperty(NetworkOptionKeys.SEED_NODES_KEY, seedNodes); + setProperty(NetworkOptionKeys.MY_ADDRESS, myAddress); + setProperty(NetworkOptionKeys.BAN_LIST, banList); + setProperty(NetworkOptionKeys.TOR_DIR, Paths.get(btcNetworkDir, "tor").toString()); + setProperty(NetworkOptionKeys.NETWORK_ID, String.valueOf(bitcoinNetwork.ordinal())); + setProperty(NetworkOptionKeys.SOCKS_5_PROXY_BTC_ADDRESS, socks5ProxyBtcAddress); + setProperty(NetworkOptionKeys.SOCKS_5_PROXY_HTTP_ADDRESS, socks5ProxyHttpAddress); + setProperty(NetworkOptionKeys.USE_TOR_FOR_HTTP, useTorForHttp); - setProperty(APP_NAME_KEY, appName); - setProperty(USER_DATA_DIR_KEY, userDataDir); + setProperty(CoreOptionKeys.APP_DATA_DIR_KEY, appDataDir); + setProperty(CoreOptionKeys.IGNORE_DEV_MSG_KEY, ignoreDevMsg); + setProperty(CoreOptionKeys.DUMP_STATISTICS, dumpStatistics); + setProperty(CoreOptionKeys.APP_NAME_KEY, appName); + setProperty(CoreOptionKeys.USER_DATA_DIR_KEY, userDataDir); + + setProperty(BtcOptionKeys.BTC_SEED_NODES, btcSeedNodes); + setProperty(BtcOptionKeys.USE_TOR_FOR_BTC, useTorForBtc); setProperty(UserAgent.NAME_KEY, appName); setProperty(UserAgent.VERSION_KEY, Version.VERSION); - - setProperty(WalletService.DIR_KEY, btcNetworkDir); - + setProperty(BtcOptionKeys.WALLET_DIR, btcNetworkDir); setProperty(Storage.DIR_KEY, Paths.get(btcNetworkDir, "db").toString()); setProperty(KeyStorage.DIR_KEY, Paths.get(btcNetworkDir, "keys").toString()); - setProperty(OptionKeys.TOR_DIR, Paths.get(btcNetworkDir, "tor").toString()); - - setProperty(OptionKeys.NETWORK_ID, String.valueOf(bitcoinNetwork.ordinal())); } }); } diff --git a/core/src/main/java/io/bitsquare/app/BitsquareExecutable.java b/core/src/main/java/io/bitsquare/app/BitsquareExecutable.java index dd5e75cceb..fd52ad4c66 100644 --- a/core/src/main/java/io/bitsquare/app/BitsquareExecutable.java +++ b/core/src/main/java/io/bitsquare/app/BitsquareExecutable.java @@ -19,8 +19,10 @@ package io.bitsquare.app; import io.bitsquare.BitsquareException; import io.bitsquare.btc.BitcoinNetwork; +import io.bitsquare.btc.BtcOptionKeys; import io.bitsquare.btc.RegTestHost; -import io.bitsquare.network.OptionKeys; +import io.bitsquare.common.CommonOptionKeys; +import io.bitsquare.network.NetworkOptionKeys; import io.bitsquare.p2p.P2PService; import io.bitsquare.util.joptsimple.EnumValueConverter; import joptsimple.OptionException; @@ -68,46 +70,58 @@ public abstract class BitsquareExecutable { } protected void customizeOptionParsing(OptionParser parser) { - parser.accepts(USER_DATA_DIR_KEY, description("User data directory", DEFAULT_USER_DATA_DIR)) - .withRequiredArg(); - parser.accepts(APP_NAME_KEY, description("Application name", DEFAULT_APP_NAME)) - .withRequiredArg(); - parser.accepts(APP_DATA_DIR_KEY, description("Application data directory", DEFAULT_APP_DATA_DIR)) - .withRequiredArg(); - parser.accepts(io.bitsquare.common.OptionKeys.LOG_LEVEL_KEY, description("Log level [OFF, ALL, ERROR, WARN, INFO, DEBUG, TRACE]", LOG_LEVEL_DEFAULT)) + parser.accepts(CommonOptionKeys.LOG_LEVEL_KEY, description("Log level [OFF, ALL, ERROR, WARN, INFO, DEBUG, TRACE]", LOG_LEVEL_DEFAULT)) .withRequiredArg(); - parser.accepts(OptionKeys.SEED_NODES_KEY, description("Override hard coded seed nodes as comma separated list: E.g. rxdkppp3vicnbgqt.onion:8002, mfla72c4igh5ta2t.onion:8002", "")) + parser.accepts(NetworkOptionKeys.SEED_NODES_KEY, description("Override hard coded seed nodes as comma separated list: E.g. rxdkppp3vicnbgqt.onion:8002, mfla72c4igh5ta2t.onion:8002", "")) .withRequiredArg(); - parser.accepts(OptionKeys.MY_ADDRESS, description("My own onion address (used for botstrap nodes to exclude itself)", "")) + parser.accepts(NetworkOptionKeys.MY_ADDRESS, description("My own onion address (used for botstrap nodes to exclude itself)", "")) .withRequiredArg(); - parser.accepts(OptionKeys.BAN_LIST, description("Nodes to exclude from network connections.", "")) + parser.accepts(NetworkOptionKeys.BAN_LIST, description("Nodes to exclude from network connections.", "")) .withRequiredArg(); - - parser.accepts(io.bitsquare.common.OptionKeys.IGNORE_DEV_MSG_KEY, description("If set to true all signed messages from Bitsquare developers are ignored " + + // use a fixed port as arbitrator use that for his ID + parser.accepts(NetworkOptionKeys.PORT_KEY, description("Port to listen on", 9999)) + .withRequiredArg() + .ofType(int.class); + parser.accepts(NetworkOptionKeys.USE_LOCALHOST, description("Use localhost network for development", false)) + .withRequiredArg() + .ofType(boolean.class); + parser.accepts(NetworkOptionKeys.MAX_CONNECTIONS, description("Max. connections a peer will try to keep", P2PService.MAX_CONNECTIONS_DEFAULT)) + .withRequiredArg() + .ofType(int.class); + parser.accepts(NetworkOptionKeys.SOCKS_5_PROXY_BTC_ADDRESS, description("A proxy address to be used for Bitcoin network. [host:port]", "")) + .withRequiredArg(); + parser.accepts(NetworkOptionKeys.SOCKS_5_PROXY_HTTP_ADDRESS, description("A proxy address to be used for Http requests (should be non-Tor). [host:port]", "")) + .withRequiredArg(); + parser.accepts(NetworkOptionKeys.USE_TOR_FOR_HTTP, description("If set to true all http traffic (expect Poloniex) is routed over tor (socks 5 proxy)", "")) + .withRequiredArg(); + + parser.accepts(CoreOptionKeys.USER_DATA_DIR_KEY, description("User data directory", DEFAULT_USER_DATA_DIR)) + .withRequiredArg(); + parser.accepts(CoreOptionKeys.APP_NAME_KEY, description("Application name", DEFAULT_APP_NAME)) + .withRequiredArg(); + parser.accepts(CoreOptionKeys.APP_DATA_DIR_KEY, description("Application data directory", DEFAULT_APP_DATA_DIR)) + .withRequiredArg(); + 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); - - // use a fixed port as arbitrator use that for his ID - parser.accepts(OptionKeys.PORT_KEY, description("Port to listen on", 9999)) - .withRequiredArg() - .ofType(int.class); - parser.accepts(OptionKeys.USE_LOCALHOST, description("Use localhost network for development", false)) + 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(OptionKeys.MAX_CONNECTIONS, description("Max. connections a peer will try to keep", P2PService.MAX_CONNECTIONS_DEFAULT)) - .withRequiredArg() - .ofType(int.class); - parser.accepts(BitcoinNetwork.KEY, description("Bitcoin network", BitcoinNetwork.DEFAULT)) + + parser.accepts(BtcOptionKeys.BTC_NETWORK, description("Bitcoin network", BitcoinNetwork.DEFAULT)) .withRequiredArg() .ofType(BitcoinNetwork.class) .withValuesConvertedBy(new EnumValueConverter(BitcoinNetwork.class)); - - parser.accepts(RegTestHost.KEY, description("", RegTestHost.DEFAULT)) + parser.accepts(BtcOptionKeys.REG_TEST_HOST, description("", RegTestHost.DEFAULT)) .withRequiredArg() .ofType(RegTestHost.class) .withValuesConvertedBy(new EnumValueConverter(RegTestHost.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 tor (socks 5 proxy).", "")) + .withRequiredArg(); } protected static String description(String descText, Object defaultValue) { diff --git a/core/src/main/java/io/bitsquare/app/CoreOptionKeys.java b/core/src/main/java/io/bitsquare/app/CoreOptionKeys.java new file mode 100644 index 0000000000..4e24b8c65b --- /dev/null +++ b/core/src/main/java/io/bitsquare/app/CoreOptionKeys.java @@ -0,0 +1,9 @@ +package io.bitsquare.app; + +public class CoreOptionKeys { + public static final String IGNORE_DEV_MSG_KEY = "ignoreDevMsg"; + public static final String DUMP_STATISTICS = "dumpStatistics"; + public static final String USER_DATA_DIR_KEY = "userDataDir"; + public static final String APP_NAME_KEY = "appName"; + public static final String APP_DATA_DIR_KEY = "appDataDir"; +} diff --git a/core/src/main/java/io/bitsquare/arbitration/messages/DisputeMessage.java b/core/src/main/java/io/bitsquare/arbitration/messages/DisputeMessage.java index 7ddee3e1c9..f6f0ec1a5f 100644 --- a/core/src/main/java/io/bitsquare/arbitration/messages/DisputeMessage.java +++ b/core/src/main/java/io/bitsquare/arbitration/messages/DisputeMessage.java @@ -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(); diff --git a/core/src/main/java/io/bitsquare/btc/BitcoinModule.java b/core/src/main/java/io/bitsquare/btc/BitcoinModule.java index 9342570216..9aacb2ad5b 100644 --- a/core/src/main/java/io/bitsquare/btc/BitcoinModule.java +++ b/core/src/main/java/io/bitsquare/btc/BitcoinModule.java @@ -20,7 +20,12 @@ package io.bitsquare.btc; import com.google.inject.Singleton; import io.bitsquare.app.AppModule; import io.bitsquare.btc.blockchain.BlockchainService; -import io.bitsquare.btc.pricefeed.PriceFeed; +import io.bitsquare.btc.blockchain.providers.BlockTrailProvider; +import io.bitsquare.btc.blockchain.providers.BlockrIOProvider; +import io.bitsquare.btc.blockchain.providers.TradeBlockProvider; +import io.bitsquare.btc.pricefeed.PriceFeedService; +import io.bitsquare.btc.pricefeed.providers.BitcoinAveragePriceProvider; +import io.bitsquare.btc.pricefeed.providers.PoloniexPriceProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.env.Environment; @@ -38,21 +43,31 @@ public class BitcoinModule extends AppModule { @Override protected void configure() { - bind(RegTestHost.class).toInstance(env.getProperty(RegTestHost.KEY, RegTestHost.class, RegTestHost.DEFAULT)); + bind(RegTestHost.class).toInstance(env.getProperty(BtcOptionKeys.REG_TEST_HOST, RegTestHost.class, RegTestHost.DEFAULT)); bind(FeePolicy.class).in(Singleton.class); bindConstant().annotatedWith(named(UserAgent.NAME_KEY)).to(env.getRequiredProperty(UserAgent.NAME_KEY)); bindConstant().annotatedWith(named(UserAgent.VERSION_KEY)).to(env.getRequiredProperty(UserAgent.VERSION_KEY)); bind(UserAgent.class).in(Singleton.class); - File walletDir = new File(env.getRequiredProperty(WalletService.DIR_KEY)); - bind(File.class).annotatedWith(named(WalletService.DIR_KEY)).toInstance(walletDir); + File walletDir = new File(env.getRequiredProperty(BtcOptionKeys.WALLET_DIR)); + bind(File.class).annotatedWith(named(BtcOptionKeys.WALLET_DIR)).toInstance(walletDir); + + bindConstant().annotatedWith(named(BtcOptionKeys.BTC_SEED_NODES)).to(env.getRequiredProperty(BtcOptionKeys.BTC_SEED_NODES)); + bindConstant().annotatedWith(named(BtcOptionKeys.USE_TOR_FOR_BTC)).to(env.getRequiredProperty(BtcOptionKeys.USE_TOR_FOR_BTC)); bind(AddressEntryList.class).in(Singleton.class); bind(TradeWalletService.class).in(Singleton.class); bind(WalletService.class).in(Singleton.class); bind(BlockchainService.class).in(Singleton.class); - bind(PriceFeed.class).in(Singleton.class); + + bind(PriceFeedService.class).in(Singleton.class); + bind(BitcoinAveragePriceProvider.class).in(Singleton.class); + bind(PoloniexPriceProvider.class).in(Singleton.class); + + bind(BlockrIOProvider.class).in(Singleton.class); + bind(BlockTrailProvider.class).in(Singleton.class); + bind(TradeBlockProvider.class).in(Singleton.class); } } diff --git a/core/src/main/java/io/bitsquare/btc/BitcoinNetwork.java b/core/src/main/java/io/bitsquare/btc/BitcoinNetwork.java index 97d68a2c92..8fe7b305a3 100644 --- a/core/src/main/java/io/bitsquare/btc/BitcoinNetwork.java +++ b/core/src/main/java/io/bitsquare/btc/BitcoinNetwork.java @@ -27,7 +27,6 @@ public enum BitcoinNetwork { TESTNET(TestNet3Params.get()), REGTEST(RegTestParams.get()); - public static final String KEY = "bitcoinNetwork"; public static final BitcoinNetwork DEFAULT = MAINNET; private final NetworkParameters parameters; diff --git a/core/src/main/java/io/bitsquare/btc/BtcOptionKeys.java b/core/src/main/java/io/bitsquare/btc/BtcOptionKeys.java new file mode 100644 index 0000000000..7e7ae13113 --- /dev/null +++ b/core/src/main/java/io/bitsquare/btc/BtcOptionKeys.java @@ -0,0 +1,9 @@ +package io.bitsquare.btc; + +public class BtcOptionKeys { + public static final String BTC_NETWORK = "bitcoinNetwork"; + public static final String REG_TEST_HOST = "bitcoinRegtestHost"; + public static final String BTC_SEED_NODES = "btcSeedNodes"; + public static final String USE_TOR_FOR_BTC = "useTorForBtc"; + public static final String WALLET_DIR = "walletDir"; +} diff --git a/core/src/main/java/io/bitsquare/btc/HttpClientProvider.java b/core/src/main/java/io/bitsquare/btc/HttpClientProvider.java new file mode 100644 index 0000000000..d9dc6515d4 --- /dev/null +++ b/core/src/main/java/io/bitsquare/btc/HttpClientProvider.java @@ -0,0 +1,28 @@ +package io.bitsquare.btc; + +import io.bitsquare.http.HttpClient; +import io.bitsquare.user.Preferences; + +import java.io.Serializable; + +public abstract class HttpClientProvider implements Serializable { + protected final HttpClient httpClient; + + public HttpClientProvider(HttpClient httpClient, Preferences preferences, String baseUrl) { + this(httpClient, preferences, baseUrl, false); + } + + public HttpClientProvider(HttpClient httpClient, Preferences preferences, String baseUrl, boolean ignoreSocks5Proxy) { + this.httpClient = httpClient; + + httpClient.setBaseUrl(baseUrl); + + httpClient.setIgnoreSocks5Proxy(ignoreSocks5Proxy || !preferences.getUseTorForHttpRequests()); + + if (!ignoreSocks5Proxy) { + preferences.useTorForHttpRequestsProperty().addListener((observable, oldValue, newValue) -> { + httpClient.setIgnoreSocks5Proxy(!newValue); + }); + } + } +} diff --git a/core/src/main/java/io/bitsquare/btc/ProxySocketFactory.java b/core/src/main/java/io/bitsquare/btc/ProxySocketFactory.java new file mode 100644 index 0000000000..0126530e6e --- /dev/null +++ b/core/src/main/java/io/bitsquare/btc/ProxySocketFactory.java @@ -0,0 +1,94 @@ + +/** + * Copyright (C) 2010-2014 Leon Blakey + * + * This file is part of PircBotX. + * + * PircBotX is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * PircBotX 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * PircBotX. If not, see . + */ +package io.bitsquare.btc; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.Socket; +import java.net.UnknownHostException; +import javax.net.SocketFactory; + +/** + * A basic SocketFactory for creating sockets that connect through the specified + * proxy. + * + * @author Leon Blakey + */ +public class ProxySocketFactory extends SocketFactory { + protected final Proxy proxy; + + /** + * Create all sockets with the specified proxy. + * + * @param proxy An existing proxy + */ + public ProxySocketFactory(Proxy proxy) { + this.proxy = proxy; + } + + /** + * A convenience constructor for creating a proxy with the specified host + * and port. + * + * @param proxyType The type of proxy were connecting to + * @param hostname The hostname of the proxy server + * @param port The port of the proxy server + */ + public ProxySocketFactory(Proxy.Type proxyType, String hostname, int port) { + this.proxy = new Proxy(proxyType, new InetSocketAddress(hostname, port)); + } + + @Override + public Socket createSocket() throws IOException { + Socket socket = new Socket(proxy); + return socket; + } + + @Override + public Socket createSocket(String string, int i) throws IOException, UnknownHostException { + Socket socket = new Socket(proxy); + socket.connect(new InetSocketAddress(string, i)); + return socket; + } + + @Override + public Socket createSocket(String string, int i, InetAddress localAddress, int localPort) throws IOException, UnknownHostException { + Socket socket = new Socket(proxy); + socket.bind(new InetSocketAddress(localAddress, localPort)); + socket.connect(new InetSocketAddress(string, i)); + return socket; + } + + @Override + public Socket createSocket(InetAddress ia, int i) throws IOException { + Socket socket = new Socket(proxy); + socket.connect(new InetSocketAddress(ia, i)); + return socket; + } + + @Override + public Socket createSocket(InetAddress ia, int i, InetAddress localAddress, int localPort) throws IOException { + Socket socket = new Socket(proxy); + socket.bind(new InetSocketAddress(localAddress, localPort)); + socket.connect(new InetSocketAddress(ia, i)); + return socket; + } +} \ No newline at end of file diff --git a/core/src/main/java/io/bitsquare/btc/RegTestHost.java b/core/src/main/java/io/bitsquare/btc/RegTestHost.java index 0e20b5fac2..31e16b3b3f 100644 --- a/core/src/main/java/io/bitsquare/btc/RegTestHost.java +++ b/core/src/main/java/io/bitsquare/btc/RegTestHost.java @@ -22,7 +22,6 @@ public enum RegTestHost { LOCALHOST, REG_TEST_SERVER; // 188.226.179.109 - public static final String KEY = "bitcoinRegtestHost"; public static final RegTestHost DEFAULT = LOCALHOST; public static final String SERVER_IP = "188.226.179.109"; diff --git a/core/src/main/java/io/bitsquare/btc/SeedPeersSocks5Dns.java b/core/src/main/java/io/bitsquare/btc/SeedPeersSocks5Dns.java new file mode 100644 index 0000000000..bfbdcb0eab --- /dev/null +++ b/core/src/main/java/io/bitsquare/btc/SeedPeersSocks5Dns.java @@ -0,0 +1,176 @@ +/** + * Copyright 2011 Micheal Swiggs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.bitsquare.btc; + +import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; +import com.runjva.sourceforge.jsocks.protocol.SocksSocket; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.net.discovery.PeerDiscovery; +import org.bitcoinj.net.discovery.PeerDiscoveryException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.concurrent.TimeUnit; + + +/** + * SeedPeersSocks5Dns resolves peers via Proxy (Socks5) remote DNS. + */ +public class SeedPeersSocks5Dns implements PeerDiscovery { + private Socks5Proxy proxy; + private NetworkParameters params; + private InetSocketAddress[] seedAddrs; + private InetSocketAddress[] seedAddrsIP; + private int pnseedIndex; + + private final InetSocketAddress[] seedAddrsResolved; + + private static final Logger log = LoggerFactory.getLogger(SeedPeersSocks5Dns.class); + + /** + * Supports finding peers by hostname over a socks5 proxy. + * + * @param Socks5Proxy proxy the socks5 proxy to connect over. + * @param NetworkParameters param to be used for seed and port information. + */ + public SeedPeersSocks5Dns(Socks5Proxy proxy, NetworkParameters params) { + + this.proxy = proxy; + this.params = params; + this.seedAddrs = convertAddrsString( params.getDnsSeeds(), params.getPort() ); + + if( false ) { + // This is an example of how .onion servers could be used. Unfortunately there is presently no way + // to hand the onion address (or a connected socket) back to bitcoinj without it crashing in PeerAddress. + // note: the onion addresses should be added into bitcoinj NetworkParameters classes, eg for mainnet, testnet + // not here! + this.seedAddrs = new InetSocketAddress[] { InetSocketAddress.createUnresolved( "cajrifqkvalh2ooa.onion", 8333 ), + InetSocketAddress.createUnresolved( "bk7yp6epnmcllq72.onion", 8333 ) + }; + } + + seedAddrsResolved = new InetSocketAddress[seedAddrs.length]; + for(int idx = seedAddrs.length; idx < seedAddrsResolved.length; idx ++) { + seedAddrsResolved[idx] = seedAddrsIP[idx-seedAddrs.length]; + } + } + + /** + * Acts as an iterator, returning the address of each node in the list sequentially. + * Once all the list has been iterated, null will be returned for each subsequent query. + * + * @return InetSocketAddress - The address/port of the next node. + * @throws PeerDiscoveryException + */ + @Nullable + public InetSocketAddress getPeer() throws PeerDiscoveryException { + try { + return nextPeer(); + } catch (UnknownHostException e) { + throw new PeerDiscoveryException(e); + } + } + + /** + * worker for getPeer() + */ + @Nullable + private InetSocketAddress nextPeer() throws UnknownHostException, PeerDiscoveryException { + if (seedAddrs == null || seedAddrs.length == 0) { + throw new PeerDiscoveryException("No IP address seeds configured; unable to find any peers"); + } + + if (pnseedIndex >= seedAddrsResolved.length) { + return null; + } + if( seedAddrsResolved[pnseedIndex] == null ) { + seedAddrsResolved[pnseedIndex] = lookup( proxy, seedAddrs[pnseedIndex] ); + } + log.error("SeedPeersSocks5Dns::nextPeer: " + seedAddrsResolved[pnseedIndex]); + + return seedAddrsResolved[pnseedIndex++]; + } + + /** + * Returns an array containing all the Bitcoin nodes within the list. + */ + @Override + public InetSocketAddress[] getPeers(long timeoutValue, TimeUnit timeoutUnit) throws PeerDiscoveryException { + try { + return allPeers(); + } catch (UnknownHostException e) { + throw new PeerDiscoveryException(e); + } + } + + /** + * returns all seed peers, performs hostname lookups if necessary. + */ + private InetSocketAddress[] allPeers() throws UnknownHostException { + for (int i = 0; i < seedAddrsResolved.length; ++i) { + if( seedAddrsResolved[i] == null ) { + seedAddrsResolved[i] = lookup( proxy, seedAddrs[i] ); + } + } + return seedAddrsResolved; + } + + /** + * Resolves a hostname via remote DNS over socks5 proxy. + */ + @Nullable + public static InetSocketAddress lookup( Socks5Proxy proxy, InetSocketAddress addr ) { + if( !addr.isUnresolved() ) { + return addr; + } + try { + SocksSocket proxySocket = new SocksSocket( proxy, addr.getHostString(), addr.getPort() ); + InetAddress addrResolved = proxySocket.getInetAddress(); + proxySocket.close(); + if( addrResolved != null ) { + log.info("Resolved " + addr.getHostString() + " to " + addrResolved.getHostAddress() ); + return new InetSocketAddress(addrResolved, addr.getPort() ); + } + else { + // note: .onion nodes fall in here when proxy is Tor. But they have no IP address. + // Unfortunately bitcoinj crashes in PeerAddress if it finds an unresolved address. + log.error("Connected to " + addr.getHostString() + ". But did not resolve to address." ); + } + } catch (Exception e) { + log.warn("Error resolving " + addr.getHostString() + ". Exception:\n" + e.toString()); + } + return null; + } + + /** + * Converts an array of hostnames to array of unresolved InetSocketAddress + */ + private InetSocketAddress[] convertAddrsString(String[] addrs, int port) { + InetSocketAddress[] list = new InetSocketAddress[addrs.length]; + for( int i = 0; i < addrs.length; i++) { + list[i] = InetSocketAddress.createUnresolved(addrs[i], port); + } + return list; + } + + @Override + public void shutdown() { + } +} diff --git a/core/src/main/java/io/bitsquare/btc/WalletAppKitBitSquare.java b/core/src/main/java/io/bitsquare/btc/WalletAppKitBitSquare.java new file mode 100644 index 0000000000..cb1a7a9564 --- /dev/null +++ b/core/src/main/java/io/bitsquare/btc/WalletAppKitBitSquare.java @@ -0,0 +1,68 @@ +/* + * 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 . + */ + +package io.bitsquare.btc; + +import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.PeerGroup; +import org.bitcoinj.kits.WalletAppKit; +import org.bitcoinj.net.BlockingClientManager; + +import java.io.File; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.util.concurrent.TimeoutException; + +public class WalletAppKitBitSquare extends WalletAppKit { + private Socks5Proxy socks5Proxy; + + /** + * Creates a new WalletAppKit, with a newly created {@link Context}. Files will be stored in the given directory. + */ + public WalletAppKitBitSquare(NetworkParameters params, Socks5Proxy socks5Proxy, File directory, String filePrefix) { + super(params, directory, filePrefix); + this.socks5Proxy = socks5Proxy; + } + + public Socks5Proxy getProxy() { + return socks5Proxy; + } + + protected PeerGroup createPeerGroup() throws TimeoutException { + + // no proxy case. + if (socks5Proxy == null) { + return super.createPeerGroup(); + } + + // proxy case. + Proxy proxy = new Proxy(Proxy.Type.SOCKS, + new InetSocketAddress(socks5Proxy.getInetAddress().getHostName(), + socks5Proxy.getPort())); + + int CONNECT_TIMEOUT_MSEC = 60 * 1000; // same value used in bitcoinj. + ProxySocketFactory proxySocketFactory = new ProxySocketFactory(proxy); + BlockingClientManager mgr = new BlockingClientManager(proxySocketFactory); + PeerGroup peerGroup = new PeerGroup(params, vChain, mgr); + + mgr.setConnectTimeoutMillis(CONNECT_TIMEOUT_MSEC); + peerGroup.setConnectTimeoutMillis(CONNECT_TIMEOUT_MSEC); + + return peerGroup; + } +} diff --git a/core/src/main/java/io/bitsquare/btc/WalletService.java b/core/src/main/java/io/bitsquare/btc/WalletService.java index 1c338326db..9a59546006 100644 --- a/core/src/main/java/io/bitsquare/btc/WalletService.java +++ b/core/src/main/java/io/bitsquare/btc/WalletService.java @@ -22,6 +22,8 @@ import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Service; +import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy; +import io.bitsquare.app.Log; import io.bitsquare.btc.listeners.AddressConfidenceListener; import io.bitsquare.btc.listeners.BalanceListener; import io.bitsquare.btc.listeners.TxConfidenceListener; @@ -30,6 +32,7 @@ import io.bitsquare.common.UserThread; import io.bitsquare.common.handlers.ErrorMessageHandler; import io.bitsquare.common.handlers.ExceptionHandler; import io.bitsquare.common.handlers.ResultHandler; +import io.bitsquare.network.Socks5ProxyProvider; import io.bitsquare.storage.FileUtil; import io.bitsquare.storage.Storage; import io.bitsquare.user.Preferences; @@ -37,7 +40,7 @@ import javafx.beans.property.*; import org.bitcoinj.core.*; import org.bitcoinj.crypto.DeterministicKey; import org.bitcoinj.crypto.KeyCrypterScrypt; -import org.bitcoinj.kits.WalletAppKit; +import org.bitcoinj.net.discovery.SeedPeers; import org.bitcoinj.params.MainNetParams; import org.bitcoinj.params.RegTestParams; import org.bitcoinj.params.TestNet3Params; @@ -54,6 +57,7 @@ import javax.inject.Named; import java.io.File; import java.io.IOException; import java.net.InetAddress; +import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.nio.file.Paths; import java.util.*; @@ -71,8 +75,6 @@ import static com.google.common.base.Preconditions.checkNotNull; public class WalletService { private static final Logger log = LoggerFactory.getLogger(WalletService.class); - public static final String DIR_KEY = "walletDir"; - public static final String PREFIX_KEY = "walletPrefix"; private static final long STARTUP_TIMEOUT_SEC = 60; private final CopyOnWriteArraySet addressConfidenceListeners = new CopyOnWriteArraySet<>(); @@ -85,12 +87,15 @@ public class WalletService { private final RegTestHost regTestHost; private final TradeWalletService tradeWalletService; private final AddressEntryList addressEntryList; + private final Preferences preferences; + private final Socks5ProxyProvider socks5ProxyProvider; + private final String seedNodes; private final NetworkParameters params; private final File walletDir; private final UserAgent userAgent; private final boolean useTor; - private WalletAppKit walletAppKit; + private WalletAppKitBitSquare walletAppKit; private Wallet wallet; private final IntegerProperty numPeers = new SimpleIntegerProperty(0); private final ObjectProperty> connectedPeers = new SimpleObjectProperty<>(); @@ -105,18 +110,37 @@ public class WalletService { /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public WalletService(RegTestHost regTestHost, TradeWalletService tradeWalletService, AddressEntryList addressEntryList, UserAgent userAgent, - @Named(DIR_KEY) File appDir, Preferences preferences) { + public WalletService(RegTestHost regTestHost, + TradeWalletService tradeWalletService, + AddressEntryList addressEntryList, + UserAgent userAgent, + Preferences preferences, + Socks5ProxyProvider socks5ProxyProvider, + @Named(BtcOptionKeys.WALLET_DIR) File appDir, + @Named(BtcOptionKeys.BTC_SEED_NODES) String seedNodes, + @Named(BtcOptionKeys.USE_TOR_FOR_BTC) String useTorFlagFromOptions) { this.regTestHost = regTestHost; this.tradeWalletService = tradeWalletService; this.addressEntryList = addressEntryList; + this.preferences = preferences; + this.socks5ProxyProvider = socks5ProxyProvider; + this.seedNodes = seedNodes; this.params = preferences.getBitcoinNetwork().getParameters(); this.walletDir = new File(appDir, "bitcoin"); this.userAgent = userAgent; + + // We support a checkbox in the settings to set the use tor flag. + // If we get the options set we override that setting. + if (useTorFlagFromOptions != null && !useTorFlagFromOptions.isEmpty()) { + if (useTorFlagFromOptions.equals("false")) + preferences.setUseTorForBitcoinJ(false); + else if (useTorFlagFromOptions.equals("true")) + preferences.setUseTorForBitcoinJ(true); + } useTor = preferences.getUseTorForBitcoinJ(); storage = new Storage<>(walletDir); - Long persisted = storage.initAndGetPersisted("BloomFilterNonce"); + Long persisted = storage.initAndGetPersistedWithFileName("BloomFilterNonce"); if (persisted != null) { bloomFilterTweak = persisted; } else { @@ -131,6 +155,7 @@ public class WalletService { /////////////////////////////////////////////////////////////////////////////////////////// public void initialize(@Nullable DeterministicSeed seed, ResultHandler resultHandler, ExceptionHandler exceptionHandler) { + Log.traceCall(); // Tell bitcoinj to execute event handlers on the JavaFX UI thread. This keeps things simple and means // we cannot forget to switch threads when adding event handlers. Unfortunately, the DownloadListener // we give to the app kit is currently an exception and runs on a library thread. It'll get fixed in @@ -142,11 +167,13 @@ public class WalletService { exceptionHandler.handleException(new TimeoutException("Wallet did not initialize in " + STARTUP_TIMEOUT_SEC + " seconds.")), STARTUP_TIMEOUT_SEC); - backupWallet(); + final Socks5Proxy socks5Proxy = preferences.getUseTorForBitcoinJ() ? socks5ProxyProvider.getSocks5Proxy() : null; + log.info("Use socks5Proxy for bitcoinj: " + socks5Proxy); + // If seed is non-null it means we are restoring from backup. - walletAppKit = new WalletAppKit(params, walletDir, "Bitsquare") { + walletAppKit = new WalletAppKitBitSquare(params, socks5Proxy, walletDir, "Bitsquare") { @Override protected void onSetupCompleted() { // Don't make the user wait for confirmations for now, as the intention is they're sending it @@ -247,11 +274,48 @@ public class WalletService { // 1333 / (2800 + 1333) = 0.32 -> 32 % probability that a pub key is in our wallet walletAppKit.setBloomFilterFalsePositiveRate(0.00005); + log.debug("seedNodes: " + seedNodes); + boolean usePeerNodes = false; - // TODO Get bitcoinj running over our tor proxy. BlockingClientManager need to be used to use the socket - // from jtorproxy. To get supported it via nio / netty will be harder - if (useTor && params.getId().equals(NetworkParameters.ID_MAINNET)) - walletAppKit.useTor(); + // Pass custom seed nodes if set in options + if (!seedNodes.isEmpty()) { + + // TODO: this parsing should be more robust, + // give validation error if needed. + String[] nodes = seedNodes.replace(", ", ",").split(","); + List peerAddressList = new ArrayList<>(); + for (String node : nodes) { + String[] parts = node.split(":"); + if (parts.length == 1) { + // port not specified. Use default port for network. + parts = new String[]{parts[0], Integer.toString(params.getPort())}; + } + if (parts.length == 2) { + // note: this will cause a DNS request if hostname used. + // note: DNS requests are routed over socks5 proxy, if used. + // fixme: .onion hostnames will fail! see comments in SeedPeersSocks5Dns + InetSocketAddress addr; + if (socks5Proxy != null) { + InetSocketAddress unresolved = InetSocketAddress.createUnresolved(parts[0], Integer.parseInt(parts[1])); + // proxy remote DNS request happens here. + addr = SeedPeersSocks5Dns.lookup(socks5Proxy, unresolved); + } else { + // DNS request happens here. if it fails, addr.isUnresolved() == true. + addr = new InetSocketAddress(parts[0], Integer.parseInt(parts[1])); + } + // note: isUnresolved check should be removed once we fix PeerAddress + if (addr != null && !addr.isUnresolved()) + peerAddressList.add(new PeerAddress(addr.getAddress(), addr.getPort())); + } + } + if (peerAddressList.size() > 0) { + PeerAddress peerAddressListFixed[] = new PeerAddress[peerAddressList.size()]; + log.debug("seedNodes parsed: " + Arrays.toString(peerAddressListFixed)); + + walletAppKit.setPeerNodes(peerAddressList.toArray(peerAddressListFixed)); + usePeerNodes = true; + } + } // Now configure and start the appkit. This will take a second or two - we could show a temporary splash screen // or progress widget to keep the user engaged whilst we initialise, but we don't. @@ -259,6 +323,7 @@ public class WalletService { if (regTestHost == RegTestHost.REG_TEST_SERVER) { try { walletAppKit.setPeerNodes(new PeerAddress(InetAddress.getByName(RegTestHost.SERVER_IP), params.getPort())); + usePeerNodes = true; } catch (UnknownHostException e) { throw new RuntimeException(e); } @@ -280,6 +345,23 @@ public class WalletService { walletAppKit.setCheckpoints(getClass().getResourceAsStream("/wallet/checkpoints.testnet")); } + // If operating over a proxy and we haven't set any peer nodes, then + // we want to use SeedPeers for discovery instead of the default DnsDiscovery. + // This is only because we do not yet have a Dns discovery class that works + // reliably over proxy/tor. + // + // todo: There should be a user pref called "Use Local DNS for Proxy/Tor" + // that disables this. In that case, the default DnsDiscovery class will + // be used which should work, but is less private. The aim here is to + // be private by default when using proxy/tor. However, the seedpeers + // could become outdated, so it is important that the user be able to + // disable it, but should be made aware of the reduced privacy. + if (socks5Proxy != null && !usePeerNodes) { + // SeedPeersSocks5Dns should replace SeedPeers once working reliably. + // SeedPeers uses hard coded stable addresses (from MainNetParams). It should be updated from time to time. + walletAppKit.setDiscovery(new SeedPeers(params)); + } + walletAppKit.setDownloadListener(downloadListener) .setBlockingStartup(false) .setUserAgent(userAgent.getName(), userAgent.getVersion()) @@ -695,7 +777,8 @@ public class WalletService { // in some cases getFee did not calculate correctly and we still get an InsufficientMoneyException log.warn("We still have a missing fee " + (e.missing != null ? e.missing.toFriendlyString() : "")); - amount = (amount.subtract(e.missing)); + if (e != null) + amount = amount.subtract(e.missing); newTransaction.clearOutputs(); newTransaction.addOutput(amount, toAddress); @@ -933,7 +1016,8 @@ public class WalletService { if (!addressEntry.isPresent()) throw new AddressEntryException("WithdrawFromAddress is not found in our wallet."); - checkNotNull(addressEntry.get().getAddress(), "addressEntry.get().getAddress() must nto be null"); + checkNotNull(addressEntry.get(), "addressEntry.get() must not be null"); + checkNotNull(addressEntry.get().getAddress(), "addressEntry.get().getAddress() must not be null"); sendRequest.coinSelector = new TradeWalletCoinSelector(params, addressEntry.get().getAddress()); sendRequest.changeAddress = addressEntry.get().getAddress(); sendRequest.feePerKb = FeePolicy.getNonTradeFeePerKb(); diff --git a/core/src/main/java/io/bitsquare/btc/blockchain/BlockchainService.java b/core/src/main/java/io/bitsquare/btc/blockchain/BlockchainService.java index 920cbea86f..9e35d84fd4 100644 --- a/core/src/main/java/io/bitsquare/btc/blockchain/BlockchainService.java +++ b/core/src/main/java/io/bitsquare/btc/blockchain/BlockchainService.java @@ -20,10 +20,11 @@ import java.util.Arrays; public class BlockchainService { private static final Logger log = LoggerFactory.getLogger(BlockchainService.class); - private final ArrayList feeProviders = new ArrayList<>(Arrays.asList(new BlockrIOProvider(), new BlockTrailProvider(), new TradeBlockProvider())); + private final ArrayList feeProviders; @Inject - public BlockchainService() { + public BlockchainService(BlockrIOProvider blockrIOProvider, BlockTrailProvider blockTrailProvider, TradeBlockProvider tradeBlockProvider) { + feeProviders = new ArrayList<>(Arrays.asList(blockrIOProvider, blockTrailProvider, tradeBlockProvider)); } public SettableFuture requestFee(String transactionId) { diff --git a/core/src/main/java/io/bitsquare/btc/blockchain/providers/BlockTrailProvider.java b/core/src/main/java/io/bitsquare/btc/blockchain/providers/BlockTrailProvider.java index f78111ab7b..399030b5b5 100644 --- a/core/src/main/java/io/bitsquare/btc/blockchain/providers/BlockTrailProvider.java +++ b/core/src/main/java/io/bitsquare/btc/blockchain/providers/BlockTrailProvider.java @@ -5,19 +5,20 @@ import com.google.gson.JsonParser; import io.bitsquare.app.Log; import io.bitsquare.http.HttpClient; import io.bitsquare.http.HttpException; +import io.bitsquare.user.Preferences; import org.bitcoinj.core.Coin; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.inject.Inject; import java.io.IOException; -public class BlockTrailProvider implements FeeProvider { +public class BlockTrailProvider extends FeeProvider { private static final Logger log = LoggerFactory.getLogger(BlockTrailProvider.class); - private final HttpClient httpClient; - - public BlockTrailProvider() { - httpClient = new HttpClient("https://www.blocktrail.com/BTC/json/blockchain/tx/"); + @Inject + public BlockTrailProvider(HttpClient httpClient, Preferences preferences) { + super(httpClient, preferences, "https://www.blocktrail.com/BTC/json/blockchain/tx/"); } @Override diff --git a/core/src/main/java/io/bitsquare/btc/blockchain/providers/BlockrIOProvider.java b/core/src/main/java/io/bitsquare/btc/blockchain/providers/BlockrIOProvider.java index d544d812ea..b890e94ff3 100644 --- a/core/src/main/java/io/bitsquare/btc/blockchain/providers/BlockrIOProvider.java +++ b/core/src/main/java/io/bitsquare/btc/blockchain/providers/BlockrIOProvider.java @@ -5,22 +5,22 @@ import com.google.gson.JsonParser; import io.bitsquare.app.Log; import io.bitsquare.http.HttpClient; import io.bitsquare.http.HttpException; +import io.bitsquare.user.Preferences; import org.bitcoinj.core.Coin; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.inject.Inject; import java.io.IOException; -public class BlockrIOProvider implements FeeProvider { +public class BlockrIOProvider extends FeeProvider { private static final Logger log = LoggerFactory.getLogger(BlockrIOProvider.class); - private final HttpClient httpClient; - - public BlockrIOProvider() { - httpClient = new HttpClient("https://btc.blockr.io/api/v1/tx/info/"); + @Inject + public BlockrIOProvider(HttpClient httpClient, Preferences preferences) { + super(httpClient, preferences, "https://btc.blockr.io/api/v1/tx/info/"); } - //https://api.bitcoinaverage.com/ticker/global/EUR/ @Override public Coin getFee(String transactionId) throws IOException, HttpException { Log.traceCall("transactionId=" + transactionId); diff --git a/core/src/main/java/io/bitsquare/btc/blockchain/providers/FeeProvider.java b/core/src/main/java/io/bitsquare/btc/blockchain/providers/FeeProvider.java index 7cebe68330..1a8f8b1870 100644 --- a/core/src/main/java/io/bitsquare/btc/blockchain/providers/FeeProvider.java +++ b/core/src/main/java/io/bitsquare/btc/blockchain/providers/FeeProvider.java @@ -1,10 +1,17 @@ package io.bitsquare.btc.blockchain.providers; +import io.bitsquare.btc.HttpClientProvider; +import io.bitsquare.http.HttpClient; import io.bitsquare.http.HttpException; +import io.bitsquare.user.Preferences; import org.bitcoinj.core.Coin; import java.io.IOException; -public interface FeeProvider { - Coin getFee(String transactionId) throws IOException, HttpException; +public abstract class FeeProvider extends HttpClientProvider { + public FeeProvider(HttpClient httpClient, Preferences preferences, String baseUrl) { + super(httpClient, preferences, baseUrl); + } + + public abstract Coin getFee(String transactionId) throws IOException, HttpException; } diff --git a/core/src/main/java/io/bitsquare/btc/blockchain/providers/TradeBlockProvider.java b/core/src/main/java/io/bitsquare/btc/blockchain/providers/TradeBlockProvider.java index 2f03644b89..af6f895d88 100644 --- a/core/src/main/java/io/bitsquare/btc/blockchain/providers/TradeBlockProvider.java +++ b/core/src/main/java/io/bitsquare/btc/blockchain/providers/TradeBlockProvider.java @@ -5,19 +5,20 @@ import com.google.gson.JsonParser; import io.bitsquare.app.Log; import io.bitsquare.http.HttpClient; import io.bitsquare.http.HttpException; +import io.bitsquare.user.Preferences; import org.bitcoinj.core.Coin; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.inject.Inject; import java.io.IOException; -public class TradeBlockProvider implements FeeProvider { +public class TradeBlockProvider extends FeeProvider { private static final Logger log = LoggerFactory.getLogger(TradeBlockProvider.class); - private final HttpClient httpClient; - - public TradeBlockProvider() { - httpClient = new HttpClient("https://tradeblock.com/api/blockchain/tx/"); + @Inject + public TradeBlockProvider(HttpClient httpClient, Preferences preferences) { + super(httpClient, preferences, "https://tradeblock.com/api/blockchain/tx/"); } @Override diff --git a/core/src/main/java/io/bitsquare/btc/pricefeed/MarketPrice.java b/core/src/main/java/io/bitsquare/btc/pricefeed/MarketPrice.java index 97efbbc7bc..fc1f83fd2e 100644 --- a/core/src/main/java/io/bitsquare/btc/pricefeed/MarketPrice.java +++ b/core/src/main/java/io/bitsquare/btc/pricefeed/MarketPrice.java @@ -30,7 +30,7 @@ public class MarketPrice { } - public double getPrice(PriceFeed.Type type) { + public double getPrice(PriceFeedService.Type type) { switch (type) { case ASK: return ask; diff --git a/core/src/main/java/io/bitsquare/btc/pricefeed/PriceFeed.java b/core/src/main/java/io/bitsquare/btc/pricefeed/PriceFeedService.java similarity index 95% rename from core/src/main/java/io/bitsquare/btc/pricefeed/PriceFeed.java rename to core/src/main/java/io/bitsquare/btc/pricefeed/PriceFeedService.java index e1b6115d25..7ea35a1af9 100644 --- a/core/src/main/java/io/bitsquare/btc/pricefeed/PriceFeed.java +++ b/core/src/main/java/io/bitsquare/btc/pricefeed/PriceFeedService.java @@ -24,8 +24,8 @@ import java.util.Random; import java.util.function.Consumer; // TODO use https://github.com/timmolter/XChange -public class PriceFeed { - private static final Logger log = LoggerFactory.getLogger(PriceFeed.class); +public class PriceFeedService { + private static final Logger log = LoggerFactory.getLogger(PriceFeedService.class); private static final long MIN_PERIOD_BETWEEN_CALLS = 5000; @@ -51,8 +51,8 @@ public class PriceFeed { private static final long PERIOD_ALL_CRYPTO_SEC = new Random().nextInt(5) + 180; private final Map cache = new HashMap<>(); - private final PriceProvider fiatPriceProvider = new BitcoinAveragePriceProvider(); - private final PriceProvider cryptoCurrenciesPriceProvider = new PoloniexPriceProvider(); + private final PriceProvider fiatPriceProvider; + private final PriceProvider cryptoCurrenciesPriceProvider; private Consumer priceConsumer; private FaultHandler faultHandler; private Type type; @@ -72,7 +72,9 @@ public class PriceFeed { /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public PriceFeed() { + public PriceFeedService(BitcoinAveragePriceProvider fiatPriceProvider, PoloniexPriceProvider cryptoCurrenciesPriceProvider) { + this.fiatPriceProvider = fiatPriceProvider; + this.cryptoCurrenciesPriceProvider = cryptoCurrenciesPriceProvider; } diff --git a/core/src/main/java/io/bitsquare/btc/pricefeed/providers/BitcoinAveragePriceProvider.java b/core/src/main/java/io/bitsquare/btc/pricefeed/providers/BitcoinAveragePriceProvider.java index 6af764a225..b1bfc07459 100644 --- a/core/src/main/java/io/bitsquare/btc/pricefeed/providers/BitcoinAveragePriceProvider.java +++ b/core/src/main/java/io/bitsquare/btc/pricefeed/providers/BitcoinAveragePriceProvider.java @@ -7,25 +7,27 @@ import com.google.gson.internal.LinkedTreeMap; import io.bitsquare.btc.pricefeed.MarketPrice; import io.bitsquare.http.HttpClient; import io.bitsquare.http.HttpException; +import io.bitsquare.user.Preferences; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.inject.Inject; import java.io.IOException; import java.util.HashMap; import java.util.Map; -public class BitcoinAveragePriceProvider implements PriceProvider { +public class BitcoinAveragePriceProvider extends PriceProvider { private static final Logger log = LoggerFactory.getLogger(BitcoinAveragePriceProvider.class); - private final HttpClient httpClient = new HttpClient("https://api.bitcoinaverage.com/ticker/global/"); - - public BitcoinAveragePriceProvider() { + @Inject + public BitcoinAveragePriceProvider(HttpClient httpClient, Preferences preferences) { + super(httpClient, preferences, "https://api.bitcoinaverage.com/ticker/global/", false); } @Override public Map getAllPrices() throws IOException, HttpException { Map marketPriceMap = new HashMap<>(); - LinkedTreeMap treeMap = new Gson().fromJson(httpClient.requestWithGET("all"), LinkedTreeMap.class); + LinkedTreeMap treeMap = new Gson().>fromJson(httpClient.requestWithGET("all"), LinkedTreeMap.class); Map temp = new HashMap<>(); treeMap.entrySet().stream().forEach(e -> { Object value = e.getValue(); @@ -56,7 +58,6 @@ public class BitcoinAveragePriceProvider implements PriceProvider { @Override public String toString() { - return "BitcoinAveragePriceProvider{" + - '}'; + return "BitcoinAveragePriceProvider"; } } diff --git a/core/src/main/java/io/bitsquare/btc/pricefeed/providers/PoloniexPriceProvider.java b/core/src/main/java/io/bitsquare/btc/pricefeed/providers/PoloniexPriceProvider.java index 39a5efaf73..dfd45b5fca 100644 --- a/core/src/main/java/io/bitsquare/btc/pricefeed/providers/PoloniexPriceProvider.java +++ b/core/src/main/java/io/bitsquare/btc/pricefeed/providers/PoloniexPriceProvider.java @@ -9,22 +9,27 @@ import io.bitsquare.http.HttpClient; import io.bitsquare.http.HttpException; import io.bitsquare.locale.CurrencyUtil; import io.bitsquare.locale.TradeCurrency; +import io.bitsquare.network.Socks5ProxyProvider; +import io.bitsquare.user.Preferences; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.inject.Inject; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; -public class PoloniexPriceProvider implements PriceProvider { +public class PoloniexPriceProvider extends PriceProvider { private static final Logger log = LoggerFactory.getLogger(PoloniexPriceProvider.class); - //https://poloniex.com/public?command=returnTicker - private final HttpClient httpClient = new HttpClient("https://poloniex.com/public"); - - public PoloniexPriceProvider() { + @Inject + public PoloniexPriceProvider(HttpClient httpClient, Preferences preferences, Socks5ProxyProvider socks5ProxyProvider) { + // Poloniex uses Cloudflare which requires a captcha if they get connected from a Tor exit node. + // We can't use Tor for Poloniex for that reason and set the ignoreSocks5Proxy flag to true if no + // custom socks5ProxyHttp is set. + super(httpClient, preferences, "https://poloniex.com/public", socks5ProxyProvider.getSocks5ProxyHttp() == null); } @Override @@ -74,7 +79,6 @@ public class PoloniexPriceProvider implements PriceProvider { @Override public String toString() { - return "PoloniexPriceProvider{" + - '}'; + return "PoloniexPriceProvider"; } } diff --git a/core/src/main/java/io/bitsquare/btc/pricefeed/providers/PriceProvider.java b/core/src/main/java/io/bitsquare/btc/pricefeed/providers/PriceProvider.java index 36a9e618a0..b79a2765ed 100644 --- a/core/src/main/java/io/bitsquare/btc/pricefeed/providers/PriceProvider.java +++ b/core/src/main/java/io/bitsquare/btc/pricefeed/providers/PriceProvider.java @@ -1,14 +1,21 @@ package io.bitsquare.btc.pricefeed.providers; +import io.bitsquare.btc.HttpClientProvider; import io.bitsquare.btc.pricefeed.MarketPrice; +import io.bitsquare.http.HttpClient; import io.bitsquare.http.HttpException; +import io.bitsquare.user.Preferences; import java.io.IOException; -import java.io.Serializable; import java.util.Map; -public interface PriceProvider extends Serializable { - Map getAllPrices() throws IOException, HttpException; +public abstract class PriceProvider extends HttpClientProvider { - MarketPrice getPrice(String currencyCode) throws IOException, HttpException; + public PriceProvider(HttpClient httpClient, Preferences preferences, String baseUrl, boolean ignoreSocks5Proxy) { + super(httpClient, preferences, baseUrl, ignoreSocks5Proxy); + } + + abstract public Map getAllPrices() throws IOException, HttpException; + + abstract public MarketPrice getPrice(String currencyCode) throws IOException, HttpException; } diff --git a/core/src/main/java/io/bitsquare/filter/FilterManager.java b/core/src/main/java/io/bitsquare/filter/FilterManager.java index ee131d8c31..579a0c4f48 100644 --- a/core/src/main/java/io/bitsquare/filter/FilterManager.java +++ b/core/src/main/java/io/bitsquare/filter/FilterManager.java @@ -19,7 +19,7 @@ package io.bitsquare.filter; import com.google.inject.Inject; import com.google.inject.name.Named; -import io.bitsquare.common.OptionKeys; +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(OptionKeys.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; diff --git a/core/src/main/java/io/bitsquare/filter/FilterModule.java b/core/src/main/java/io/bitsquare/filter/FilterModule.java index c7b90ddae4..8c6759238c 100644 --- a/core/src/main/java/io/bitsquare/filter/FilterModule.java +++ b/core/src/main/java/io/bitsquare/filter/FilterModule.java @@ -19,7 +19,7 @@ package io.bitsquare.filter; import com.google.inject.Singleton; import io.bitsquare.app.AppModule; -import io.bitsquare.common.OptionKeys; +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(OptionKeys.IGNORE_DEV_MSG_KEY)).to(env.getRequiredProperty(OptionKeys.IGNORE_DEV_MSG_KEY)); + bindConstant().annotatedWith(named(CoreOptionKeys.IGNORE_DEV_MSG_KEY)).to(env.getRequiredProperty(CoreOptionKeys.IGNORE_DEV_MSG_KEY)); } } diff --git a/core/src/main/java/io/bitsquare/locale/BankUtil.java b/core/src/main/java/io/bitsquare/locale/BankUtil.java index a500b6cb98..fcecabc5e2 100644 --- a/core/src/main/java/io/bitsquare/locale/BankUtil.java +++ b/core/src/main/java/io/bitsquare/locale/BankUtil.java @@ -83,7 +83,7 @@ public class BankUtil { case "HK": return "Bank code:"; default: - return "Bank nr. (e.g. BIC or SWIFT) (optional):"; + return "Bank ID (e.g. BIC or SWIFT) (optional):"; } } diff --git a/core/src/main/java/io/bitsquare/locale/CountryUtil.java b/core/src/main/java/io/bitsquare/locale/CountryUtil.java index a66b0b902f..b26cf5d5e4 100644 --- a/core/src/main/java/io/bitsquare/locale/CountryUtil.java +++ b/core/src/main/java/io/bitsquare/locale/CountryUtil.java @@ -20,11 +20,14 @@ package io.bitsquare.locale; import com.google.common.collect.Collections2; import com.google.common.collect.Lists; import io.bitsquare.user.Preferences; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.*; import java.util.stream.Collectors; public class CountryUtil { + private static final Logger log = LoggerFactory.getLogger(CountryUtil.class); public static List getAllSepaEuroCountries() { List list = new ArrayList<>(); @@ -147,7 +150,8 @@ public class CountryUtil { allLocales = new ArrayList<>(); allLocales.addAll(allLocalesAsSet); - allLocales.add(new Locale("", "MD", "")); + allLocales.add(new Locale("", "MD", "")); // Moldava + allLocales.add(new Locale("", "KH", "")); // Cambodia allLocales.sort((locale1, locale2) -> locale1.getDisplayCountry().compareTo(locale2.getDisplayCountry())); return allLocales; } @@ -160,7 +164,7 @@ public class CountryUtil { private static final String[] countryCodes = new String[]{"AE", "AL", "AR", "AT", "AU", "BA", "BE", "BG", "BH", "BO", "BR", "BY", "CA", "CH", "CL", "CN", "CO", "CR", "CS", "CU", "CY", "CZ", "DE", "DK", "DO", "DZ", "EC", "EE", "EG", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HR", "HU", "ID", "IE", "IL", "IN", - "IQ", "IS", "IT", "JO", "JP", "KR", "KW", "LB", "LT", "LU", "LV", "LY", "MA", "MD", "ME", "MK", "MT", "MX", + "IQ", "IS", "IT", "JO", "JP", "KH", "KR", "KW", "LB", "LT", "LU", "LV", "LY", "MA", "MD", "ME", "MK", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "OM", "PA", "PE", "PH", "PL", "PR", "PT", "PY", "QA", "RO", "RS", "RU", "SA", "SD", "SE", "SG", "SI", "SK", "SV", "SY", "TH", "TN", "TR", "TW", "UA", "US", "UY", "VE", "VN", "YE", "ZA"}; @@ -169,7 +173,7 @@ public class CountryUtil { private static final String[] regionCodes = new String[]{"AS", "EU", "SA", "EU", "OC", "EU", "EU", "EU", "AS", "SA", "SA", "EU", "NA", "EU", "SA", "AS", "SA", "NA", "EU", "NA", "EU", "EU", "EU", "EU", "NA", "AF", "SA", "EU", "AF", "EU", "EU", "EU", "EU", "EU", "NA", "AS", "NA", "EU", "EU", "AS", "EU", "AS", "AS", - "AS", "EU", "EU", "AS", "AS", "AS", "AS", "AS", "EU", "EU", "EU", "AF", "AF", "EU", "EU", "EU", "EU", "NA", + "AS", "EU", "EU", "AS", "AS", "AS", "AS", "AS", "AS", "EU", "EU", "EU", "AF", "AF", "EU", "EU", "EU", "EU", "NA", "AS", "NA", "EU", "EU", "OC", "AS", "NA", "SA", "AS", "EU", "NA", "EU", "SA", "AS", "EU", "EU", "EU", "AS", "AF", "EU", "AS", "EU", "EU", "NA", "AS", "AS", "AF", "AS", "AS", "EU", "NA", "SA", "SA", "AS", "AS", "AF"}; diff --git a/core/src/main/java/io/bitsquare/locale/CurrencyUtil.java b/core/src/main/java/io/bitsquare/locale/CurrencyUtil.java index 1cabf1a7f6..4bbcb101d0 100644 --- a/core/src/main/java/io/bitsquare/locale/CurrencyUtil.java +++ b/core/src/main/java/io/bitsquare/locale/CurrencyUtil.java @@ -75,7 +75,10 @@ public class CurrencyUtil { result.add(new CryptoCurrency("XMR", "Monero")); result.add(new CryptoCurrency("SC", "Siacoin")); result.add(new CryptoCurrency("ETH", "Ether")); - result.add(new CryptoCurrency("ETHC", "EtherClassic")); + result.add(new CryptoCurrency("ETC", "EtherClassic")); + result.add(new CryptoCurrency("STEEM", "STEEM")); + result.add(new CryptoCurrency("FLO", "FlorinCoin")); + result.add(new CryptoCurrency("XEM", "NEM")); result.add(new CryptoCurrency("LTC", "Litecoin")); result.add(new CryptoCurrency("DASH", "Dash")); result.add(new CryptoCurrency("NMC", "Namecoin")); @@ -90,16 +93,12 @@ public class CurrencyUtil { result.add(new CryptoCurrency("BLK", "Blackcoin")); result.add(new CryptoCurrency("FCT", "Factom")); result.add(new CryptoCurrency("NXT", "Nxt")); - result.add(new CryptoCurrency("STEEM", "STEEM")); result.add(new CryptoCurrency("BTS", "BitShares")); result.add(new CryptoCurrency("XCP", "Counterparty")); result.add(new CryptoCurrency("XRP", "Ripple")); result.add(new CryptoCurrency("FAIR", "FairCoin")); - result.add(new CryptoCurrency("FLO", "FlorinCoin")); result.add(new CryptoCurrency("MKR", "Maker", true)); result.add(new CryptoCurrency("DGD", "DigixDAO Tokens", true)); - result.add(new CryptoCurrency("DAO", "DAO", true)); - result.add(new CryptoCurrency("XEM", "NEM")); result.add(new CryptoCurrency("ANTI", "Anti")); result.add(new CryptoCurrency("VPN", "VPNCoin")); result.add(new CryptoCurrency("MAID", "MaidSafeCoin")); @@ -131,23 +130,25 @@ public class CurrencyUtil { result.add(new CryptoCurrency("EURT", "EUR Tether")); result.add(new CryptoCurrency("JPYT", "JPY Tether")); result.add(new CryptoCurrency("WDC", "Worldcoin")); + result.add(new CryptoCurrency("DAO", "DAO", true)); + result.add(new CryptoCurrency("ETHC", "EtherClassic (deprecated ticker symbol)")); return result; - } + } public static List getMainCryptoCurrencies() { final List result = new ArrayList<>(); result.add(new CryptoCurrency("XMR", "Monero")); result.add(new CryptoCurrency("SC", "Siacoin")); result.add(new CryptoCurrency("ETH", "Ether")); - result.add(new CryptoCurrency("ETHC", "EtherClassic")); + result.add(new CryptoCurrency("ETC", "EtherClassic")); + result.add(new CryptoCurrency("STEEM", "STEEM")); + result.add(new CryptoCurrency("FLO", "FlorinCoin")); result.add(new CryptoCurrency("LTC", "Litecoin")); result.add(new CryptoCurrency("DASH", "Dash")); result.add(new CryptoCurrency("NMC", "Namecoin")); result.add(new CryptoCurrency("NBT", "NuBits")); - result.add(new CryptoCurrency("SDC", "ShadowCash")); result.add(new CryptoCurrency("DOGE", "Dogecoin")); result.add(new CryptoCurrency("NXT", "Nxt")); - result.add(new CryptoCurrency("STEEM", "STEEM")); result.add(new CryptoCurrency("BTS", "BitShares")); return result; } diff --git a/core/src/main/java/io/bitsquare/payment/BankAccountContractData.java b/core/src/main/java/io/bitsquare/payment/BankAccountContractData.java index f6ddffdf46..14c85f6286 100644 --- a/core/src/main/java/io/bitsquare/payment/BankAccountContractData.java +++ b/core/src/main/java/io/bitsquare/payment/BankAccountContractData.java @@ -97,7 +97,7 @@ public abstract class BankAccountContractData extends CountryBasedPaymentAccount @Nullable public String getBankId() { - return bankId; + return BankUtil.isBankIdRequired(countryCode) ? bankId : bankName; } public void setBranchId(String branchId) { diff --git a/core/src/main/java/io/bitsquare/payment/PaymentAccountUtil.java b/core/src/main/java/io/bitsquare/payment/PaymentAccountUtil.java new file mode 100644 index 0000000000..ec611d6949 --- /dev/null +++ b/core/src/main/java/io/bitsquare/payment/PaymentAccountUtil.java @@ -0,0 +1,91 @@ +package io.bitsquare.payment; + +import io.bitsquare.locale.TradeCurrency; +import io.bitsquare.trade.offer.Offer; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.Set; +import java.util.stream.Collectors; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class PaymentAccountUtil { + private static final Logger log = LoggerFactory.getLogger(PaymentAccountUtil.class); + + public static boolean isAnyPaymentAccountValidForOffer(Offer offer, Collection paymentAccounts) { + for (PaymentAccount paymentAccount : paymentAccounts) { + if (isPaymentAccountValidForOffer(offer, paymentAccount)) + return true; + } + return false; + } + + public static ObservableList getPossiblePaymentAccounts(Offer offer, Set paymentAccounts) { + ObservableList result = FXCollections.observableArrayList(); + for (PaymentAccount paymentAccount : paymentAccounts) { + if (isPaymentAccountValidForOffer(offer, paymentAccount)) + result.add(paymentAccount); + } + return result; + } + + //TODO not tested with all combinations yet.... + public static boolean isPaymentAccountValidForOffer(Offer offer, PaymentAccount paymentAccount) { + // check if we have a matching currency + Set paymentAccountCurrencyCodes = paymentAccount.getTradeCurrencies().stream().map(TradeCurrency::getCode).collect(Collectors.toSet()); + boolean matchesCurrencyCode = paymentAccountCurrencyCodes.contains(offer.getCurrencyCode()); + if (!matchesCurrencyCode) + return false; + + // check if we have a matching payment method or if its a bank account payment method which is treated special + if (paymentAccount instanceof CountryBasedPaymentAccount) { + CountryBasedPaymentAccount countryBasedPaymentAccount = (CountryBasedPaymentAccount) paymentAccount; + + // check if we have a matching country + boolean matchesCountryCodes = offer.getAcceptedCountryCodes() != null && countryBasedPaymentAccount.getCountry() != null && + offer.getAcceptedCountryCodes().contains(countryBasedPaymentAccount.getCountry().code); + if (!matchesCountryCodes) + return false; + + if (paymentAccount instanceof SepaAccount || offer.getPaymentMethod().equals(PaymentMethod.SEPA)) { + boolean samePaymentMethod = paymentAccount.getPaymentMethod().equals(offer.getPaymentMethod()); + return samePaymentMethod; + } else if (offer.getPaymentMethod().equals(PaymentMethod.SAME_BANK) || + offer.getPaymentMethod().equals(PaymentMethod.SPECIFIC_BANKS)) { + + checkNotNull(offer.getAcceptedBankIds(), "offer.getAcceptedBankIds() must not be null"); + if (paymentAccount instanceof SpecificBanksAccount) { + // check if we have a matching bank + boolean offerSideMatchesBank = offer.getAcceptedBankIds().contains(((BankAccount) paymentAccount).getBankId()); + boolean paymentAccountSideMatchesBank = ((SpecificBanksAccount) paymentAccount).getAcceptedBanks().contains(offer.getBankId()); + return offerSideMatchesBank && paymentAccountSideMatchesBank; + } else { + // national or same bank + boolean matchesBank = offer.getAcceptedBankIds().contains(((BankAccount) paymentAccount).getBankId()); + return matchesBank; + } + } else { + if (paymentAccount instanceof SpecificBanksAccount) { + // check if we have a matching bank + boolean paymentAccountSideMatchesBank = ((SpecificBanksAccount) paymentAccount).getAcceptedBanks().contains(offer.getBankId()); + return paymentAccountSideMatchesBank; + } else if (paymentAccount instanceof SameBankAccount) { + // check if we have a matching bank + boolean paymentAccountSideMatchesBank = ((SameBankAccount) paymentAccount).getBankId().equals(offer.getBankId()); + return paymentAccountSideMatchesBank; + } else { + // national + return true; + } + } + + } else { + return paymentAccount.getPaymentMethod().equals(offer.getPaymentMethod()); + } + } + +} diff --git a/core/src/main/java/io/bitsquare/trade/TradeManager.java b/core/src/main/java/io/bitsquare/trade/TradeManager.java index e0c338b040..46962b15aa 100644 --- a/core/src/main/java/io/bitsquare/trade/TradeManager.java +++ b/core/src/main/java/io/bitsquare/trade/TradeManager.java @@ -24,7 +24,8 @@ import io.bitsquare.btc.AddressEntry; import io.bitsquare.btc.AddressEntryException; import io.bitsquare.btc.TradeWalletService; import io.bitsquare.btc.WalletService; -import io.bitsquare.btc.pricefeed.PriceFeed; +import io.bitsquare.btc.pricefeed.PriceFeedService; +import io.bitsquare.common.UserThread; import io.bitsquare.common.crypto.KeyRing; import io.bitsquare.common.handlers.ErrorMessageHandler; import io.bitsquare.common.handlers.FaultHandler; @@ -47,6 +48,8 @@ import io.bitsquare.trade.offer.OpenOfferManager; import io.bitsquare.trade.protocol.availability.OfferAvailabilityModel; import io.bitsquare.trade.protocol.trade.messages.PayDepositRequest; import io.bitsquare.trade.protocol.trade.messages.TradeMessage; +import io.bitsquare.trade.statistics.TradeStatistics; +import io.bitsquare.trade.statistics.TradeStatisticsManager; import io.bitsquare.user.User; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; @@ -63,9 +66,8 @@ import org.spongycastle.crypto.params.KeyParameter; import javax.inject.Inject; import javax.inject.Named; import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; +import java.util.*; +import java.util.concurrent.TimeUnit; import java.util.stream.Stream; import static io.bitsquare.util.Validator.nonEmptyStringOf; @@ -73,6 +75,8 @@ import static io.bitsquare.util.Validator.nonEmptyStringOf; public class TradeManager { private static final Logger log = LoggerFactory.getLogger(TradeManager.class); + private static final long REPUBLISH_STATISTICS_INTERVAL_MIN = TimeUnit.HOURS.toMillis(1); + private final User user; private final KeyRing keyRing; private final WalletService walletService; @@ -82,11 +86,14 @@ 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> tradableListStorage; private final TradableList trades; private final BooleanProperty pendingTradesInitialized = new SimpleBooleanProperty(); + private boolean stopped; + private List tradesForStatistics; /////////////////////////////////////////////////////////////////////////////////////////// @@ -103,8 +110,9 @@ public class TradeManager { FailedTradesManager failedTradesManager, ArbitratorManager arbitratorManager, P2PService p2PService, - PriceFeed priceFeed, + PriceFeedService priceFeedService, FilterManager filterManager, + TradeStatisticsManager tradeStatisticsManager, @Named(Storage.DIR_KEY) File storageDir) { this.user = user; this.keyRing = keyRing; @@ -116,10 +124,11 @@ public class TradeManager { this.arbitratorManager = arbitratorManager; this.p2PService = p2PService; this.filterManager = filterManager; + this.tradeStatisticsManager = tradeStatisticsManager; tradableListStorage = new Storage<>(storageDir); trades = new TradableList<>(tradableListStorage, "PendingTrades"); - trades.forEach(e -> e.getOffer().setPriceFeed(priceFeed)); + trades.forEach(e -> e.getOffer().setPriceFeedService(priceFeedService)); p2PService.addDecryptedDirectMessageListener(new DecryptedDirectMessageListener() { @Override @@ -158,6 +167,11 @@ public class TradeManager { }); } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + public void onAllServicesInitialized() { Log.traceCall(); if (p2PService.isBootstrapped()) @@ -173,37 +187,80 @@ public class TradeManager { }); } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Lifecycle - /////////////////////////////////////////////////////////////////////////////////////////// + public void shutDown() { + stopped = true; + } private void initPendingTrades() { Log.traceCall(); List toAdd = new ArrayList<>(); List toRemove = new ArrayList<>(); + tradesForStatistics = new ArrayList<>(); for (Trade trade : trades) { trade.setStorage(tradableListStorage); if (trade.isDepositPaid() || (trade.isTakerFeePaid() && trade.errorMessageProperty().get() == null)) { initTrade(trade, trade.getProcessModel().getUseSavingsWallet(), trade.getProcessModel().getFundsNeededForTrade()); trade.updateDepositTxFromWallet(); + tradesForStatistics.add(trade); } else if (trade.isTakerFeePaid()) { toAdd.add(trade); } else { toRemove.add(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) + tradesForStatistics.add((Trade) tradable); } + + // We start later to have better connectivity to the network + UserThread.runPeriodically(() -> publishTradeStatistics(tradesForStatistics), + 30, TimeUnit.SECONDS); + + //TODO can be removed at next release + // For the first 2 weeks of the release we re publish the trades to get faster good distribution + // otherwise the trades would only be published again at restart and if a client dont do that the stats might be missing + // for a longer time as initially there are not many peer upgraded and supporting flooding of the stats data. + if (new Date().before(new Date(2016 - 1900, Calendar.AUGUST, 8))) + UserThread.runPeriodically(() -> publishTradeStatistics(tradesForStatistics), + REPUBLISH_STATISTICS_INTERVAL_MIN, TimeUnit.MILLISECONDS); + pendingTradesInitialized.set(true); } + private void publishTradeStatistics(List trades) { + for (int i = 0; i < trades.size(); i++) { + Trade trade = trades.get(i); + TradeStatistics tradeStatistics = new TradeStatistics(trade.getOffer(), + trade.getTradePrice(), + trade.getTradeAmount(), + trade.getDate(), + (trade.getDepositTx() != null ? trade.getDepositTx().getHashAsString() : ""), + keyRing.getPubKeyRing()); + tradeStatisticsManager.add(tradeStatistics); + + // Only trades from last 30 days + if ((new Date().getTime() - trade.getDate().getTime()) < TimeUnit.DAYS.toMillis(30)) { + long delay = 3000; + final long minDelay = (i + 1) * delay; + final long maxDelay = (i + 2) * delay; + UserThread.runAfterRandomDelay(() -> { + if (!stopped) + p2PService.addData(tradeStatistics, true); + }, minDelay, maxDelay, TimeUnit.MILLISECONDS); + } + } + } + private void handleInitialTakeOfferRequest(TradeMessage message, NodeAddress peerNodeAddress) { log.trace("handleNewMessage: message = " + message.getClass().getSimpleName() + " from " + peerNodeAddress); try { @@ -289,7 +346,7 @@ public class TradeManager { if (offer.getState() == Offer.State.AVAILABLE) createTrade(amount, tradePrice, fundsNeededForTrade, offer, paymentAccountId, useSavingsWallet, model, tradeResultHandler); }, - errorMessage -> errorMessageHandler.handleErrorMessage(errorMessage)); + errorMessageHandler::handleErrorMessage); } private void createTrade(Coin amount, diff --git a/core/src/main/java/io/bitsquare/trade/TradeModule.java b/core/src/main/java/io/bitsquare/trade/TradeModule.java index 3d1b5878fd..d4afb74741 100644 --- a/core/src/main/java/io/bitsquare/trade/TradeModule.java +++ b/core/src/main/java/io/bitsquare/trade/TradeModule.java @@ -19,12 +19,16 @@ 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 io.bitsquare.trade.statistics.TradeStatisticsManager; 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 +39,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)); } } diff --git a/core/src/main/java/io/bitsquare/trade/closed/ClosedTradableManager.java b/core/src/main/java/io/bitsquare/trade/closed/ClosedTradableManager.java index 7e29f9c6aa..ee153f236c 100644 --- a/core/src/main/java/io/bitsquare/trade/closed/ClosedTradableManager.java +++ b/core/src/main/java/io/bitsquare/trade/closed/ClosedTradableManager.java @@ -18,7 +18,7 @@ package io.bitsquare.trade.closed; import com.google.inject.Inject; -import io.bitsquare.btc.pricefeed.PriceFeed; +import io.bitsquare.btc.pricefeed.PriceFeedService; import io.bitsquare.common.crypto.KeyRing; import io.bitsquare.storage.Storage; import io.bitsquare.trade.Tradable; @@ -38,10 +38,10 @@ public class ClosedTradableManager { private final KeyRing keyRing; @Inject - public ClosedTradableManager(KeyRing keyRing, PriceFeed priceFeed, @Named(Storage.DIR_KEY) File storageDir) { + public ClosedTradableManager(KeyRing keyRing, PriceFeedService priceFeedService, @Named(Storage.DIR_KEY) File storageDir) { this.keyRing = keyRing; this.closedTrades = new TradableList<>(new Storage<>(storageDir), "ClosedTrades"); - closedTrades.forEach(e -> e.getOffer().setPriceFeed(priceFeed)); + closedTrades.forEach(e -> e.getOffer().setPriceFeedService(priceFeedService)); } public void add(Tradable tradable) { diff --git a/core/src/main/java/io/bitsquare/trade/failed/FailedTradesManager.java b/core/src/main/java/io/bitsquare/trade/failed/FailedTradesManager.java index ffe153d7ae..1c4e168ec8 100644 --- a/core/src/main/java/io/bitsquare/trade/failed/FailedTradesManager.java +++ b/core/src/main/java/io/bitsquare/trade/failed/FailedTradesManager.java @@ -18,7 +18,7 @@ package io.bitsquare.trade.failed; import com.google.inject.Inject; -import io.bitsquare.btc.pricefeed.PriceFeed; +import io.bitsquare.btc.pricefeed.PriceFeedService; import io.bitsquare.common.crypto.KeyRing; import io.bitsquare.storage.Storage; import io.bitsquare.trade.TradableList; @@ -38,10 +38,10 @@ public class FailedTradesManager { private final KeyRing keyRing; @Inject - public FailedTradesManager(KeyRing keyRing, PriceFeed priceFeed, @Named(Storage.DIR_KEY) File storageDir) { + public FailedTradesManager(KeyRing keyRing, PriceFeedService priceFeedService, @Named(Storage.DIR_KEY) File storageDir) { this.keyRing = keyRing; this.failedTrades = new TradableList<>(new Storage<>(storageDir), "FailedTrades"); - failedTrades.forEach(e -> e.getOffer().setPriceFeed(priceFeed)); + failedTrades.forEach(e -> e.getOffer().setPriceFeedService(priceFeedService)); } public void add(Trade trade) { diff --git a/core/src/main/java/io/bitsquare/trade/offer/Offer.java b/core/src/main/java/io/bitsquare/trade/offer/Offer.java index 8af1f314a7..0a7cafd255 100644 --- a/core/src/main/java/io/bitsquare/trade/offer/Offer.java +++ b/core/src/main/java/io/bitsquare/trade/offer/Offer.java @@ -21,7 +21,7 @@ import io.bitsquare.app.DevFlags; import io.bitsquare.app.Version; import io.bitsquare.btc.Restrictions; import io.bitsquare.btc.pricefeed.MarketPrice; -import io.bitsquare.btc.pricefeed.PriceFeed; +import io.bitsquare.btc.pricefeed.PriceFeedService; import io.bitsquare.common.crypto.KeyRing; import io.bitsquare.common.crypto.PubKeyRing; import io.bitsquare.common.handlers.ErrorMessageHandler; @@ -144,7 +144,8 @@ public final class Offer implements StoragePayload, RequiresOwnerIsOnlinePayload transient private OfferAvailabilityProtocol availabilityProtocol; @JsonExclude transient private StringProperty errorMessageProperty = new SimpleStringProperty(); - transient private PriceFeed priceFeed; + @JsonExclude + transient private PriceFeedService priceFeedService; /////////////////////////////////////////////////////////////////////////////////////////// @@ -168,7 +169,7 @@ public final class Offer implements StoragePayload, RequiresOwnerIsOnlinePayload @Nullable ArrayList acceptedCountryCodes, @Nullable String bankId, @Nullable ArrayList acceptedBankIds, - PriceFeed priceFeed) { + PriceFeedService priceFeedService) { this.id = id; this.offererNodeAddress = offererNodeAddress; this.pubKeyRing = pubKeyRing; @@ -186,7 +187,7 @@ public final class Offer implements StoragePayload, RequiresOwnerIsOnlinePayload this.acceptedCountryCodes = acceptedCountryCodes; this.bankId = bankId; this.acceptedBankIds = acceptedBankIds; - this.priceFeed = priceFeed; + this.priceFeedService = priceFeedService; protocolVersion = Version.TRADE_PROTOCOL_VERSION; @@ -300,8 +301,8 @@ public final class Offer implements StoragePayload, RequiresOwnerIsOnlinePayload // Setters /////////////////////////////////////////////////////////////////////////////////////////// - public void setPriceFeed(PriceFeed priceFeed) { - this.priceFeed = priceFeed; + public void setPriceFeedService(PriceFeedService priceFeedService) { + this.priceFeedService = priceFeedService; } public void setState(State state) { @@ -355,10 +356,10 @@ public final class Offer implements StoragePayload, RequiresOwnerIsOnlinePayload @Nullable public Fiat getPrice() { if (useMarketBasedPrice) { - checkNotNull(priceFeed, "priceFeed must not be null"); - MarketPrice marketPrice = priceFeed.getMarketPrice(currencyCode); + checkNotNull(priceFeedService, "priceFeed must not be null"); + MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); if (marketPrice != null) { - PriceFeed.Type priceFeedType = direction == Direction.BUY ? PriceFeed.Type.ASK : PriceFeed.Type.BID; + PriceFeedService.Type priceFeedType = direction == Direction.BUY ? PriceFeedService.Type.ASK : PriceFeedService.Type.BID; double marketPriceAsDouble = marketPrice.getPrice(priceFeedType); double factor = direction == Offer.Direction.BUY ? 1 - marketPriceMargin : 1 + marketPriceMargin; double targetPrice = marketPriceAsDouble * factor; diff --git a/core/src/main/java/io/bitsquare/trade/offer/OfferBookService.java b/core/src/main/java/io/bitsquare/trade/offer/OfferBookService.java index b348072aa1..50a54afe94 100644 --- a/core/src/main/java/io/bitsquare/trade/offer/OfferBookService.java +++ b/core/src/main/java/io/bitsquare/trade/offer/OfferBookService.java @@ -17,7 +17,7 @@ package io.bitsquare.trade.offer; -import io.bitsquare.btc.pricefeed.PriceFeed; +import io.bitsquare.btc.pricefeed.PriceFeedService; import io.bitsquare.common.handlers.ErrorMessageHandler; import io.bitsquare.common.handlers.ResultHandler; import io.bitsquare.p2p.P2PService; @@ -46,7 +46,7 @@ public class OfferBookService { } private final P2PService p2PService; - private PriceFeed priceFeed; + private PriceFeedService priceFeedService; private final List offerBookChangedListeners = new LinkedList<>(); @@ -55,9 +55,9 @@ public class OfferBookService { /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public OfferBookService(P2PService p2PService, PriceFeed priceFeed) { + public OfferBookService(P2PService p2PService, PriceFeedService priceFeedService) { this.p2PService = p2PService; - this.priceFeed = priceFeed; + this.priceFeedService = priceFeedService; p2PService.addHashSetChangedListener(new HashMapChangedListener() { @Override @@ -65,7 +65,7 @@ public class OfferBookService { offerBookChangedListeners.stream().forEach(listener -> { if (data.getStoragePayload() instanceof Offer) { Offer offer = (Offer) data.getStoragePayload(); - offer.setPriceFeed(priceFeed); + offer.setPriceFeedService(priceFeedService); listener.onAdded(offer); } }); @@ -126,7 +126,7 @@ public class OfferBookService { .filter(data -> data.getStoragePayload() instanceof Offer) .map(data -> { Offer offer = (Offer) data.getStoragePayload(); - offer.setPriceFeed(priceFeed); + offer.setPriceFeedService(priceFeedService); return offer; }) .collect(Collectors.toList()); diff --git a/core/src/main/java/io/bitsquare/trade/offer/OpenOfferManager.java b/core/src/main/java/io/bitsquare/trade/offer/OpenOfferManager.java index 10cbc493b8..7ccb74c1a2 100644 --- a/core/src/main/java/io/bitsquare/trade/offer/OpenOfferManager.java +++ b/core/src/main/java/io/bitsquare/trade/offer/OpenOfferManager.java @@ -23,7 +23,7 @@ import io.bitsquare.app.Log; import io.bitsquare.btc.AddressEntry; import io.bitsquare.btc.TradeWalletService; import io.bitsquare.btc.WalletService; -import io.bitsquare.btc.pricefeed.PriceFeed; +import io.bitsquare.btc.pricefeed.PriceFeedService; import io.bitsquare.common.Timer; import io.bitsquare.common.UserThread; import io.bitsquare.common.crypto.KeyRing; @@ -72,7 +72,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe private static final long RETRY_REPUBLISH_DELAY_SEC = 10; private static final long REPUBLISH_AGAIN_AT_STARTUP_DELAY_SEC = 10; - private static final long REPUBLISH_INTERVAL_MS = TimeUnit.MINUTES.toMillis(DevFlags.STRESS_TEST_MODE ? 12 : 12); + private static final long REPUBLISH_INTERVAL_MS = TimeUnit.MINUTES.toMillis(DevFlags.STRESS_TEST_MODE ? 14 : 14); private static final long REFRESH_INTERVAL_MS = TimeUnit.MINUTES.toMillis(DevFlags.STRESS_TEST_MODE ? 4 : 4); private final KeyRing keyRing; @@ -102,7 +102,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe TradeWalletService tradeWalletService, OfferBookService offerBookService, ClosedTradableManager closedTradableManager, - PriceFeed priceFeed, + PriceFeedService priceFeedService, Preferences preferences, @Named(Storage.DIR_KEY) File storageDir) { this.keyRing = keyRing; @@ -116,7 +116,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe openOffersStorage = new Storage<>(storageDir); openOffers = new TradableList<>(openOffersStorage, "OpenOffers"); - openOffers.forEach(e -> e.getOffer().setPriceFeed(priceFeed)); + openOffers.forEach(e -> e.getOffer().setPriceFeedService(priceFeedService)); // In case the app did get killed the shutDown from the modules is not called, so we use a shutdown hook Runtime.getRuntime().addShutdownHook(new Thread(() -> { @@ -269,7 +269,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe startPeriodicRepublishOffersTimer(); startPeriodicRefreshOffersTimer(); } else { - log.warn("We have stopped already. We ignore that placeOfferProtocol.placeOffer.onResult call."); + log.debug("We have stopped already. We ignore that placeOfferProtocol.placeOffer.onResult call."); } } ); @@ -415,7 +415,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe log.info("Exception at handleRequestIsOfferAvailableMessage " + t.getMessage()); } } else { - log.warn("We have stopped already. We ignore that handleOfferAvailabilityRequest call."); + log.debug("We have stopped already. We ignore that handleOfferAvailabilityRequest call."); } } @@ -432,18 +432,18 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe stopPeriodicRefreshOffersTimer(); for (int i = 0; i < size; i++) { // we delay to avoid reaching throttle limits - // roughly 1 offer per second - final int n = i; - final long minDelay = i * 500 + 1; - final long maxDelay = minDelay * 2 + 500; + + long delay = 500; + final long minDelay = (i + 1) * delay; + final long maxDelay = (i + 2) * delay; + final OpenOffer openOffer = openOffersList.get(i); UserThread.runAfterRandomDelay(() -> { - OpenOffer openOffer = openOffersList.get(n); if (openOffers.contains(openOffer)) republishOffer(openOffer); }, minDelay, maxDelay, TimeUnit.MILLISECONDS); } } else { - log.warn("We have stopped already. We ignore that republishOffers call."); + log.debug("We have stopped already. We ignore that republishOffers call."); } } @@ -456,7 +456,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe if (periodicRefreshOffersTimer == null) startPeriodicRefreshOffersTimer(); } else { - log.warn("We have stopped already. We ignore that offerBookService.republishOffers.onSuccess call."); + log.debug("We have stopped already. We ignore that offerBookService.republishOffers.onSuccess call."); } }, errorMessage -> { @@ -466,7 +466,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe retryRepublishOffersTimer = UserThread.runAfter(OpenOfferManager.this::republishOffers, RETRY_REPUBLISH_DELAY_SEC); } else { - log.warn("We have stopped already. We ignore that offerBookService.republishOffers.onFault call."); + log.debug("We have stopped already. We ignore that offerBookService.republishOffers.onFault call."); } }); openOffer.setStorage(openOffersStorage); @@ -480,7 +480,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe if (!stopped) { republishOffers(); } else { - log.warn("We have stopped already. We ignore that periodicRepublishOffersTimer.run call."); + log.debug("We have stopped already. We ignore that periodicRepublishOffersTimer.run call."); } }, REPUBLISH_INTERVAL_MS, @@ -503,19 +503,20 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe final ArrayList openOffersList = new ArrayList<>(openOffers); for (int i = 0; i < size; i++) { // we delay to avoid reaching throttle limits - // roughly 1 offer per second - final int n = i; - final long minDelay = i * 500 + 1; - final long maxDelay = minDelay * 2 + 500; + // roughly 4 offers per second + + long delay = 150; + final long minDelay = (i + 1) * delay; + final long maxDelay = (i + 2) * delay; + final OpenOffer openOffer = openOffersList.get(i); UserThread.runAfterRandomDelay(() -> { - OpenOffer openOffer = openOffersList.get(n); // we need to check if in the meantime the offer has been removed if (openOffers.contains(openOffer)) refreshOffer(openOffer); }, minDelay, maxDelay, TimeUnit.MILLISECONDS); } } else { - log.warn("We have stopped already. We ignore that periodicRefreshOffersTimer.run call."); + log.debug("We have stopped already. We ignore that periodicRefreshOffersTimer.run call."); } }, REFRESH_INTERVAL_MS, diff --git a/core/src/main/java/io/bitsquare/trade/protocol/availability/messages/OfferAvailabilityRequest.java b/core/src/main/java/io/bitsquare/trade/protocol/availability/messages/OfferAvailabilityRequest.java index ccb784760d..590b9a9fd9 100644 --- a/core/src/main/java/io/bitsquare/trade/protocol/availability/messages/OfferAvailabilityRequest.java +++ b/core/src/main/java/io/bitsquare/trade/protocol/availability/messages/OfferAvailabilityRequest.java @@ -17,15 +17,24 @@ package io.bitsquare.trade.protocol.availability.messages; +import io.bitsquare.app.Capabilities; 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; + +// We add here the SupportedCapabilitiesMessage interface as that message always predates a direct connection +// to the trading peer +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; private final PubKeyRing pubKeyRing; public final long takersTradePrice; + @Nullable + private ArrayList supportedCapabilities = Capabilities.getCapabilities(); public OfferAvailabilityRequest(String offerId, PubKeyRing pubKeyRing, long takersTradePrice) { super(offerId); @@ -33,6 +42,12 @@ public final class OfferAvailabilityRequest extends OfferMessage { this.takersTradePrice = takersTradePrice; } + @Override + @Nullable + public ArrayList getSupportedCapabilities() { + return supportedCapabilities; + } + public PubKeyRing getPubKeyRing() { return pubKeyRing; } diff --git a/core/src/main/java/io/bitsquare/trade/protocol/availability/messages/OfferAvailabilityResponse.java b/core/src/main/java/io/bitsquare/trade/protocol/availability/messages/OfferAvailabilityResponse.java index e40f1a18f5..e17b8b262d 100644 --- a/core/src/main/java/io/bitsquare/trade/protocol/availability/messages/OfferAvailabilityResponse.java +++ b/core/src/main/java/io/bitsquare/trade/protocol/availability/messages/OfferAvailabilityResponse.java @@ -17,10 +17,18 @@ package io.bitsquare.trade.protocol.availability.messages; + +import io.bitsquare.app.Capabilities; 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; + +// We add here the SupportedCapabilitiesMessage interface as that message always predates a direct connection +// to the trading peer +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; @@ -28,6 +36,8 @@ public final class OfferAvailabilityResponse extends OfferMessage { // TODO keep for backward compatibility. Can be removed once everyone is on v0.4.9 public boolean isAvailable; + @Nullable + private ArrayList supportedCapabilities = Capabilities.getCapabilities(); public OfferAvailabilityResponse(String offerId, AvailabilityResult availabilityResult) { super(offerId); @@ -35,6 +45,12 @@ public final class OfferAvailabilityResponse extends OfferMessage { isAvailable = availabilityResult == AvailabilityResult.AVAILABLE; } + @Override + @Nullable + public ArrayList getSupportedCapabilities() { + return supportedCapabilities; + } + @Override public String toString() { return "OfferAvailabilityResponse{" + diff --git a/core/src/main/java/io/bitsquare/trade/protocol/availability/messages/OfferMessage.java b/core/src/main/java/io/bitsquare/trade/protocol/availability/messages/OfferMessage.java index c96b082367..98b87d7a2c 100644 --- a/core/src/main/java/io/bitsquare/trade/protocol/availability/messages/OfferMessage.java +++ b/core/src/main/java/io/bitsquare/trade/protocol/availability/messages/OfferMessage.java @@ -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; diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/BuyerAsOffererProtocol.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/BuyerAsOffererProtocol.java index 799e1823a8..eb6fc8511e 100644 --- a/core/src/main/java/io/bitsquare/trade/protocol/trade/BuyerAsOffererProtocol.java +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/BuyerAsOffererProtocol.java @@ -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(); } diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/BuyerAsTakerProtocol.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/BuyerAsTakerProtocol.java index 8fe8970907..fd5c6179f4 100644 --- a/core/src/main/java/io/bitsquare/trade/protocol/trade/BuyerAsTakerProtocol.java +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/BuyerAsTakerProtocol.java @@ -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(); } diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/SellerAsOffererProtocol.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/SellerAsOffererProtocol.java index 01b692cf9b..da7a247869 100644 --- a/core/src/main/java/io/bitsquare/trade/protocol/trade/SellerAsOffererProtocol.java +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/SellerAsOffererProtocol.java @@ -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(); } diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/SellerAsTakerProtocol.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/SellerAsTakerProtocol.java index 30b64125c9..aaf36a292a 100644 --- a/core/src/main/java/io/bitsquare/trade/protocol/trade/SellerAsTakerProtocol.java +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/SellerAsTakerProtocol.java @@ -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(); } diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/messages/TradeMessage.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/messages/TradeMessage.java index f8bb508f80..17d64481f3 100644 --- a/core/src/main/java/io/bitsquare/trade/protocol/trade/messages/TradeMessage.java +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/messages/TradeMessage.java @@ -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; @@ -38,7 +39,6 @@ public abstract class TradeMessage implements DirectMessage { TradeMessage that = (TradeMessage) o; return !(tradeId != null ? !tradeId.equals(that.tradeId) : that.tradeId != null); - } @Override diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/offerer/PublishTradeStatistics.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/offerer/PublishTradeStatistics.java new file mode 100644 index 0000000000..19a65bb4d3 --- /dev/null +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/offerer/PublishTradeStatistics.java @@ -0,0 +1,47 @@ +/* + * 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 . + */ + +package io.bitsquare.trade.protocol.trade.tasks.offerer; + +import io.bitsquare.common.taskrunner.TaskRunner; +import io.bitsquare.trade.Trade; +import io.bitsquare.trade.protocol.trade.tasks.TradeTask; +import io.bitsquare.trade.statistics.TradeStatistics; + +public class PublishTradeStatistics extends TradeTask { + public PublishTradeStatistics(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + // Offerer is responsible for publishing. Only in case the offerer uses an old verison the taker publishes. + 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); + } + } +} diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/PublishTradeStatistics.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/PublishTradeStatistics.java new file mode 100644 index 0000000000..183ee30a72 --- /dev/null +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/PublishTradeStatistics.java @@ -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 . + */ + +package io.bitsquare.trade.protocol.trade.tasks.taker; + +import io.bitsquare.common.taskrunner.TaskRunner; +import io.bitsquare.trade.Trade; +import io.bitsquare.trade.protocol.trade.tasks.TradeTask; +import io.bitsquare.trade.statistics.TradeStatistics; +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 requiredCapabilities = tradeStatistics.getRequiredCapabilities(); + final List 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.warn("We publish tradeStatistics because the offerer uses an old version."); + processModel.getP2PService().addData(tradeStatistics, true); + } else { + log.trace("We do not publish tradeStatistics because the offerer support the capabilities."); + } + }); + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/io/bitsquare/trade/statistics/TradeStatistics.java b/core/src/main/java/io/bitsquare/trade/statistics/TradeStatistics.java new file mode 100644 index 0000000000..ca3481c590 --- /dev/null +++ b/core/src/main/java/io/bitsquare/trade/statistics/TradeStatistics.java @@ -0,0 +1,160 @@ +package io.bitsquare.trade.statistics; + +import io.bitsquare.app.Capabilities; +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(30); + + 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 getRequiredCapabilities() { + return Arrays.asList( + Capabilities.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 + + '}'; + } +} diff --git a/core/src/main/java/io/bitsquare/trade/statistics/TradeStatisticsManager.java b/core/src/main/java/io/bitsquare/trade/statistics/TradeStatisticsManager.java new file mode 100644 index 0000000000..0fc31ff066 --- /dev/null +++ b/core/src/main/java/io/bitsquare/trade/statistics/TradeStatisticsManager.java @@ -0,0 +1,93 @@ +package io.bitsquare.trade.statistics; + +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> storage; + private Storage jsonStorage; + private boolean dumpStatistics; + private ObservableSet observableTradeStatisticsSet = FXCollections.observableSet(); + private HashSet tradeStatisticsSet = new HashSet<>(); + + @Inject + public TradeStatisticsManager(Storage> storage, Storage 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 persisted = storage.initAndGetPersistedWithFileName("TradeStatistics"); + if (persisted != null) + persisted.stream().forEach(this::add); + + 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); + + dump(); + } 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 getObservableTradeStatisticsSet() { + return observableTradeStatisticsSet; + } + + private void dump() { + 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 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); + } + } + +} diff --git a/core/src/main/java/io/bitsquare/user/Preferences.java b/core/src/main/java/io/bitsquare/user/Preferences.java index 141704b031..22070ac55d 100644 --- a/core/src/main/java/io/bitsquare/user/Preferences.java +++ b/core/src/main/java/io/bitsquare/user/Preferences.java @@ -25,6 +25,7 @@ import io.bitsquare.btc.FeePolicy; import io.bitsquare.common.persistance.Persistable; import io.bitsquare.common.util.Utilities; import io.bitsquare.locale.*; +import io.bitsquare.network.NetworkOptionKeys; import io.bitsquare.storage.Storage; import io.nucleo.net.bridge.BridgeProvider; import javafx.beans.Observable; @@ -43,6 +44,7 @@ import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import javax.inject.Inject; +import javax.inject.Named; import java.util.*; import java.util.stream.Collectors; @@ -111,8 +113,8 @@ public final class Preferences implements Persistable { private boolean autoSelectArbitrators = true; private final Map dontShowAgainMap; private boolean tacAccepted; - // Don't remove as we don't want to break old serialized data private boolean useTorForBitcoinJ = false; + private boolean useTorForHttpRequests = true; private boolean showOwnOffersInOfferBook = true; private Locale preferredLocale; private TradeCurrency preferredTradeCurrency; @@ -120,8 +122,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 peerTagMap = new HashMap<>(); @@ -134,6 +139,7 @@ public final class Preferences implements Persistable { transient private final StringProperty btcDenominationProperty = new SimpleStringProperty(btcDenomination); transient private final BooleanProperty useAnimationsProperty = new SimpleBooleanProperty(useAnimations); transient private final BooleanProperty useInvertedMarketPriceProperty = new SimpleBooleanProperty(useInvertedMarketPrice); + transient private final BooleanProperty useTorForHttpRequestsProperty = new SimpleBooleanProperty(useTorForHttpRequests); transient private final ObservableList fiatCurrenciesAsObservable = FXCollections.observableArrayList(); transient private final ObservableList cryptoCurrenciesAsObservable = FXCollections.observableArrayList(); transient private final ObservableList tradeCurrenciesAsObservable = FXCollections.observableArrayList(); @@ -144,7 +150,8 @@ public final class Preferences implements Persistable { /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public Preferences(Storage storage, BitsquareEnvironment bitsquareEnvironment) { + public Preferences(Storage storage, BitsquareEnvironment bitsquareEnvironment, + @Named(NetworkOptionKeys.USE_TOR_FOR_HTTP) String useTorForHttpFromOptions) { log.debug("Preferences " + this); INSTANCE = this; this.storage = storage; @@ -185,8 +192,12 @@ public final class Preferences implements Persistable { defaultLocale = preferredLocale; preferredTradeCurrency = persisted.getPreferredTradeCurrency(); defaultTradeCurrency = preferredTradeCurrency; - // useTorForBitcoinJ = persisted.getUseTorForBitcoinJ(); - useTorForBitcoinJ = false; + useTorForBitcoinJ = persisted.getUseTorForBitcoinJ(); + if (useTorForHttpFromOptions.isEmpty()) + setUseTorForHttpRequests(persisted.useTorForHttpRequests); + else + setUseTorForHttpRequests(useTorForHttpFromOptions.toLowerCase().equals("true")); + useStickyMarketPrice = persisted.getUseStickyMarketPrice(); usePercentageBasedPrice = persisted.getUsePercentageBasedPrice(); showOwnOffersInOfferBook = persisted.getShowOwnOffersInOfferBook(); @@ -207,6 +218,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(); @@ -236,16 +249,16 @@ public final class Preferences implements Persistable { // Use that to guarantee update of the serializable field and to make a storage update in case of a change btcDenominationProperty.addListener((ov) -> { btcDenomination = btcDenominationProperty.get(); - storage.queueUpForSave(2000); + storage.queueUpForSave(); }); useAnimationsProperty.addListener((ov) -> { useAnimations = useAnimationsProperty.get(); staticUseAnimations = useAnimations; - storage.queueUpForSave(2000); + storage.queueUpForSave(); }); useInvertedMarketPriceProperty.addListener((ov) -> { useInvertedMarketPrice = useInvertedMarketPriceProperty.get(); - storage.queueUpForSave(2000); + storage.queueUpForSave(); }); fiatCurrenciesAsObservable.addListener((Observable ov) -> { fiatCurrencies.clear(); @@ -257,21 +270,25 @@ public final class Preferences implements Persistable { cryptoCurrencies.addAll(cryptoCurrenciesAsObservable); storage.queueUpForSave(); }); + useTorForHttpRequestsProperty.addListener((ov) -> { + useTorForHttpRequests = useTorForHttpRequestsProperty.get(); + storage.queueUpForSave(); + }); - fiatCurrenciesAsObservable.addListener((ListChangeListener) this::updateTradeCurrencies); - cryptoCurrenciesAsObservable.addListener((ListChangeListener) this::updateTradeCurrencies); + fiatCurrenciesAsObservable.addListener(this::updateTradeCurrencies); + cryptoCurrenciesAsObservable.addListener(this::updateTradeCurrencies); tradeCurrenciesAsObservable.addAll(fiatCurrencies); tradeCurrenciesAsObservable.addAll(cryptoCurrencies); } public void dontShowAgain(String key, boolean dontShowAgain) { dontShowAgainMap.put(key, dontShowAgain); - storage.queueUpForSave(1000); + storage.queueUpForSave(); } public void resetDontShowAgainForType() { dontShowAgainMap.clear(); - storage.queueUpForSave(1000); + storage.queueUpForSave(); } @@ -288,7 +305,7 @@ public final class Preferences implements Persistable { } public void setUseInvertedMarketPrice(boolean useInvertedMarketPrice) { - this.useInvertedMarketPriceProperty.set(useInvertedMarketPrice); + useInvertedMarketPriceProperty.set(useInvertedMarketPrice); } public void setBitcoinNetwork(BitcoinNetwork bitcoinNetwork) { @@ -372,10 +389,10 @@ public final class Preferences implements Persistable { storage.queueUpForSave(); } - /* public void setUseTorForBitcoinJ(boolean useTorForBitcoinJ) { + public void setUseTorForBitcoinJ(boolean useTorForBitcoinJ) { this.useTorForBitcoinJ = useTorForBitcoinJ; storage.queueUpForSave(); - }*/ + } public void setShowOwnOffersInOfferBook(boolean showOwnOffersInOfferBook) { this.showOwnOffersInOfferBook = showOwnOffersInOfferBook; @@ -457,6 +474,19 @@ 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(); + } + + public void setUseTorForHttpRequests(boolean useTorForHttpRequests) { + useTorForHttpRequestsProperty.set(useTorForHttpRequests); + } /////////////////////////////////////////////////////////////////////////////////////////// // Getter @@ -609,6 +639,24 @@ public final class Preferences implements Persistable { public String getDefaultPath() { return defaultPath; } + + public String getTradeStatisticsScreenCurrencyCode() { + return tradeStatisticsScreenCurrencyCode; + } + + public int getTradeStatisticsTickUnitIndex() { + return tradeStatisticsTickUnitIndex; + } + + public boolean getUseTorForHttpRequests() { + return useTorForHttpRequestsProperty.get(); + } + + public BooleanProperty useTorForHttpRequestsProperty() { + return useTorForHttpRequestsProperty; + } + + /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/test/java/io/bitsquare/btc/blockchain/BlockchainServiceTest.java b/core/src/test/java/io/bitsquare/btc/blockchain/BlockchainServiceTest.java index d4b82f48a7..7f6082f6c3 100644 --- a/core/src/test/java/io/bitsquare/btc/blockchain/BlockchainServiceTest.java +++ b/core/src/test/java/io/bitsquare/btc/blockchain/BlockchainServiceTest.java @@ -18,7 +18,7 @@ public class BlockchainServiceTest { @Test public void testGetFee() throws InterruptedException { - BlockchainService blockchainService = new BlockchainService(); + BlockchainService blockchainService = new BlockchainService(null, null, null); // that tx has 0.001 BTC as fee String transactionId = "38d176d0b1079b99fcb59859401d6b1679d2fa18fd8989d2c244b3682e52fce6"; diff --git a/core/src/test/java/io/bitsquare/btc/pricefeed/MarketPriceFeedTest.java b/core/src/test/java/io/bitsquare/btc/pricefeed/MarketPriceFeedServiceTest.java similarity index 72% rename from core/src/test/java/io/bitsquare/btc/pricefeed/MarketPriceFeedTest.java rename to core/src/test/java/io/bitsquare/btc/pricefeed/MarketPriceFeedServiceTest.java index ef49d2098d..d2d95b83fc 100644 --- a/core/src/test/java/io/bitsquare/btc/pricefeed/MarketPriceFeedTest.java +++ b/core/src/test/java/io/bitsquare/btc/pricefeed/MarketPriceFeedServiceTest.java @@ -8,14 +8,14 @@ import org.slf4j.LoggerFactory; import static junit.framework.TestCase.assertTrue; @Ignore -public class MarketPriceFeedTest { - private static final Logger log = LoggerFactory.getLogger(MarketPriceFeedTest.class); +public class MarketPriceFeedServiceTest { + private static final Logger log = LoggerFactory.getLogger(MarketPriceFeedServiceTest.class); @Test public void testGetPrice() throws InterruptedException { - PriceFeed priceFeed = new PriceFeed(); - priceFeed.setCurrencyCode("EUR"); - priceFeed.init(tradeCurrency -> { + PriceFeedService priceFeedService = new PriceFeedService(null, null); + priceFeedService.setCurrencyCode("EUR"); + priceFeedService.init(tradeCurrency -> { log.debug(tradeCurrency.toString()); assertTrue(true); }, diff --git a/doc/build.md b/doc/build.md index f15c45628e..49b46386d1 100644 --- a/doc/build.md +++ b/doc/build.md @@ -5,43 +5,19 @@ This guide will walk you through the process of building Bitsquare from source. > _**NOTE:** For most users, building from source is not necessary. See the [releases page](https://github.com/bitsquare/bitsquare/releases), where you'll find installers for Windows, Linux and Mac OS X._ - -For the impatient ------------------ - -What follows is explained in detail in the sections below, but for those who know their way around Java, git and Maven, here are the instructions in a nutshell: - - $ javac -version - javac 1.8.0_66 # must be 1.8.0_66 or better - - $ git clone -b FixBloomFilters https://github.com/bitsquare/bitcoinj.git - $ cd bitcoinj - $ mvn clean install -DskipTests -Dmaven.javadoc.skip=true - - $ git clone https://github.com/bitsquare/bitsquare.git - $ cd bitsquare - $ mvn clean package -DskipTests - -When the build completes, you will find an executable jar: `gui/target/shaded.jar`. -To run it use: - $ java -jar gui/target/shaded.jar - -To build the binary check out the build scripts under the package directory. - -Prerequisites +System requirements ------------- -The only prerequisite for building Bitsquare is installing the Java Development Kit (JDK), version 8u40 or better (as well as maven and git). +The prerequisite for building Bitsquare is installing the Java Development Kit (JDK), version 8u66 or better (as well as maven and git). In Debian/Ubuntu systems with OpenJDK you'll need OpenJFX as well, i.e. you'll need the `openjfx` package besides the `openjdk-8-jdk` package. -##### 1. Check the version of Java you currently have installed +### 1. Check the version of Java you currently have installed - $ javac -version - javac 1.8.0_66 + $ java -version -If `javac` is not found, or your version is anything less than `1.8.0_66`, then follow the next steps, otherwise you can skip to step 2: +If `java` is not found, or your version is anything less than `1.8.0_66`, then follow the next steps, otherwise you can skip to step 2: -###### 1.1 Debian based systems (Ubuntu) +#### 1.1 Debian based systems (Ubuntu) To install OpenJDK use: @@ -51,8 +27,12 @@ To install the Oracle JDK use: $ sudo add-apt-repository ppa:webupd8team/java $ sudo apt-get update - $ sudo apt-get install oracle-java8-installer + $ sudo apt-get -y oracle-java8-installer install +Check if $JAVA_HOME is set + + $ echo $JAVA_HOME + If $JAVA_HOME is not present, add it to the .bashrc file $ touch .bashrc @@ -60,97 +40,115 @@ If $JAVA_HOME is not present, add it to the .bashrc file $ export JAVA_HOME=/usr/lib/jvm/java-8-oracle $ echo $JAVA_HOME -###### 1.2 Other systems +#### 1.2 Other systems [Download and install the latest JDK]( http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) for your platform. - -##### 2. Enable unlimited Strength for cryptographic keys - -Bitsquare uses 256 bit length keys which are still not permitted by default. -Get around that ridiculous fact by adding the missing [jars from Oracle](http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html). -Please follow the steps described in the Readme file at the downloaded package. -You will get an error when building Bitsquare package if you don't have these. - -##### 3. Copy the BountyCastle provider jar file - -Copy the BountyCastle provider jar file (bcprov-jdk15on-1.53.jar) from you local maven repository (/home/.m2/repository/org/bouncycastle/bcprov-jdk15on/1.53/bcprov-jdk15on-1.53.jar) to $JavaHome/jre/lib/ext/. -This prevent a "JCE cannot authenticate the provider BC" exception when starting the Bitsquare client. - -##### 4. Edit the jre\lib\security\java.security file to add BouncyCastleProvider - -Add org.bouncycastle.jce.provider.BouncyCastleProvider as last entry at: List of providers and their preference orders -E.g.: -security.provider.10=org.bouncycastle.jce.provider.BouncyCastleProvider - -##### 5. Copy the jdkfix jar file - -Copy the jdkfix jar file (lib/jdkfix-0.4.9.jar) from the Bitsquare src directory to $JavaHome/jre/lib/ext/. -Jdkfix.jar include a bugfix of the SortedList class which will be released with the next JDK version. -As we need to load that before the default java class we need that hack. - - -Steps ------ - -### 1. Get the source - -The preferred approach is to clone the Bitsquare repository using [git](http://www.git-scm.com/): - - git clone https://github.com/bitsquare/bitsquare.git - -However, if you're not familiar with git or it is otherwise inconvenient to use, you can also download and extract a zip file of the latest sources at https://github.com/bitsquare/bitsquare/archive/master.zip. - - +Build bitcoinj +----------------- ### 2. Install bitcoinj fork -Versions later than 0.13.1 has removed support for Java serialisation. +> _**NOTE:** +Bitcoinj versions later than 0.13.1 has removed support for Java serialisation. In version 0.13.1 is also missing support for Java serialisation in MainNetParams (HttpDiscovery.Details). -We removed usage of Cartographer/HttpDiscovery and fixed privacy issues with Bloom Filters at our [fork version 0.13.1.4](https://github.com/bitsquare/bitcoinj/tree/FixBloomFilters). +We removed usage of Cartographer/HttpDiscovery and fixed privacy issues with Bloom Filters at our [fork version 0.13.1.5](https://github.com/bitsquare/bitcoinj/tree/FixBloomFilters). Beside the Java serialisation issues there are [privacy concerns](http://bitcoin-development.narkive.com/hczWIAby/bitcoin-development-cartographer#post3) regarding Cartographer. -Here is a Github issue with background and open tasks regarding [Bloom Filters](https://github.com/bitsquare/bitsquare/issues/414). +Here is a Github issue with background and open tasks regarding [Bloom Filters](https://github.com/bitsquare/bitsquare/issues/414)._ $ git clone -b FixBloomFilters https://github.com/bitsquare/bitcoinj.git $ cd bitcoinj $ mvn clean install -DskipTests -Dmaven.javadoc.skip=true -### 3. Build jar +Prepare Bitsquare build +----------------- -Bitsquare uses maven as a build system. +### 3. Get Bitsquare source code and build a preliminary Bitsquare version +You need to get the Bitsquare dependencies first as we need to copy the BountyCastle jar to the JRE directory as well as the jdkfix jar. + + $ git clone https://github.com/bitsquare/bitsquare.git $ cd bitsquare - $ mvn clean package -DskipTests + $ mvn clean package -DskipTests -Dmaven.javadoc.skip=true + +### 4. Copy the jdkfix jar file + +Copy the jdkfix-0.4.9.1.jar from the Bitsquare jdkfix/target directory to $JAVA_HOME/jre/lib/ext/. +jdkfix-0.4.9.1.jar includes a bugfix of the SortedList class which will be released with the next JDK version. +We need to load that class before the default java class. This step will be removed once the bugfix is in the official JDK. + + $ sudo cp bitsquare/jdkfix/target/jdkfix-0.4.9.1.jar $JAVA_HOME/jre/lib/ext/jdkfix-0.4.9.1.jar -### 4. Run +### 5. Copy the BountyCastle provider jar file +Copy the BountyCastle provider jar file from the local maven repository to the jre/lib/ext directory. +This prevents a "JCE cannot authenticate the provider BC" exception when starting the Bitsquare client. + + $ sudo cp ~/.m2/repository/org/bouncycastle/bcprov-jdk15on/1.53/bcprov-jdk15on-1.53.jar $JAVA_HOME/jre/lib/ext/bcprov-jdk15on-1.53.jar + +### 6. Edit the java.security file and add BouncyCastleProvider + +Add org.bouncycastle.jce.provider.BouncyCastleProvider as last entry at: List of providers and their preference orders +E.g.: +security.provider.10=org.bouncycastle.jce.provider.BouncyCastleProvider + + $ sudo gedit $JAVA_HOME/jre/lib/security/java.security + ... edit and save + +### 7. Enable unlimited Strength for cryptographic keys (if Oracle JDK is used) + +If you are using Oracle JDK you need to follow the following step. If you use OpenJDK + OpenJFX you can skip that step. +Bitsquare uses 256 bit length keys which are still not permitted by default. +Get around that ridiculous fact by adding the missing [jars from Oracle](http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html). +Please follow the steps described in the Readme file at the downloaded package. +You will get an error when building Bitsquare package if you don't have these. + + $ wget --no-check-certificate --no-cookies --header "Cookie: oraclelicense=accept-securebackup-cookie" http://download.oracle.com/otn-pub/java/jce/8/jce_policy-8.zip + $ unzip jce_policy-8.zip + $ sudo cp UnlimitedJCEPolicyJDK8/US_export_policy.jar $JAVA_HOME/jre/lib/security/US_export_policy.jar + $ sudo cp UnlimitedJCEPolicyJDK8/local_policy.jar $JAVA_HOME/jre/lib/security/local_policy.jar + +Build Bitsquare +----------------- + +### 8. Build final Bitsquare jar + +Now we have all prepared to build the correct Bitsquare jar. + + $ mvn clean package -DskipTests -Dmaven.javadoc.skip=true + When the build completes, you will find an executable jar: `gui/target/shaded.jar`. To run it use: $ java -jar gui/target/shaded.jar - -Please note that testnet is the default bitcoin network. - -### 5. Development mode + +Build binaries +----------------- + +If you want to build the binaryies check out the build scripts under the package directory. + +Development mode +----------------- -Please check out our wiki for more information about [testing](https://github.com/bitsquare/bitsquare/wiki/Testing-Bitsquare-with-Testnet) +Please check out our wiki for more information about [testing](https://github.com/bitsquare/bitsquare/wiki/Testing-Bitsquare-with-Mainnet) and how to use [regtest](https://github.com/bitsquare/bitsquare/wiki/How-to-use-Bitsquare-with-regtest-%28advanced%29) -Here are example program arguments for using regtest with localhost environment (not via Tor): +Here are example program arguments for using regtest with localhost environment (not using Tor): - $ java -jar seednode/target/SeedNode.jar --useLocalhost=true --nodePort=2002 --bitcoinNetwork=regtest + $ java -jar seednode/target/SeedNode.jar --bitcoinNetwork=REGTEST --useLocalhost=true --myAddress=localhost:2002 --nodePort=2002 --appName=Bitsquare_seed_node_localhost_2002 - $ java -jar gui/target/shaded.jar --useLocalhost=true --nodePort=2222 --devTest=true --appName=Bitsquare-Local-Regtest-Arbitrator + $ java -jar gui/target/shaded.jar --bitcoinNetwork=REGTEST --useLocalhost=true --myAddress=localhost:2222 --nodePort=2222 --appName=Bitsquare-Local-Regtest-Arbitrator - $ java -jar gui/target/shaded.jar --bitcoinNetwork=regtest --nodePort=3332 --useLocalhost=true --devTest=true --appName=Bitsquare-Local-Regtest-Alice + $ java -jar gui/target/shaded.jar --bitcoinNetwork=REGTEST --useLocalhost=true --myAddress=localhost:3333 --nodePort=3333 --appName=Bitsquare-Local-Regtest-Alice - $ java -jar gui/target/shaded.jar --bitcoinNetwork=regtest --nodePort=4442 --useLocalhost=true --devTest=true --appName=Bitsquare-Local-Regtest-Bob + $ java -jar gui/target/shaded.jar --bitcoinNetwork=REGTEST --useLocalhost=true --myAddress=localhost:4444 --nodePort=4444 --appName=Bitsquare-Local-Regtest-Bob -### 6. Running local seed node with Tor +Running local seed node with Tor and RegTest +----------------- If you want to run locally a seed node via Tor you need to add your seed node's hidden service address to the SeedNodesRepository.java class. You can find the hidden service address after you started once a seed node. Start it with a placeholder address like: - $ java -jar seednode/target/SeedNode.jar --bitcoinNetwork=REGTEST --myAddress=xxxxxxx.onion:8002 --appName=Bitsquare_seed_node_xxxxxxx.onion_8002 --nodePort=8002 + $ java -jar seednode/target/SeedNode.jar --bitcoinNetwork=REGTEST --nodePort=8002 --myAddress=xxxxxxxx.onion:8002 --appName=Bitsquare_seed_node_xxxxxxxx.onion_8000 Once the hidden service is published (check console output) quit the seed node and copy the hidden service address from the console output. Alternatively you can navigate to the application directory and open Bitsquare_seed_node_xxxxxxx.onion_8002/tor/hiddenservice/hostname. @@ -158,15 +156,18 @@ use that hidden service address also to rename the xxxxxxx placeholder of your B Start again the SeedNode.jar now with the correct hidden service address. Instructions are also at the SeedNodesRepository class. -Here are example program arguments for using regtest and using the Tor network: +Here are example program arguments for using regtest and using the Tor network (example onion address is ewdkppp3vicnbgqt): - $ java -jar seednode/target/SeedNode.jar --bitcoinNetwork=REGTEST --myAddress=rxdkppp3vicnbgqt.onion:8002 --appName=Bitsquare_seed_node_rxdkppp3vicnbgqt.onion_8002 --nodePort=8002 + $ java -jar seednode/target/SeedNode.jar ewdkppp3vicnbgqt.onion:8002 2 50 - $ java -jar gui/target/shaded.jar --bitcoinNetwork=regtest nodePort=2222 --devTest=true --appName=Bitsquare-Tor-Regtest-Arbitrator - - $ java -jar gui/target/shaded.jar --bitcoinNetwork=regtest nodePort=3332 --devTest=true --appName=Bitsquare-Tor-Regtest-Alice - - $ java -jar gui/target/shaded.jar --bitcoinNetwork=regtest nodePort=4442 --devTest=true --appName=Bitsquare-Tor-Regtest-Bob + $ java -jar seednode/target/SeedNode.jar --bitcoinNetwork=REGTEST --nodePort=8002 --myAddress=ewdkppp3vicnbgqt.onion:8002 --appName=Bitsquare_seed_node_ewdkppp3vicnbgqt.oinion_8002 + + $ java -jar gui/target/shaded.jar --bitcoinNetwork=REGTEST --myAddress=localhost:2222 --nodePort=2222 --appName=Bitsquare-Local-Regtest-Arbitrator + + $ java -jar gui/target/shaded.jar --bitcoinNetwork=REGTEST --myAddress=localhost:3333 --nodePort=3333 --appName=Bitsquare-Local-Regtest-Alice + + $ java -jar gui/target/shaded.jar --bitcoinNetwork=REGTEST --myAddress=localhost:4444 --nodePort=4444 --appName=Bitsquare-Local-Regtest-Bob + Problems? --------- diff --git a/gui/pom.xml b/gui/pom.xml index d0f9401222..cf870b7bef 100644 --- a/gui/pom.xml +++ b/gui/pom.xml @@ -22,7 +22,7 @@ parent io.bitsquare - 0.4.9 + 0.4.9.1 4.0.0 diff --git a/gui/src/main/java/io/bitsquare/app/BitsquareApp.java b/gui/src/main/java/io/bitsquare/app/BitsquareApp.java index 92e37ccd38..fdfadb32a2 100644 --- a/gui/src/main/java/io/bitsquare/app/BitsquareApp.java +++ b/gui/src/main/java/io/bitsquare/app/BitsquareApp.java @@ -24,7 +24,7 @@ import com.google.inject.Injector; import io.bitsquare.alert.AlertManager; import io.bitsquare.arbitration.ArbitratorManager; import io.bitsquare.btc.WalletService; -import io.bitsquare.common.OptionKeys; +import io.bitsquare.common.CommonOptionKeys; import io.bitsquare.common.UserThread; import io.bitsquare.common.handlers.ResultHandler; import io.bitsquare.common.util.Utilities; @@ -45,6 +45,7 @@ import io.bitsquare.gui.main.overlays.windows.SendAlertMessageWindow; import io.bitsquare.gui.util.ImageUtil; import io.bitsquare.p2p.P2PService; import io.bitsquare.storage.Storage; +import io.bitsquare.trade.TradeManager; import io.bitsquare.trade.offer.OpenOfferManager; import javafx.application.Application; import javafx.application.Platform; @@ -77,7 +78,7 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; -import static io.bitsquare.app.BitsquareEnvironment.APP_NAME_KEY; +import static io.bitsquare.app.CoreOptionKeys.APP_NAME_KEY; public class BitsquareApp extends Application { private static final Logger log = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(BitsquareApp.class); @@ -104,12 +105,12 @@ public class BitsquareApp extends Application { public void start(Stage stage) throws IOException { BitsquareApp.primaryStage = stage; - String logPath = Paths.get(env.getProperty(BitsquareEnvironment.APP_DATA_DIR_KEY), "bitsquare").toString(); + String logPath = Paths.get(env.getProperty(CoreOptionKeys.APP_DATA_DIR_KEY), "bitsquare").toString(); Log.setup(logPath); log.info("Log files under: " + logPath); Version.printVersion(); Utilities.printSysInfo(); - Log.setLevel(Level.toLevel(env.getRequiredProperty(OptionKeys.LOG_LEVEL_KEY))); + Log.setLevel(Level.toLevel(env.getRequiredProperty(CommonOptionKeys.LOG_LEVEL_KEY))); UserThread.setExecutor(Platform::runLater); UserThread.setTimerClass(UITimer.class); @@ -174,7 +175,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); @@ -346,20 +348,21 @@ public class BitsquareApp extends Application { @Override public void stop() { - shutDownRequested = true; - - new Popup().headLine("Shut down in progress") - .backgroundInfo("Shutting down application can take a few seconds.\n" + - "Please don't interrupt that process.") - .hideCloseButton() - .useAnimation(false) - .show(); - UserThread.runAfter(() -> { - gracefulShutDown(() -> { - log.info("App shutdown complete"); - System.exit(0); - }); - }, 200, TimeUnit.MILLISECONDS); + if (!shutDownRequested) { + new Popup().headLine("Shut down in progress") + .backgroundInfo("Shutting down application can take a few seconds.\n" + + "Please don't interrupt that process.") + .hideCloseButton() + .useAnimation(false) + .show(); + UserThread.runAfter(() -> { + gracefulShutDown(() -> { + log.info("App shutdown complete"); + System.exit(0); + }); + }, 200, TimeUnit.MILLISECONDS); + shutDownRequested = true; + } } private void gracefulShutDown(ResultHandler resultHandler) { @@ -368,6 +371,7 @@ public class BitsquareApp extends Application { if (injector != null) { injector.getInstance(ArbitratorManager.class).shutDown(); injector.getInstance(MainViewModel.class).shutDown(); + injector.getInstance(TradeManager.class).shutDown(); injector.getInstance(OpenOfferManager.class).shutDown(() -> { injector.getInstance(P2PService.class).shutDown(() -> { injector.getInstance(WalletService.class).shutDownDone.addListener((ov, o, n) -> { @@ -378,8 +382,8 @@ public class BitsquareApp extends Application { injector.getInstance(WalletService.class).shutDown(); }); }); - // we wait max 5 sec. - UserThread.runAfter(resultHandler::handleResult, 5); + // we wait max 20 sec. + UserThread.runAfter(resultHandler::handleResult, 20); } else { UserThread.runAfter(resultHandler::handleResult, 1); } diff --git a/gui/src/main/java/io/bitsquare/app/BitsquareAppMain.java b/gui/src/main/java/io/bitsquare/app/BitsquareAppMain.java index 561a56c081..cc901765a8 100644 --- a/gui/src/main/java/io/bitsquare/app/BitsquareAppMain.java +++ b/gui/src/main/java/io/bitsquare/app/BitsquareAppMain.java @@ -23,7 +23,8 @@ import joptsimple.OptionSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static io.bitsquare.app.BitsquareEnvironment.*; +import static io.bitsquare.app.BitsquareEnvironment.DEFAULT_APP_NAME; +import static io.bitsquare.app.BitsquareEnvironment.DEFAULT_USER_DATA_DIR; public class BitsquareAppMain extends BitsquareExecutable { private static final Logger log = LoggerFactory.getLogger(BitsquareAppMain.class); @@ -33,9 +34,9 @@ public class BitsquareAppMain extends BitsquareExecutable { // So we only handle the absolute minimum which is APP_NAME, APP_DATA_DIR_KEY and USER_DATA_DIR OptionParser parser = new OptionParser(); parser.allowsUnrecognizedOptions(); - parser.accepts(USER_DATA_DIR_KEY, description("User data directory", DEFAULT_USER_DATA_DIR)) + parser.accepts(CoreOptionKeys.USER_DATA_DIR_KEY, description("User data directory", DEFAULT_USER_DATA_DIR)) .withRequiredArg(); - parser.accepts(APP_NAME_KEY, description("Application name", DEFAULT_APP_NAME)) + parser.accepts(CoreOptionKeys.APP_NAME_KEY, description("Application name", DEFAULT_APP_NAME)) .withRequiredArg(); OptionSet options; @@ -51,7 +52,7 @@ public class BitsquareAppMain extends BitsquareExecutable { BitsquareEnvironment bitsquareEnvironment = new BitsquareEnvironment(options); // need to call that before BitsquareAppMain().execute(args) - BitsquareExecutable.initAppDir(bitsquareEnvironment.getProperty(BitsquareEnvironment.APP_DATA_DIR_KEY)); + BitsquareExecutable.initAppDir(bitsquareEnvironment.getProperty(CoreOptionKeys.APP_DATA_DIR_KEY)); // For some reason the JavaFX launch process results in us losing the thread context class loader: reset it. // In order to work around a bug in JavaFX 8u25 and below, you must include the following code as the first line of your realMain method: diff --git a/gui/src/main/java/io/bitsquare/gui/CandleStickChart.css b/gui/src/main/java/io/bitsquare/gui/CandleStickChart.css new file mode 100644 index 0000000000..f76ede7e91 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/CandleStickChart.css @@ -0,0 +1,99 @@ +/* + * 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); +} + +.chart-alternative-row-fill { + -fx-fill: transparent; + -fx-stroke: transparent; + -fx-stroke-width: 0; +} + +.chart-plot-background { + -fx-background-color: transparent; +} diff --git a/gui/src/main/java/io/bitsquare/gui/GuiModule.java b/gui/src/main/java/io/bitsquare/gui/GuiModule.java index 3c4b96c341..286e7b60c4 100644 --- a/gui/src/main/java/io/bitsquare/gui/GuiModule.java +++ b/gui/src/main/java/io/bitsquare/gui/GuiModule.java @@ -20,7 +20,7 @@ package io.bitsquare.gui; import com.google.inject.Singleton; import com.google.inject.name.Names; import io.bitsquare.app.AppModule; -import io.bitsquare.app.BitsquareEnvironment; +import io.bitsquare.app.CoreOptionKeys; import io.bitsquare.gui.common.fxml.FxmlViewLoader; import io.bitsquare.gui.common.view.CachingViewLoader; import io.bitsquare.gui.common.view.ViewFactory; @@ -69,6 +69,6 @@ public class GuiModule extends AppModule { bind(Stage.class).toInstance(primaryStage); - bindConstant().annotatedWith(Names.named(MainView.TITLE_KEY)).to(env.getRequiredProperty(BitsquareEnvironment.APP_NAME_KEY)); + bindConstant().annotatedWith(Names.named(MainView.TITLE_KEY)).to(env.getRequiredProperty(CoreOptionKeys.APP_NAME_KEY)); } } diff --git a/gui/src/main/java/io/bitsquare/gui/bitsquare.css b/gui/src/main/java/io/bitsquare/gui/bitsquare.css index fa9aff1622..86a7c6833f 100644 --- a/gui/src/main/java/io/bitsquare/gui/bitsquare.css +++ b/gui/src/main/java/io/bitsquare/gui/bitsquare.css @@ -27,7 +27,7 @@ bg color of non edit textFields: fafafa -bs-content-bg-grey: #f4f4f4; -bs-very-light-grey: #f8f8f8; - -fx-accent: #0f87c3; + -fx-accent: #0f86c3; -bs-blue-soft: derive(-fx-accent, 60%); -bs-blue-transparent: #0f87c344; @@ -1069,4 +1069,35 @@ textfield */ -fx-alignment: center; -fx-font-size: 10; -fx-text-fill: white; -} \ No newline at end of file +} + +#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; +} + diff --git a/gui/src/main/java/io/bitsquare/gui/components/SearchComboBox.java b/gui/src/main/java/io/bitsquare/gui/components/SearchComboBox.java index 44a36013d9..9c6753bd0f 100644 --- a/gui/src/main/java/io/bitsquare/gui/components/SearchComboBox.java +++ b/gui/src/main/java/io/bitsquare/gui/components/SearchComboBox.java @@ -30,7 +30,7 @@ public class SearchComboBox extends ComboBox { findAny().isPresent()) { UserThread.execute(() -> { filteredList.setPredicate(item -> newValue.isEmpty() || - getConverter().toString(item).toLowerCase().startsWith(newValue.toLowerCase())); + getConverter().toString(item).toLowerCase().contains(newValue.toLowerCase())); hide(); setVisibleRowCount(Math.min(20, filteredList.size())); show(); diff --git a/gui/src/main/java/io/bitsquare/gui/main/MainView.java b/gui/src/main/java/io/bitsquare/gui/main/MainView.java index 26c2412a5a..4f5f411bef 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/MainView.java +++ b/gui/src/main/java/io/bitsquare/gui/main/MainView.java @@ -20,7 +20,7 @@ package io.bitsquare.gui.main; import io.bitsquare.BitsquareException; import io.bitsquare.app.BitsquareApp; import io.bitsquare.app.DevFlags; -import io.bitsquare.btc.pricefeed.PriceFeed; +import io.bitsquare.btc.pricefeed.PriceFeedService; import io.bitsquare.common.UserThread; import io.bitsquare.common.util.Tuple2; import io.bitsquare.common.util.Tuple3; @@ -158,7 +158,7 @@ public class MainView extends InitializableView { marketPriceBox.second.textProperty().bind(createStringBinding( () -> { - PriceFeed.Type type = model.typeProperty.get(); + PriceFeedService.Type type = model.typeProperty.get(); return type != null ? "Market price (" + type.name + ")" : ""; }, model.marketPriceCurrencyCode, model.typeProperty)); diff --git a/gui/src/main/java/io/bitsquare/gui/main/MainViewModel.java b/gui/src/main/java/io/bitsquare/gui/main/MainViewModel.java index ec6973611e..b2c72f368e 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/MainViewModel.java +++ b/gui/src/main/java/io/bitsquare/gui/main/MainViewModel.java @@ -34,7 +34,7 @@ import io.bitsquare.btc.TradeWalletService; import io.bitsquare.btc.WalletService; import io.bitsquare.btc.listeners.BalanceListener; import io.bitsquare.btc.pricefeed.MarketPrice; -import io.bitsquare.btc.pricefeed.PriceFeed; +import io.bitsquare.btc.pricefeed.PriceFeedService; import io.bitsquare.common.Clock; import io.bitsquare.common.Timer; import io.bitsquare.common.UserThread; @@ -120,7 +120,7 @@ public class MainViewModel implements ViewModel { final StringProperty walletServiceErrorMsg = new SimpleStringProperty(); final StringProperty btcSplashSyncIconId = new SimpleStringProperty(); final StringProperty marketPriceCurrencyCode = new SimpleStringProperty(""); - final ObjectProperty typeProperty = new SimpleObjectProperty<>(PriceFeed.Type.LAST); + final ObjectProperty typeProperty = new SimpleObjectProperty<>(PriceFeedService.Type.LAST); final ObjectProperty selectedPriceFeedComboBoxItemProperty = new SimpleObjectProperty<>(); final BooleanProperty isFiatCurrencyPriceFeedSelected = new SimpleBooleanProperty(true); final BooleanProperty isCryptoCurrencyPriceFeedSelected = new SimpleBooleanProperty(false); @@ -153,7 +153,7 @@ public class MainViewModel implements ViewModel { final StringProperty p2pNetworkLabelId = new SimpleStringProperty("footer-pane"); private MonadicBinding allServicesDone, tradesAndUIReady; - final PriceFeed priceFeed; + final PriceFeedService priceFeedService; private final User user; private int numBtcPeers = 0; private Timer checkNumberOfBtcPeersTimer; @@ -164,7 +164,7 @@ public class MainViewModel implements ViewModel { private Subscription priceFeedAllLoadedSubscription; private Popup startupTimeoutPopup; private BooleanProperty p2pNetWorkReady; - private BooleanProperty walletInitialized; + private final BooleanProperty walletInitialized = new SimpleBooleanProperty(); /////////////////////////////////////////////////////////////////////////////////////////// @@ -173,14 +173,14 @@ public class MainViewModel implements ViewModel { @Inject public MainViewModel(WalletService walletService, TradeWalletService tradeWalletService, - PriceFeed priceFeed, + PriceFeedService priceFeedService, ArbitratorManager arbitratorManager, P2PService p2PService, TradeManager tradeManager, OpenOfferManager openOfferManager, DisputeManager disputeManager, Preferences preferences, User user, AlertManager alertManager, PrivateNotificationManager privateNotificationManager, FilterManager filterManager, WalletPasswordWindow walletPasswordWindow, NotificationCenter notificationCenter, TacWindow tacWindow, Clock clock, KeyRing keyRing, Navigation navigation, BSFormatter formatter) { - this.priceFeed = priceFeed; + this.priceFeedService = priceFeedService; this.user = user; this.walletService = walletService; this.tradeWalletService = tradeWalletService; @@ -234,8 +234,8 @@ public class MainViewModel implements ViewModel { showStartupTimeoutPopup(); }, 4, TimeUnit.MINUTES); - walletInitialized = initBitcoinWallet(); p2pNetWorkReady = initP2PNetwork(); + initBitcoinWallet(); // need to store it to not get garbage collected allServicesDone = EasyBind.combine(walletInitialized, p2pNetWorkReady, (a, b) -> a && b); @@ -283,6 +283,7 @@ public class MainViewModel implements ViewModel { /////////////////////////////////////////////////////////////////////////////////////////// private BooleanProperty initP2PNetwork() { + Log.traceCall(); StringProperty bootstrapState = new SimpleStringProperty(); StringProperty bootstrapWarning = new SimpleStringProperty(); BooleanProperty hiddenServicePublished = new SimpleBooleanProperty(); @@ -348,6 +349,9 @@ public class MainViewModel implements ViewModel { public void onTorNodeReady() { bootstrapState.set("Tor node created"); p2PNetworkIconId.set("image-connection-tor"); + + if (preferences.getUseTorForBitcoinJ()) + initWalletService(); } @Override @@ -421,7 +425,17 @@ public class MainViewModel implements ViewModel { return p2pNetworkInitialized; } - private BooleanProperty initBitcoinWallet() { + private void initBitcoinWallet() { + Log.traceCall(); + + // We only init wallet service here if not using Tor for bitcoinj. + // When using Tor, wallet init must be deferred until Tor is ready. + if (!preferences.getUseTorForBitcoinJ()) + initWalletService(); + } + + private void initWalletService() { + Log.traceCall(); ObjectProperty walletServiceException = new SimpleObjectProperty<>(); btcInfoBinding = EasyBind.combine(walletService.downloadPercentageProperty(), walletService.numPeersProperty(), walletServiceException, (downloadPercentage, numPeers, exception) -> { @@ -462,7 +476,6 @@ public class MainViewModel implements ViewModel { btcInfo.set(newValue); }); - final BooleanProperty walletInitialized = new SimpleBooleanProperty(); walletService.initialize(null, () -> { numBtcPeers = walletService.numPeersProperty().get(); @@ -484,7 +497,6 @@ public class MainViewModel implements ViewModel { } }, walletServiceException::set); - return walletInitialized; } private void onAllServicesInitialized() { @@ -738,11 +750,11 @@ public class MainViewModel implements ViewModel { } private void setupMarketPriceFeed() { - if (priceFeed.getCurrencyCode() == null) - priceFeed.setCurrencyCode(preferences.getPreferredTradeCurrency().getCode()); - if (priceFeed.getType() == null) - priceFeed.setType(PriceFeed.Type.LAST); - priceFeed.init(price -> { + if (priceFeedService.getCurrencyCode() == null) + priceFeedService.setCurrencyCode(preferences.getPreferredTradeCurrency().getCode()); + if (priceFeedService.getType() == null) + priceFeedService.setType(PriceFeedService.Type.LAST); + priceFeedService.init(price -> { marketPrice.set(formatter.formatMarketPrice(price)); marketPriceInverted.set(price != 0 ? formatter.formatMarketPrice(1 / price, 8) : ""); }, @@ -750,8 +762,8 @@ public class MainViewModel implements ViewModel { marketPrice.set("N/A"); marketPriceInverted.set("N/A"); }); - marketPriceCurrencyCode.bind(priceFeed.currencyCodeProperty()); - typeProperty.bind(priceFeed.typeProperty()); + marketPriceCurrencyCode.bind(priceFeedService.currencyCodeProperty()); + typeProperty.bind(priceFeedService.typeProperty()); marketPriceBinding = EasyBind.combine( marketPriceCurrencyCode, marketPrice, marketPriceInverted, preferences.useInvertedMarketPriceProperty(), @@ -765,7 +777,7 @@ public class MainViewModel implements ViewModel { String code = preferences.getUseStickyMarketPrice() ? preferences.getPreferredTradeCurrency().getCode() : - priceFeed.currencyCodeProperty().get(); + priceFeedService.currencyCodeProperty().get(); Optional itemOptional = findPriceFeedComboBoxItem(code); if (itemOptional.isPresent()) { if (selectedPriceFeedComboBoxItemProperty.get() == null || !preferences.getUseStickyMarketPrice()) { @@ -791,7 +803,7 @@ public class MainViewModel implements ViewModel { } }); - priceFeedAllLoadedSubscription = EasyBind.subscribe(priceFeed.currenciesUpdateFlagProperty(), newPriceUpdate -> setMarketPriceInItems()); + priceFeedAllLoadedSubscription = EasyBind.subscribe(priceFeedService.currenciesUpdateFlagProperty(), newPriceUpdate -> setMarketPriceInItems()); preferences.getTradeCurrenciesAsObservable().addListener((ListChangeListener) c -> { UserThread.runAfter(() -> { @@ -804,12 +816,12 @@ public class MainViewModel implements ViewModel { private void setMarketPriceInItems() { priceFeedComboBoxItems.stream().forEach(item -> { String currencyCode = item.currencyCode; - MarketPrice marketPrice = priceFeed.getMarketPrice(currencyCode); + MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); boolean useInvertedMarketPrice = preferences.getUseInvertedMarketPrice(); String priceString; String currencyPairString = useInvertedMarketPrice ? "BTC/" + currencyCode : currencyCode + "/BTC"; if (marketPrice != null) { - double price = marketPrice.getPrice(priceFeed.getType()); + double price = marketPrice.getPrice(priceFeedService.getType()); if (price != 0) { double priceInverted = 1 / price; priceString = useInvertedMarketPrice ? formatter.formatMarketPrice(priceInverted, 8) : formatter.formatMarketPrice(price); @@ -828,7 +840,7 @@ public class MainViewModel implements ViewModel { public void setPriceFeedComboBoxItem(PriceFeedComboBoxItem item) { if (!preferences.getUseStickyMarketPrice() && item != null) { - Optional itemOptional = findPriceFeedComboBoxItem(priceFeed.currencyCodeProperty().get()); + Optional itemOptional = findPriceFeedComboBoxItem(priceFeedService.currencyCodeProperty().get()); if (itemOptional.isPresent()) selectedPriceFeedComboBoxItemProperty.set(itemOptional.get()); else @@ -836,7 +848,7 @@ public class MainViewModel implements ViewModel { .ifPresent(item2 -> selectedPriceFeedComboBoxItemProperty.set(item2)); } else if (item != null) { selectedPriceFeedComboBoxItemProperty.set(item); - priceFeed.setCurrencyCode(item.currencyCode); + priceFeedService.setCurrencyCode(item.currencyCode); } else { findPriceFeedComboBoxItem(preferences.getPreferredTradeCurrency().getCode()) .ifPresent(item2 -> selectedPriceFeedComboBoxItemProperty.set(item2)); diff --git a/gui/src/main/java/io/bitsquare/gui/main/account/content/altcoinaccounts/AltCoinAccountsView.java b/gui/src/main/java/io/bitsquare/gui/main/account/content/altcoinaccounts/AltCoinAccountsView.java index 143770eb28..5c9e8eb840 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/account/content/altcoinaccounts/AltCoinAccountsView.java +++ b/gui/src/main/java/io/bitsquare/gui/main/account/content/altcoinaccounts/AltCoinAccountsView.java @@ -167,37 +167,17 @@ public class AltCoinAccountsView extends ActivatableViewAndModel 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")) { + } else if (code.equals("ETC")) { //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) diff --git a/gui/src/main/java/io/bitsquare/gui/main/account/content/backup/BackupView.java b/gui/src/main/java/io/bitsquare/gui/main/account/content/backup/BackupView.java index 70d9a4e12e..95ec90910e 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/account/content/backup/BackupView.java +++ b/gui/src/main/java/io/bitsquare/gui/main/account/content/backup/BackupView.java @@ -18,6 +18,7 @@ package io.bitsquare.gui.main.account.content.backup; import io.bitsquare.app.BitsquareEnvironment; +import io.bitsquare.app.CoreOptionKeys; import io.bitsquare.common.util.Tuple3; import io.bitsquare.common.util.Utilities; import io.bitsquare.gui.common.view.ActivatableView; @@ -62,7 +63,7 @@ public class BackupView extends ActivatableView { super(); this.stage = stage; this.preferences = preferences; - dataDir = new File(environment.getProperty(BitsquareEnvironment.APP_DATA_DIR_KEY)); + dataDir = new File(environment.getProperty(CoreOptionKeys.APP_DATA_DIR_KEY)); } @Override diff --git a/gui/src/main/java/io/bitsquare/gui/main/markets/MarketView.fxml b/gui/src/main/java/io/bitsquare/gui/main/markets/MarketView.fxml index 34e8a03b46..bc67988576 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/markets/MarketView.fxml +++ b/gui/src/main/java/io/bitsquare/gui/main/markets/MarketView.fxml @@ -27,4 +27,5 @@ + diff --git a/gui/src/main/java/io/bitsquare/gui/main/markets/MarketView.java b/gui/src/main/java/io/bitsquare/gui/main/markets/MarketView.java index fa69da8a03..632b41767e 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/markets/MarketView.java +++ b/gui/src/main/java/io/bitsquare/gui/main/markets/MarketView.java @@ -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 { @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 { 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 { 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 { 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"); diff --git a/gui/src/main/java/io/bitsquare/gui/main/markets/charts/MarketsChartsView.java b/gui/src/main/java/io/bitsquare/gui/main/markets/charts/MarketsChartsView.java index b8fa3333a2..607942bc5a 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/markets/charts/MarketsChartsView.java +++ b/gui/src/main/java/io/bitsquare/gui/main/markets/charts/MarketsChartsView.java @@ -203,7 +203,7 @@ public class MarketsChartsView extends ActivatableViewAndModel observable, Number oldValue, Number newValue) { if (offer != null && offer.getPrice() != null) { setText(formatter.formatFiat(offer.getPrice())); - model.priceFeed.currenciesUpdateFlagProperty().removeListener(listener); + model.priceFeedService.currenciesUpdateFlagProperty().removeListener(listener); } } }; @@ -214,14 +214,14 @@ public class MarketsChartsView extends ActivatableViewAndModel observable, Number oldValue, Number newValue) { if (offer != null && offer.getPrice() != null) { setText(formatter.formatFiat(offer.getOfferVolume())); - model.priceFeed.currenciesUpdateFlagProperty().removeListener(listener); + model.priceFeedService.currenciesUpdateFlagProperty().removeListener(listener); } } }; @@ -285,14 +285,14 @@ public class MarketsChartsView extends ActivatableViewAndModel tradeCurrency = new SimpleObjectProperty<>(); private final List buyData = new ArrayList<>(); @@ -67,10 +67,10 @@ class MarketsChartsViewModel extends ActivatableViewModel { /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public MarketsChartsViewModel(OfferBook offerBook, Preferences preferences, PriceFeed priceFeed) { + public MarketsChartsViewModel(OfferBook offerBook, Preferences preferences, PriceFeedService priceFeedService) { this.offerBook = offerBook; this.preferences = preferences; - this.priceFeed = priceFeed; + this.priceFeedService = priceFeedService; Optional tradeCurrencyOptional = CurrencyUtil.getTradeCurrency(preferences.getMarketScreenCurrencyCode()); if (tradeCurrencyOptional.isPresent()) @@ -100,7 +100,7 @@ class MarketsChartsViewModel extends ActivatableViewModel { if (!isAnyPricePresent()) { offerBook.fillOfferBookListItems(); updateChartData(); - priceFeed.currenciesUpdateFlagProperty().removeListener(currenciesUpdatedListener); + priceFeedService.currenciesUpdateFlagProperty().removeListener(currenciesUpdatedListener); } } }; @@ -108,17 +108,17 @@ class MarketsChartsViewModel extends ActivatableViewModel { @Override protected void activate() { - priceFeed.setType(PriceFeed.Type.LAST); + priceFeedService.setType(PriceFeedService.Type.LAST); offerBookListItems.addListener(listChangeListener); offerBook.fillOfferBookListItems(); updateChartData(); if (isAnyPricePresent()) - priceFeed.currenciesUpdateFlagProperty().addListener(currenciesUpdatedListener); + priceFeedService.currenciesUpdateFlagProperty().addListener(currenciesUpdatedListener); if (!preferences.getUseStickyMarketPrice()) - priceFeed.setCurrencyCode(tradeCurrency.get().getCode()); + priceFeedService.setCurrencyCode(tradeCurrency.get().getCode()); } @Override @@ -189,7 +189,7 @@ class MarketsChartsViewModel extends ActivatableViewModel { updateChartData(); if (!preferences.getUseStickyMarketPrice()) - priceFeed.setCurrencyCode(tradeCurrency.getCode()); + priceFeedService.setCurrencyCode(tradeCurrency.getCode()); preferences.setMarketScreenCurrencyCode(tradeCurrency.getCode()); } diff --git a/gui/src/main/java/io/bitsquare/gui/main/markets/trades/TradesChartsView.fxml b/gui/src/main/java/io/bitsquare/gui/main/markets/trades/TradesChartsView.fxml new file mode 100644 index 0000000000..379a99420e --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/markets/trades/TradesChartsView.fxml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/gui/src/main/java/io/bitsquare/gui/main/markets/trades/TradesChartsView.java b/gui/src/main/java/io/bitsquare/gui/main/markets/trades/TradesChartsView.java new file mode 100644 index 0000000000..fb54a8a9a6 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/markets/trades/TradesChartsView.java @@ -0,0 +1,514 @@ +/* + * 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 . + */ + +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.statistics.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 { + private static final Logger log = LoggerFactory.getLogger(TradesChartsView.class); + + private final BSFormatter formatter; + + private TableView tableView; + private ComboBox currencyComboBox; + private VolumeChart volumeChart; + private CandleStickChart priceChart; + private NumberAxis priceAxisX, priceAxisY, volumeAxisY, volumeAxisX; + private XYChart.Series priceSeries; + private XYChart.Series volumeSeries; + private ChangeListener priceAxisYWidthListener; + private ChangeListener volumeAxisYWidthListener; + private double priceAxisYWidth; + private double volumeAxisYWidth; + private final StringProperty priceColumnLabel = new SimpleStringProperty(); + private ChangeListener toggleChangeListener; + private ToggleGroup toggleGroup; + private final ListChangeListener> itemsChangeListener; + private Subscription tradeCurrencySubscriber; + private SortedList sortedList; + private Label nrOfTradeStatisticsLabel; + private ListChangeListener tradeStatisticsByCurrencyListener; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // 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(); + nrOfTradeStatisticsLabel = new Label(""); + nrOfTradeStatisticsLabel.setId("num-offers"); + nrOfTradeStatisticsLabel.setPadding(new Insets(-5, 0, -10, 5)); + root.getChildren().addAll(toolBox, priceChart, volumeChart, tableView, nrOfTradeStatisticsLabel); + + toggleChangeListener = (observable, oldValue, newValue) -> { + if (newValue != null) { + model.setTickUnit((TradesChartsViewModel.TickUnit) newValue.getUserData()); + priceAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); + volumeAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); + } + }; + priceAxisYWidthListener = (observable, oldValue, newValue) -> { + priceAxisYWidth = (double) newValue; + layoutChart(); + }; + volumeAxisYWidthListener = (observable, oldValue, newValue) -> { + volumeAxisYWidth = (double) newValue; + layoutChart(); + }; + tradeStatisticsByCurrencyListener = c -> nrOfTradeStatisticsLabel.setText("Nr. of trades: " + model.tradeStatisticsByCurrency.size()); + } + + @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()); + volumeAxisX.setTickLabelFormatter(getTimeAxisStringConverter()); + + model.tradeStatisticsByCurrency.addListener(tradeStatisticsByCurrencyListener); + nrOfTradeStatisticsLabel.setText("Nr. of trades: " + model.tradeStatisticsByCurrency.size()); + } + + @Override + protected void deactivate() { + model.priceItems.removeListener(itemsChangeListener); + toggleGroup.selectedToggleProperty().removeListener(toggleChangeListener); + priceAxisY.widthProperty().removeListener(priceAxisYWidthListener); + volumeAxisY.widthProperty().removeListener(volumeAxisYWidthListener); + model.tradeStatisticsByCurrency.removeListener(tradeStatisticsByCurrencyListener); + 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() { + @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() { + @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() { + @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() { + @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 getTimeAxisStringConverter() { + return new StringConverter() { + @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() { + @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 dateColumn = new TableColumn<>("Date/Time"); + dateColumn.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue())); + dateColumn.setCellFactory( + new Callback, TableCell>() { + @Override + public TableCell call( + TableColumn column) { + return new TableCell() { + @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 amountColumn = new TableColumn<>("Amount in BTC"); + amountColumn.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue())); + amountColumn.setCellFactory( + new Callback, TableCell>() { + @Override + public TableCell call( + TableColumn column) { + return new TableCell() { + @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 priceColumn = new TableColumn<>(); + priceColumn.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue())); + priceColumn.textProperty().bind(priceColumnLabel); + priceColumn.setCellFactory( + new Callback, TableCell>() { + @Override + public TableCell call( + TableColumn column) { + return new TableCell() { + @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 volumeColumn = new TableColumn<>(); + volumeColumn.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue())); + volumeColumn.setText("Volume (BTC)"); + volumeColumn.setCellFactory( + new Callback, TableCell>() { + @Override + public TableCell call( + TableColumn column) { + return new TableCell() { + @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 directionColumn = new TableColumn<>("Trade type"); + directionColumn.setCellValueFactory((tradeStatistics) -> new ReadOnlyObjectWrapper<>(tradeStatistics.getValue())); + directionColumn.setCellFactory( + new Callback, TableCell>() { + @Override + public TableCell call( + TableColumn column) { + return new TableCell() { + @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); + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/main/markets/trades/TradesChartsViewModel.java b/gui/src/main/java/io/bitsquare/gui/main/markets/trades/TradesChartsViewModel.java new file mode 100644 index 0000000000..f9d16e2b20 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/markets/trades/TradesChartsViewModel.java @@ -0,0 +1,270 @@ +/* + * 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 . + */ + +package io.bitsquare.gui.main.markets.trades; + +import com.google.common.annotations.VisibleForTesting; +import com.google.inject.Inject; +import io.bitsquare.btc.pricefeed.PriceFeedService; +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.statistics.TradeStatistics; +import io.bitsquare.trade.statistics.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.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 PriceFeedService priceFeedService; + + private final SetChangeListener setChangeListener; + final ObjectProperty tradeCurrencyProperty = new SimpleObjectProperty<>(); + + final ObservableList tradeStatisticsByCurrency = FXCollections.observableArrayList(); + ObservableList> priceItems = FXCollections.observableArrayList(); + ObservableList> volumeItems = FXCollections.observableArrayList(); + + TickUnit tickUnit = TickUnit.MONTH; + int maxTicks = 30; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public TradesChartsViewModel(TradeStatisticsManager tradeStatisticsManager, Preferences preferences, PriceFeedService priceFeedService) { + this.tradeStatisticsManager = tradeStatisticsManager; + this.preferences = preferences; + this.priceFeedService = priceFeedService; + + setChangeListener = change -> updateChartData(); + + Optional 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(); + + if (!preferences.getUseStickyMarketPrice()) + priceFeedService.setCurrencyCode(tradeCurrencyProperty.get().getCode()); + } + + @Override + protected void deactivate() { + tradeStatisticsManager.getObservableTradeStatisticsSet().removeListener(setChangeListener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // UI actions + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onSetTradeCurrency(TradeCurrency tradeCurrency) { + this.tradeCurrencyProperty.set(tradeCurrency); + preferences.setTradeStatisticsScreenCurrencyCode(tradeCurrency.getCode()); + updateChartData(); + + if (!preferences.getUseStickyMarketPrice()) + priceFeedService.setCurrencyCode(tradeCurrency.getCode()); + + } + + public void setTickUnit(TickUnit tickUnit) { + this.tickUnit = tickUnit; + preferences.setTradeStatisticsTickUnitIndex(tickUnit.ordinal()); + updateChartData(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + public String getCurrencyCode() { + return tradeCurrencyProperty.get().getCode(); + } + + public ObservableList 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> itemsPerInterval = new HashMap<>(); + tradeStatisticsByCurrency.stream().forEach(e -> { + Set 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 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(e.tick, e.open, e)) + .collect(Collectors.toList())); + + volumeItems.setAll(candleDataList.stream() + .map(e -> new XYChart.Data(e.tick, e.accumulatedAmount, e)) + .collect(Collectors.toList())); + } + + @VisibleForTesting + CandleData getCandleData(long tick, Set 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; + } + // 100000000 -> Coin.COIN.value; + long averagePrice = Math.round((double) accumulatedVolume * 100000000d / (double) accumulatedAmount); + + List 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); + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/main/markets/trades/charts/CandleData.java b/gui/src/main/java/io/bitsquare/gui/main/markets/trades/charts/CandleData.java new file mode 100644 index 0000000000..14f77bbf7a --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/markets/trades/charts/CandleData.java @@ -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 . + */ + +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; + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/main/markets/trades/charts/price/Candle.java b/gui/src/main/java/io/bitsquare/gui/main/markets/trades/charts/price/Candle.java new file mode 100644 index 0000000000..dd4022a56c --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/markets/trades/charts/price/Candle.java @@ -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 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); + } +} \ No newline at end of file diff --git a/gui/src/main/java/io/bitsquare/gui/main/markets/trades/charts/price/CandleStickChart.java b/gui/src/main/java/io/bitsquare/gui/main/markets/trades/charts/price/CandleStickChart.java new file mode 100644 index 0000000000..ba8c2b1298 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/markets/trades/charts/price/CandleStickChart.java @@ -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. + *

+ * 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 { + private static final Logger log = LoggerFactory.getLogger(CandleStickChart.class); + + private StringConverter 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 xAxis, Axis yAxis) { + super(xAxis, yAxis); + } + + // -------------- METHODS ------------------------------------------------------------------------------------------ + + public final void setToolTipStringConverter(StringConverter 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 series = getData().get(seriesIndex); + Iterator> iter = getDisplayedDataIterator(series); + Path seriesPath = null; + if (series.getNode() instanceof Path) { + seriesPath = (Path) series.getNode(); + seriesPath.getElements().clear(); + } + while (iter.hasNext()) { + XYChart.Data 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 item) { + } + + @Override + protected void dataItemAdded(XYChart.Series series, int itemIndex, XYChart.Data 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 item, XYChart.Series 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 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 series) { + // remove all candle nodes + for (XYChart.Data 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 xa = getXAxis(); + final Axis ya = getYAxis(); + List xData = null; + List yData = null; + if (xa.isAutoRanging()) { + xData = new ArrayList<>(); + } + if (ya.isAutoRanging()) { + yData = new ArrayList<>(); + } + if (xData != null || yData != null) { + for (XYChart.Series series : getData()) { + for (XYChart.Data 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); + } + } + } +} \ No newline at end of file diff --git a/gui/src/main/java/io/bitsquare/gui/main/markets/trades/charts/price/TooltipContent.java b/gui/src/main/java/io/bitsquare/gui/main/markets/trades/charts/price/TooltipContent.java new file mode 100644 index 0000000000..31a736977c --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/markets/trades/charts/price/TooltipContent.java @@ -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 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 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)); + } +} \ No newline at end of file diff --git a/gui/src/main/java/io/bitsquare/gui/main/markets/trades/charts/volume/VolumeBar.java b/gui/src/main/java/io/bitsquare/gui/main/markets/trades/charts/volume/VolumeBar.java new file mode 100644 index 0000000000..1bad815254 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/markets/trades/charts/volume/VolumeBar.java @@ -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 . + */ +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 volumeStringConverter; + + private final Region bar = new Region(); + private final Tooltip tooltip; + + VolumeBar(String seriesStyleClass, String dataStyleClass, StringConverter 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"); + } +} \ No newline at end of file diff --git a/gui/src/main/java/io/bitsquare/gui/main/markets/trades/charts/volume/VolumeChart.java b/gui/src/main/java/io/bitsquare/gui/main/markets/trades/charts/volume/VolumeChart.java new file mode 100644 index 0000000000..f86466453a --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/markets/trades/charts/volume/VolumeChart.java @@ -0,0 +1,190 @@ +/* + * 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 . + */ +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 { + private static final Logger log = LoggerFactory.getLogger(CandleStickChart.class); + + private StringConverter toolTipStringConverter; + + public VolumeChart(Axis xAxis, Axis yAxis) { + super(xAxis, yAxis); + } + + public final void setToolTipStringConverter(StringConverter toolTipStringConverter) { + this.toolTipStringConverter = toolTipStringConverter; + } + + @Override + protected void layoutPlotChildren() { + if (getData() == null) { + return; + } + for (int seriesIndex = 0; seriesIndex < getData().size(); seriesIndex++) { + XYChart.Series series = getData().get(seriesIndex); + Iterator> iter = getDisplayedDataIterator(series); + while (iter.hasNext()) { + XYChart.Data 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 item) { + } + + @Override + protected void dataItemAdded(XYChart.Series series, int itemIndex, XYChart.Data 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 item, XYChart.Series 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 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 series) { + for (XYChart.Data 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 xa = getXAxis(); + final Axis ya = getYAxis(); + List xData = null; + List yData = null; + if (xa.isAutoRanging()) { + xData = new ArrayList<>(); + } + if (ya.isAutoRanging()) + yData = new ArrayList<>(); + if (xData != null || yData != null) { + for (XYChart.Series series : getData()) { + for (XYChart.Data 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); + } + } + } +} \ No newline at end of file diff --git a/gui/src/main/java/io/bitsquare/gui/main/offer/BuyOfferView.java b/gui/src/main/java/io/bitsquare/gui/main/offer/BuyOfferView.java index fe9fe63f61..b2b0829db4 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/offer/BuyOfferView.java +++ b/gui/src/main/java/io/bitsquare/gui/main/offer/BuyOfferView.java @@ -17,7 +17,7 @@ package io.bitsquare.gui.main.offer; -import io.bitsquare.btc.pricefeed.PriceFeed; +import io.bitsquare.btc.pricefeed.PriceFeedService; import io.bitsquare.gui.Navigation; import io.bitsquare.gui.common.view.FxmlView; import io.bitsquare.gui.common.view.ViewLoader; @@ -29,8 +29,8 @@ import javax.inject.Inject; public class BuyOfferView extends OfferView { @Inject - public BuyOfferView(ViewLoader viewLoader, Navigation navigation, PriceFeed priceFeed, Preferences preferences) { - super(viewLoader, navigation, priceFeed, preferences); + public BuyOfferView(ViewLoader viewLoader, Navigation navigation, PriceFeedService priceFeedService, Preferences preferences) { + super(viewLoader, navigation, priceFeedService, preferences); } @Override diff --git a/gui/src/main/java/io/bitsquare/gui/main/offer/OfferView.java b/gui/src/main/java/io/bitsquare/gui/main/offer/OfferView.java index 14b49a9046..1d349dc1b9 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/offer/OfferView.java +++ b/gui/src/main/java/io/bitsquare/gui/main/offer/OfferView.java @@ -17,7 +17,7 @@ package io.bitsquare.gui.main.offer; -import io.bitsquare.btc.pricefeed.PriceFeed; +import io.bitsquare.btc.pricefeed.PriceFeedService; import io.bitsquare.common.UserThread; import io.bitsquare.gui.Navigation; import io.bitsquare.gui.common.view.ActivatableView; @@ -54,7 +54,7 @@ public abstract class OfferView extends ActivatableView { private final ViewLoader viewLoader; private final Navigation navigation; - private final PriceFeed priceFeed; + private final PriceFeedService priceFeedService; private Preferences preferences; private final Offer.Direction direction; private Tab takeOfferTab, createOfferTab, offerBookTab; @@ -63,10 +63,10 @@ public abstract class OfferView extends ActivatableView { private ChangeListener tabChangeListener; private ListChangeListener tabListChangeListener; - protected OfferView(ViewLoader viewLoader, Navigation navigation, PriceFeed priceFeed, Preferences preferences) { + protected OfferView(ViewLoader viewLoader, Navigation navigation, PriceFeedService priceFeedService, Preferences preferences) { this.viewLoader = viewLoader; this.navigation = navigation; - this.priceFeed = priceFeed; + this.priceFeedService = priceFeedService; this.preferences = preferences; this.direction = (this instanceof BuyOfferView) ? Offer.Direction.BUY : Offer.Direction.SELL; } @@ -139,7 +139,7 @@ public abstract class OfferView extends ActivatableView { View view; boolean isBuy = direction == Offer.Direction.BUY; - priceFeed.setType(isBuy ? PriceFeed.Type.ASK : PriceFeed.Type.BID); + priceFeedService.setType(isBuy ? PriceFeedService.Type.ASK : PriceFeedService.Type.BID); if (viewClass == OfferBookView.class && offerBookView == null) { view = viewLoader.load(viewClass); diff --git a/gui/src/main/java/io/bitsquare/gui/main/offer/SellOfferView.java b/gui/src/main/java/io/bitsquare/gui/main/offer/SellOfferView.java index 230052f200..76ef5547a9 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/offer/SellOfferView.java +++ b/gui/src/main/java/io/bitsquare/gui/main/offer/SellOfferView.java @@ -17,7 +17,7 @@ package io.bitsquare.gui.main.offer; -import io.bitsquare.btc.pricefeed.PriceFeed; +import io.bitsquare.btc.pricefeed.PriceFeedService; import io.bitsquare.gui.Navigation; import io.bitsquare.gui.common.view.FxmlView; import io.bitsquare.gui.common.view.ViewLoader; @@ -29,8 +29,8 @@ import javax.inject.Inject; public class SellOfferView extends OfferView { @Inject - public SellOfferView(ViewLoader viewLoader, Navigation navigation, PriceFeed priceFeed, Preferences preferences) { - super(viewLoader, navigation, priceFeed, preferences); + public SellOfferView(ViewLoader viewLoader, Navigation navigation, PriceFeedService priceFeedService, Preferences preferences) { + super(viewLoader, navigation, priceFeedService, preferences); } @Override diff --git a/gui/src/main/java/io/bitsquare/gui/main/offer/createoffer/CreateOfferDataModel.java b/gui/src/main/java/io/bitsquare/gui/main/offer/createoffer/CreateOfferDataModel.java index 9ace8d86ab..a59337ab49 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/offer/createoffer/CreateOfferDataModel.java +++ b/gui/src/main/java/io/bitsquare/gui/main/offer/createoffer/CreateOfferDataModel.java @@ -26,7 +26,7 @@ import io.bitsquare.btc.TradeWalletService; import io.bitsquare.btc.WalletService; import io.bitsquare.btc.blockchain.BlockchainService; import io.bitsquare.btc.listeners.BalanceListener; -import io.bitsquare.btc.pricefeed.PriceFeed; +import io.bitsquare.btc.pricefeed.PriceFeedService; import io.bitsquare.common.crypto.KeyRing; import io.bitsquare.gui.Navigation; import io.bitsquare.gui.common.model.ActivatableDataModel; @@ -70,7 +70,7 @@ class CreateOfferDataModel extends ActivatableDataModel { private final User user; private final KeyRing keyRing; private final P2PService p2PService; - private final PriceFeed priceFeed; + private final PriceFeedService priceFeedService; final String shortOfferId; private Navigation navigation; private final BlockchainService blockchainService; @@ -122,7 +122,7 @@ class CreateOfferDataModel extends ActivatableDataModel { @Inject CreateOfferDataModel(OpenOfferManager openOfferManager, WalletService walletService, TradeWalletService tradeWalletService, - Preferences preferences, User user, KeyRing keyRing, P2PService p2PService, PriceFeed priceFeed, + Preferences preferences, User user, KeyRing keyRing, P2PService p2PService, PriceFeedService priceFeedService, Navigation navigation, BlockchainService blockchainService, BSFormatter formatter) { this.openOfferManager = openOfferManager; this.walletService = walletService; @@ -131,7 +131,7 @@ class CreateOfferDataModel extends ActivatableDataModel { this.user = user; this.keyRing = keyRing; this.p2PService = p2PService; - this.priceFeed = priceFeed; + this.priceFeedService = priceFeedService; this.navigation = navigation; this.blockchainService = blockchainService; this.formatter = formatter; @@ -187,7 +187,7 @@ class CreateOfferDataModel extends ActivatableDataModel { paymentAccounts.setAll(user.getPaymentAccounts()); if (!preferences.getUseStickyMarketPrice() && isTabSelected) - priceFeed.setCurrencyCode(tradeCurrencyCode.get()); + priceFeedService.setCurrencyCode(tradeCurrencyCode.get()); updateBalance(); } @@ -243,7 +243,7 @@ class CreateOfferDataModel extends ActivatableDataModel { tradeCurrencyCode.set(this.tradeCurrency.getCode()); if (!preferences.getUseStickyMarketPrice()) - priceFeed.setCurrencyCode(tradeCurrencyCode.get()); + priceFeedService.setCurrencyCode(tradeCurrencyCode.get()); calculateVolume(); calculateTotalToPay(); @@ -253,7 +253,7 @@ class CreateOfferDataModel extends ActivatableDataModel { void onTabSelected(boolean isSelected) { this.isTabSelected = isSelected; if (!preferences.getUseStickyMarketPrice() && isTabSelected) - priceFeed.setCurrencyCode(tradeCurrencyCode.get()); + priceFeedService.setCurrencyCode(tradeCurrencyCode.get()); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -306,7 +306,7 @@ class CreateOfferDataModel extends ActivatableDataModel { acceptedCountryCodes, bankId, acceptedBanks, - priceFeed); + priceFeedService); } void onPlaceOffer(Offer offer, TransactionResultHandler resultHandler) { @@ -328,7 +328,7 @@ class CreateOfferDataModel extends ActivatableDataModel { paymentAccount.setSelectedTradeCurrency(tradeCurrency); if (!preferences.getUseStickyMarketPrice()) - priceFeed.setCurrencyCode(code); + priceFeedService.setCurrencyCode(code); Optional tradeCurrencyOptional = preferences.getTradeCurrenciesAsObservable().stream().filter(e -> e.getCode().equals(code)).findAny(); if (!tradeCurrencyOptional.isPresent()) { diff --git a/gui/src/main/java/io/bitsquare/gui/main/offer/createoffer/CreateOfferView.java b/gui/src/main/java/io/bitsquare/gui/main/offer/createoffer/CreateOfferView.java index 9841f45a2e..e788801970 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/offer/createoffer/CreateOfferView.java +++ b/gui/src/main/java/io/bitsquare/gui/main/offer/createoffer/CreateOfferView.java @@ -208,8 +208,8 @@ public class CreateOfferView extends ActivatableViewAndModel implements ViewModel { private final BtcValidator btcValidator; private final P2PService p2PService; - private PriceFeed priceFeed; + private PriceFeedService priceFeedService; private Preferences preferences; private Navigation navigation; final BSFormatter formatter; @@ -127,7 +127,7 @@ class CreateOfferViewModel extends ActivatableWithDataModel errorMessageListener; private Offer offer; private Timer timeoutTimer; - private PriceFeed.Type priceFeedType; + private PriceFeedService.Type priceFeedType; private boolean inputIsMarketBasedPrice; private ChangeListener useMarketBasedPriceListener; private ChangeListener currencyCodeListener; @@ -139,14 +139,14 @@ class CreateOfferViewModel extends ActivatableWithDataModel price.set(formatter.formatFiat(newValue)); volumeAsFiatListener = (ov, oldValue, newValue) -> volume.set(formatter.formatFiat(newValue)); - isWalletFundedListener = (ov, oldValue, newValue) -> { - updateButtonDisableState(); - }; + isWalletFundedListener = (ov, oldValue, newValue) -> updateButtonDisableState(); /* feeFromFundingTxListener = (ov, oldValue, newValue) -> { updateButtonDisableState(); };*/ @@ -350,20 +348,12 @@ class CreateOfferViewModel extends ActivatableWithDataModel 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("ETC")) { + 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") @@ -430,7 +420,7 @@ class CreateOfferViewModel extends ActivatableWithDataModel o1.getOffer().getPaymentMethod().compareTo(o2.getOffer().getPaymentMethod())); avatarColumn.setComparator((o1, o2) -> o1.getOffer().getOwnerNodeAddress().hostName.compareTo(o2.getOffer().getOwnerNodeAddress().hostName)); - nrOfOffersLabel = new Label("Nr. of offers: -"); + nrOfOffersLabel = new Label(""); nrOfOffersLabel.setId("num-offers"); GridPane.setHalignment(nrOfOffersLabel, HPos.LEFT); GridPane.setVgrow(nrOfOffersLabel, Priority.NEVER); @@ -209,6 +209,7 @@ public class OfferBookView extends ActivatableViewAndModel nrOfOffersLabel.setText("Nr. of offers: " + model.getOfferList().size()); } @Override @@ -254,7 +255,6 @@ public class OfferBookView extends ActivatableViewAndModel nrOfOffersLabel.setText("Nr. of offers: " + model.getOfferList().size()); model.getOfferList().addListener(offerListListener); nrOfOffersLabel.setText("Nr. of offers: " + model.getOfferList().size()); } @@ -478,7 +478,7 @@ public class OfferBookView extends ActivatableViewAndModel observable, Number oldValue, Number newValue) { if (offerBookListItem != null && offerBookListItem.getOffer().getPrice() != null) { setText(model.getPrice(offerBookListItem)); - model.priceFeed.currenciesUpdateFlagProperty().removeListener(listener); + model.priceFeedService.currenciesUpdateFlagProperty().removeListener(listener); } } }; @@ -490,14 +490,14 @@ public class OfferBookView extends ActivatableViewAndModel observable, Number oldValue, Number newValue) { if (offerBookListItem != null && offerBookListItem.getOffer().getOfferVolume() != null) { setText(model.getVolume(offerBookListItem)); - model.priceFeed.currenciesUpdateFlagProperty().removeListener(listener); + model.priceFeedService.currenciesUpdateFlagProperty().removeListener(listener); } } }; @@ -539,14 +539,14 @@ public class OfferBookView extends ActivatableViewAndModel paymentAccounts) { - for (PaymentAccount paymentAccount : paymentAccounts) { - if (isPaymentAccountValidForOffer(offer, paymentAccount)) - return true; - } - return false; - } - - //TODO not tested with all combinations yet.... - static boolean isPaymentAccountValidForOffer(Offer offer, PaymentAccount paymentAccount) { - // check if we have a matching currency - Set paymentAccountCurrencyCodes = paymentAccount.getTradeCurrencies().stream().map(TradeCurrency::getCode).collect(Collectors.toSet()); - boolean matchesCurrencyCode = paymentAccountCurrencyCodes.contains(offer.getCurrencyCode()); - if (!matchesCurrencyCode) - return false; - - // check if we have a matching payment method or if its a bank account payment method which is treated special - if (paymentAccount instanceof CountryBasedPaymentAccount) { - CountryBasedPaymentAccount countryBasedPaymentAccount = (CountryBasedPaymentAccount) paymentAccount; - - // check if we have a matching country - boolean matchesCountryCodes = offer.getAcceptedCountryCodes() != null && countryBasedPaymentAccount.getCountry() != null && - offer.getAcceptedCountryCodes().contains(countryBasedPaymentAccount.getCountry().code); - if (!matchesCountryCodes) - return false; - - if (paymentAccount instanceof SepaAccount || offer.getPaymentMethod().equals(PaymentMethod.SEPA)) { - boolean samePaymentMethod = paymentAccount.getPaymentMethod().equals(offer.getPaymentMethod()); - return samePaymentMethod; - } else if (offer.getPaymentMethod().equals(PaymentMethod.SAME_BANK) || - offer.getPaymentMethod().equals(PaymentMethod.SPECIFIC_BANKS)) { - - checkNotNull(offer.getAcceptedBankIds(), "offer.getAcceptedBankIds() must not be null"); - if (paymentAccount instanceof SpecificBanksAccount) { - // check if we have a matching bank - boolean offerSideMatchesBank = offer.getAcceptedBankIds().contains(((BankAccount) paymentAccount).getBankId()); - boolean paymentAccountSideMatchesBank = ((SpecificBanksAccount) paymentAccount).getAcceptedBanks().contains(offer.getBankId()); - return offerSideMatchesBank && paymentAccountSideMatchesBank; - } else { - // national or same bank - boolean matchesBank = offer.getAcceptedBankIds().contains(((BankAccount) paymentAccount).getBankId()); - return matchesBank; - } - } else { - if (paymentAccount instanceof SpecificBanksAccount) { - // check if we have a matching bank - boolean paymentAccountSideMatchesBank = ((SpecificBanksAccount) paymentAccount).getAcceptedBanks().contains(offer.getBankId()); - return paymentAccountSideMatchesBank; - } else if (paymentAccount instanceof SameBankAccount) { - // check if we have a matching bank - boolean paymentAccountSideMatchesBank = ((SameBankAccount) paymentAccount).getBankId().equals(offer.getBankId()); - return paymentAccountSideMatchesBank; - } else { - // national - return true; - } - } - - } else { - return paymentAccount.getPaymentMethod().equals(offer.getPaymentMethod()); - } + return PaymentAccountUtil.isAnyPaymentAccountValidForOffer(offer, user.getPaymentAccounts()); } public boolean hasPaymentAccountForCurrency() { diff --git a/gui/src/main/java/io/bitsquare/gui/main/offer/takeoffer/TakeOfferDataModel.java b/gui/src/main/java/io/bitsquare/gui/main/offer/takeoffer/TakeOfferDataModel.java index 22316c5cc5..2c09a20fd9 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/offer/takeoffer/TakeOfferDataModel.java +++ b/gui/src/main/java/io/bitsquare/gui/main/offer/takeoffer/TakeOfferDataModel.java @@ -26,14 +26,14 @@ import io.bitsquare.btc.TradeWalletService; import io.bitsquare.btc.WalletService; import io.bitsquare.btc.blockchain.BlockchainService; import io.bitsquare.btc.listeners.BalanceListener; -import io.bitsquare.btc.pricefeed.PriceFeed; +import io.bitsquare.btc.pricefeed.PriceFeedService; import io.bitsquare.gui.common.model.ActivatableDataModel; import io.bitsquare.gui.main.overlays.notifications.Notification; import io.bitsquare.gui.main.overlays.popups.Popup; import io.bitsquare.gui.util.BSFormatter; import io.bitsquare.locale.CurrencyUtil; -import io.bitsquare.locale.TradeCurrency; import io.bitsquare.payment.PaymentAccount; +import io.bitsquare.payment.PaymentAccountUtil; import io.bitsquare.payment.PaymentMethod; import io.bitsquare.trade.TradeManager; import io.bitsquare.trade.handlers.TradeResultHandler; @@ -41,14 +41,12 @@ import io.bitsquare.trade.offer.Offer; import io.bitsquare.user.Preferences; import io.bitsquare.user.User; import javafx.beans.property.*; -import javafx.collections.FXCollections; import javafx.collections.ObservableList; import org.bitcoinj.core.Coin; import org.bitcoinj.core.Transaction; import org.bitcoinj.utils.ExchangeRate; import org.bitcoinj.utils.Fiat; -import java.util.ArrayList; import java.util.List; import static com.google.common.base.Preconditions.checkArgument; @@ -65,7 +63,7 @@ class TakeOfferDataModel extends ActivatableDataModel { final WalletService walletService; private final User user; private final Preferences preferences; - private final PriceFeed priceFeed; + private final PriceFeedService priceFeedService; private final BlockchainService blockchainService; private final BSFormatter formatter; @@ -104,14 +102,14 @@ class TakeOfferDataModel extends ActivatableDataModel { @Inject TakeOfferDataModel(TradeManager tradeManager, TradeWalletService tradeWalletService, WalletService walletService, User user, - Preferences preferences, PriceFeed priceFeed, BlockchainService blockchainService, + Preferences preferences, PriceFeedService priceFeedService, BlockchainService blockchainService, BSFormatter formatter) { this.tradeManager = tradeManager; this.tradeWalletService = tradeWalletService; this.walletService = walletService; this.user = user; this.preferences = preferences; - this.priceFeed = priceFeed; + this.priceFeedService = priceFeedService; this.blockchainService = blockchainService; this.formatter = formatter; @@ -139,7 +137,7 @@ class TakeOfferDataModel extends ActivatableDataModel { // feeFromFundingTxProperty.set(FeePolicy.getMinRequiredFeeForFundingTx()); if (!preferences.getUseStickyMarketPrice() && isTabSelected) - priceFeed.setCurrencyCode(offer.getCurrencyCode()); + priceFeedService.setCurrencyCode(offer.getCurrencyCode()); tradeManager.checkOfferAvailability(offer, () -> { @@ -214,13 +212,13 @@ class TakeOfferDataModel extends ActivatableDataModel { offer.resetState(); if (!preferences.getUseStickyMarketPrice()) - priceFeed.setCurrencyCode(offer.getCurrencyCode()); + priceFeedService.setCurrencyCode(offer.getCurrencyCode()); } void onTabSelected(boolean isSelected) { this.isTabSelected = isSelected; if (!preferences.getUseStickyMarketPrice() && isTabSelected) - priceFeed.setCurrencyCode(offer.getCurrencyCode()); + priceFeedService.setCurrencyCode(offer.getCurrencyCode()); } @@ -271,17 +269,7 @@ class TakeOfferDataModel extends ActivatableDataModel { } ObservableList getPossiblePaymentAccounts() { - ObservableList paymentAccounts = FXCollections.observableArrayList(new ArrayList<>()); - for (PaymentAccount paymentAccount : user.getPaymentAccounts()) { - if (paymentAccount.getPaymentMethod().equals(offer.getPaymentMethod())) { - for (TradeCurrency tradeCurrency : paymentAccount.getTradeCurrencies()) { - if (tradeCurrency.getCode().equals(offer.getCurrencyCode())) { - paymentAccounts.add(paymentAccount); - } - } - } - } - return paymentAccounts; + return PaymentAccountUtil.getPossiblePaymentAccounts(offer, user.getPaymentAccounts()); } boolean hasAcceptedArbitrators() { diff --git a/gui/src/main/java/io/bitsquare/gui/main/offer/takeoffer/TakeOfferView.java b/gui/src/main/java/io/bitsquare/gui/main/offer/takeoffer/TakeOfferView.java index 71e532581c..1e2d99172a 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/offer/takeoffer/TakeOfferView.java +++ b/gui/src/main/java/io/bitsquare/gui/main/offer/takeoffer/TakeOfferView.java @@ -192,8 +192,8 @@ public class TakeOfferView extends ActivatableViewAndModel onShowPayFundsScreen(), 200, TimeUnit.MILLISECONDS); + /* if (DevFlags.DEV_MODE) + UserThread.runAfter(() -> onShowPayFundsScreen(), 200, TimeUnit.MILLISECONDS);*/ } @Override @@ -268,7 +268,7 @@ public class TakeOfferView extends ActivatableViewAndModel im final TakeOfferDataModel dataModel; private final BtcValidator btcValidator; private final P2PService p2PService; - private PriceFeed priceFeed; + private PriceFeedService priceFeedService; private final Navigation navigation; final BSFormatter formatter; @@ -107,14 +107,14 @@ class TakeOfferViewModel extends ActivatableWithDataModel im /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public TakeOfferViewModel(TakeOfferDataModel dataModel, BtcValidator btcValidator, P2PService p2PService, PriceFeed priceFeed, + public TakeOfferViewModel(TakeOfferDataModel dataModel, BtcValidator btcValidator, P2PService p2PService, PriceFeedService priceFeedService, Navigation navigation, BSFormatter formatter) { super(dataModel); this.dataModel = dataModel; this.btcValidator = btcValidator; this.p2PService = p2PService; - this.priceFeed = priceFeed; + this.priceFeedService = priceFeedService; this.navigation = navigation; this.formatter = formatter; @@ -141,20 +141,12 @@ class TakeOfferViewModel extends ActivatableWithDataModel im updateSpinnerInfo(); //TODO remove after AUGUST, 30 - String key = "ETH-ETHC-Warning"; + String key = "ETH-ETC-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("ETC")) { + 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") diff --git a/gui/src/main/java/io/bitsquare/gui/main/overlays/windows/OfferDetailsWindow.java b/gui/src/main/java/io/bitsquare/gui/main/overlays/windows/OfferDetailsWindow.java index e32f63709c..f7a5891b60 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/overlays/windows/OfferDetailsWindow.java +++ b/gui/src/main/java/io/bitsquare/gui/main/overlays/windows/OfferDetailsWindow.java @@ -32,8 +32,10 @@ import io.bitsquare.gui.main.overlays.popups.Popup; import io.bitsquare.gui.util.BSFormatter; import io.bitsquare.gui.util.Layout; import io.bitsquare.locale.BSResources; +import io.bitsquare.locale.BankUtil; import io.bitsquare.locale.CountryUtil; import io.bitsquare.locale.CurrencyUtil; +import io.bitsquare.payment.PaymentAccount; import io.bitsquare.payment.PaymentMethod; import io.bitsquare.trade.offer.Offer; import io.bitsquare.user.Preferences; @@ -194,15 +196,29 @@ public class OfferDetailsWindow extends Overlay { addLabelTextField(gridPane, ++rowIndex, "Price:", formatter.formatPriceWithCode(price)); } } - if (offer.isMyOffer(keyRing) && user.getPaymentAccount(offer.getOffererPaymentAccountId()) != null) - addLabelTextField(gridPane, ++rowIndex, "Payment account:", user.getPaymentAccount(offer.getOffererPaymentAccountId()).getAccountName()); - else - addLabelTextField(gridPane, ++rowIndex, "Payment method:", BSResources.get(offer.getPaymentMethod().getId())); - + final PaymentMethod paymentMethod = offer.getPaymentMethod(); + final String offererPaymentAccountId = offer.getOffererPaymentAccountId(); + final PaymentAccount paymentAccount = user.getPaymentAccount(offererPaymentAccountId); + final String bankId = offer.getBankId(); + final boolean isSpecificBanks = paymentMethod.equals(PaymentMethod.SPECIFIC_BANKS); + final boolean isNationalBanks = paymentMethod.equals(PaymentMethod.NATIONAL_BANK); + if (offer.isMyOffer(keyRing) && offererPaymentAccountId != null && paymentAccount != null) { + addLabelTextField(gridPane, ++rowIndex, "Payment account:", paymentAccount.getAccountName()); + } else { + final String method = BSResources.get(paymentMethod.getId()); + if (isNationalBanks || isSpecificBanks) { + if (BankUtil.isBankIdRequired(offer.getCountryCode())) + addLabelTextField(gridPane, ++rowIndex, "Payment method (offerers bank ID):", method + " (" + bankId + ")"); + else if (BankUtil.isBankNameRequired(offer.getCountryCode())) + addLabelTextField(gridPane, ++rowIndex, "Payment method (offerers bank name):", method + " (" + bankId + ")"); + } else { + addLabelTextField(gridPane, ++rowIndex, "Payment method:", method); + } + } if (showAcceptedBanks) { - if (offer.getPaymentMethod().equals(PaymentMethod.SAME_BANK)) { - addLabelTextField(gridPane, ++rowIndex, "Bank name:", acceptedBanks.get(0)); - } else if (offer.getPaymentMethod().equals(PaymentMethod.SPECIFIC_BANKS)) { + if (paymentMethod.equals(PaymentMethod.SAME_BANK)) { + addLabelTextField(gridPane, ++rowIndex, "Bank ID (e.g. BIC or SWIFT):", acceptedBanks.get(0)); + } else if (isSpecificBanks) { String value = Joiner.on(", ").join(acceptedBanks); Tooltip tooltip = new Tooltip("Accepted banks: " + value); TextField acceptedBanksTextField = addLabelTextField(gridPane, ++rowIndex, "Accepted banks:", value).second; diff --git a/gui/src/main/java/io/bitsquare/gui/main/settings/network/NetworkSettingsView.fxml b/gui/src/main/java/io/bitsquare/gui/main/settings/network/NetworkSettingsView.fxml index 44006d185e..d039c39762 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/settings/network/NetworkSettingsView.fxml +++ b/gui/src/main/java/io/bitsquare/gui/main/settings/network/NetworkSettingsView.fxml @@ -32,16 +32,19 @@ - + -