From fc01f797f092818b5022b788f2ca876095d756f0 Mon Sep 17 00:00:00 2001 From: SimonTuberlin Date: Mon, 13 Mar 2017 21:25:11 +0100 Subject: [PATCH] Prepared DownloadUtil task and updateAlert window --- .idea/copyright/profiles_settings.xml | 3 - .../bitsquare/common/util/DownloadUtil.java | 110 ++ .../io/bitsquare/common/util/Utilities.java | 483 ++++++++ .../arbitration/ArbitratorManager.java | 299 +++++ .../io/bitsquare/gui/main/MainViewModel.java | 1044 +++++++++++++++++ .../windows/DisplayUpdateDownloadWindow.java | 131 +++ 6 files changed, 2067 insertions(+), 3 deletions(-) delete mode 100644 .idea/copyright/profiles_settings.xml create mode 100644 common/src/main/java/io/bitsquare/common/util/DownloadUtil.java create mode 100644 common/src/main/java/io/bitsquare/common/util/Utilities.java create mode 100644 core/src/main/java/io/bitsquare/arbitration/ArbitratorManager.java create mode 100644 gui/src/main/java/io/bitsquare/gui/main/MainViewModel.java create mode 100644 gui/src/main/java/io/bitsquare/gui/main/overlays/windows/DisplayUpdateDownloadWindow.java diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml deleted file mode 100644 index e7bedf3377..0000000000 --- a/.idea/copyright/profiles_settings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/common/src/main/java/io/bitsquare/common/util/DownloadUtil.java b/common/src/main/java/io/bitsquare/common/util/DownloadUtil.java new file mode 100644 index 0000000000..d72d60f3ca --- /dev/null +++ b/common/src/main/java/io/bitsquare/common/util/DownloadUtil.java @@ -0,0 +1,110 @@ +package io.bitsquare.common.util; + +/** + * A utility that downloads a file from a URL. + * @author www.codejava.net + * + */ + + +import javafx.concurrent.Task; +import javafx.scene.control.ProgressIndicator; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; + + +public class DownloadUtil extends Task { + +/* public static void main(String [] args) { + try { + downloadFile("https://github.com/bitsquare/bitsquare/releases/download/v0.4.9.9/Bitsquare-64bit-0.4.9.9.exe", "/home/bob/test"); + } catch (IOException e) {e.printStackTrace();} + } +*/ + private static final int BUFFER_SIZE = 4096; + private final String fileURL; + private final String saveDir; + + /** + * Downloads a file from a URL + * @param fileURL HTTP URL of the file to be downloaded + * @param saveDir path of the directory to save the file + */ + public DownloadUtil (final String fileURL, final String saveDir) { + this.fileURL = fileURL; + this.saveDir = saveDir; + } + + /** + * Downloads a file from a URL + * @param fileURL HTTP URL of the file to be downloaded + */ + public DownloadUtil (final String fileURL) { + this.fileURL = fileURL; + this.saveDir = System.getProperty("java.io.tmpdir"); + } + + @Override protected File call() throws Exception{ + URL url = new URL(fileURL); + HttpURLConnection httpConn = (HttpURLConnection) url.openConnection(); + int responseCode = httpConn.getResponseCode(); + + // always check HTTP response code first + if (responseCode == HttpURLConnection.HTTP_OK) { + String fileName = ""; + String disposition = httpConn.getHeaderField("Content-Disposition"); + String contentType = httpConn.getContentType(); + int contentLength = httpConn.getContentLength(); + if (! contentLength > 0) + contentLength = -1; + + if (disposition != null) { + // extracts file name from header field + int index = disposition.indexOf("filename="); + if (index > 0) { + fileName = disposition.substring(index + 9, disposition.length()); + } + } else { + // extracts file name from URL + fileName = fileURL.substring(fileURL.lastIndexOf("/") + 1, fileURL.length()); + } + +/* System.out.println("Content-Type = " + contentType); + System.out.println("Content-Disposition = " + disposition); + System.out.println("Content-Length = " + contentLength); + System.out.println("fileName = " + fileName); +*/ + // opens input stream from the HTTP connection + InputStream inputStream = httpConn.getInputStream(); + String saveFilePath = saveDir + File.separator + fileName; + + // opens an output stream to save into file + FileOutputStream outputStream = new FileOutputStream(saveFilePath); + + int bytesRead = -1; + int totalRead = 0; + byte[] buffer = new byte[BUFFER_SIZE]; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + totalRead += bytesRead; + updateProgress(totalRead, contentLength); + } + + try { + outputStream.close(); + } catch (IOException e) { + } finally { + inputStream.close(); + } + + System.out.println("File downloaded"); + return saveFilePath; + } else { + System.out.println("No file to download. Server replied HTTP code: " + responseCode); + } + httpConn.disconnect(); + return null; + } +} \ No newline at end of file diff --git a/common/src/main/java/io/bitsquare/common/util/Utilities.java b/common/src/main/java/io/bitsquare/common/util/Utilities.java new file mode 100644 index 0000000000..dbe4ebc9a6 --- /dev/null +++ b/common/src/main/java/io/bitsquare/common/util/Utilities.java @@ -0,0 +1,483 @@ +/* + * 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.common.util; + +import com.google.common.base.Charsets; +import com.google.common.io.CharStreams; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.google.gson.*; +import io.bitsquare.io.LookAheadObjectInputStream; +import javafx.concurrent.Task; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.Cipher; +import java.awt.*; +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.net.URLConnection; +import java.security.NoSuchAlgorithmException; +import java.util.Locale; +import java.util.Random; +import java.util.concurrent.*; + + +/** + * General utilities + */ +public class Utilities { + private static final Logger log = LoggerFactory.getLogger(Utilities.class); + private static long lastTimeStamp = System.currentTimeMillis(); + public static final String LB = System.getProperty("line.separator"); + public static final String LB2 = LB + LB; + + public static String objectToJson(Object object) { + Gson gson = new GsonBuilder() + .setExclusionStrategies(new AnnotationExclusionStrategy()) + /*.excludeFieldsWithModifiers(Modifier.TRANSIENT)*/ + /* .setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE)*/ + .setPrettyPrinting() + .create(); + return gson.toJson(object); + } + + public static ListeningExecutorService getListeningExecutorService(String name, + int corePoolSize, + int maximumPoolSize, + long keepAliveTimeInSec) { + return MoreExecutors.listeningDecorator(getThreadPoolExecutor(name, corePoolSize, maximumPoolSize, keepAliveTimeInSec)); + } + + public static ThreadPoolExecutor getThreadPoolExecutor(String name, + int corePoolSize, + int maximumPoolSize, + long keepAliveTimeInSec) { + final ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setNameFormat(name) + .setDaemon(true) + .build(); + ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTimeInSec, + TimeUnit.SECONDS, new ArrayBlockingQueue<>(maximumPoolSize), threadFactory); + executor.allowCoreThreadTimeOut(true); + executor.setRejectedExecutionHandler((r, e) -> { + log.debug("RejectedExecutionHandler called"); + }); + return executor; + } + + + public static ScheduledThreadPoolExecutor getScheduledThreadPoolExecutor(String name, + int corePoolSize, + int maximumPoolSize, + long keepAliveTimeInSec) { + final ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setNameFormat(name) + .setDaemon(true) + .setPriority(Thread.MIN_PRIORITY) + .build(); + ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(corePoolSize, threadFactory); + executor.setKeepAliveTime(keepAliveTimeInSec, TimeUnit.SECONDS); + executor.allowCoreThreadTimeOut(true); + executor.setMaximumPoolSize(maximumPoolSize); + executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); + executor.setRejectedExecutionHandler((r, e) -> { + log.debug("RejectedExecutionHandler called"); + }); + return executor; + } + + public static boolean isUnix() { + return isOSX() || isLinux() || getOSName().contains("freebsd"); + } + + public static boolean isWindows() { + return getOSName().contains("win"); + } + + public static boolean isOSX() { + return getOSName().contains("mac") || getOSName().contains("darwin"); + } + + public static boolean isLinux() { + return getOSName().contains("linux"); + } + + private static String getOSName() { + return System.getProperty("os.name").toLowerCase(Locale.US); + } + + public static String getOSArchitecture() { + String osArch = System.getProperty("os.arch"); + if (isWindows()) { + // See: Like always windows needs extra treatment + // https://stackoverflow.com/questions/20856694/how-to-find-the-os-bit-type + String arch = System.getenv("PROCESSOR_ARCHITECTURE"); + String wow64Arch = System.getenv("PROCESSOR_ARCHITEW6432"); + return arch.endsWith("64") + || wow64Arch != null && wow64Arch.endsWith("64") + ? "64" : "32"; + } else if (osArch.contains("arm")) { + // armv8 is 64 bit, armv7l is 32 bit + return osArch.contains("64") || osArch.contains("v8") ? "64" : "32"; + } else if (isLinux()) { + return osArch.startsWith("i") ? "32" : "64"; + } else { + return osArch.contains("64") ? "64" : osArch; + } + } + + public static void printSysInfo() { + log.info("os.name: " + System.getProperty("os.name")); + log.info("os.version: " + System.getProperty("os.version")); + log.info("os.arch: " + System.getProperty("os.arch")); + log.info("sun.arch.data.model: " + getJVMArchitecture()); + log.info("JRE: " + System.getProperty("java.runtime.version", "-") + " (" + System.getProperty("java.vendor", "-") + ")"); + log.info("JVM: " + System.getProperty("java.vm.version", "-") + " (" + System.getProperty("java.vm.name", "-") + ")"); + } + + public static String getJVMArchitecture() { + return System.getProperty("sun.arch.data.model"); + } + + public static boolean isCorrectOSArchitecture() { + boolean result = getOSArchitecture().endsWith(getJVMArchitecture()); + if (!result) { + log.warn("System.getProperty(\"os.arch\") " + System.getProperty("os.arch")); + log.warn("System.getenv(\"ProgramFiles(x86)\") " + System.getenv("ProgramFiles(x86)")); + log.warn("System.getenv(\"PROCESSOR_ARCHITECTURE\")" + System.getenv("PROCESSOR_ARCHITECTURE")); + log.warn("System.getenv(\"PROCESSOR_ARCHITEW6432\") " + System.getenv("PROCESSOR_ARCHITEW6432")); + log.warn("System.getProperty(\"sun.arch.data.model\") " + System.getProperty("sun.arch.data.model")); + } + return result; + } + + public static void openURI(URI uri) throws IOException { + if (!isLinux() + && Desktop.isDesktopSupported() + && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { + Desktop.getDesktop().browse(uri); + } else { + // Maybe Application.HostServices works in those cases? + // HostServices hostServices = getHostServices(); + // hostServices.showDocument(uri.toString()); + + // On Linux Desktop is poorly implemented. + // See https://stackoverflow.com/questions/18004150/desktop-api-is-not-supported-on-the-current-platform + if (!DesktopUtil.browse(uri)) + throw new IOException("Failed to open URI: " + uri.toString()); + } + } + + public static void openDirectory(File directory) throws IOException { + if (!isLinux() + && Desktop.isDesktopSupported() + && Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) { + Desktop.getDesktop().open(directory); + } else { + // Maybe Application.HostServices works in those cases? + // HostServices hostServices = getHostServices(); + // hostServices.showDocument(uri.toString()); + + // On Linux Desktop is poorly implemented. + // See https://stackoverflow.com/questions/18004150/desktop-api-is-not-supported-on-the-current-platform + if (!DesktopUtil.open(directory)) + throw new IOException("Failed to open directory: " + directory.toString()); + } + } + + public static File downloadFile(String fileURL, String saveDir, ProgressIndicator indicator) throws IOException { + DownloadUtil task; + if (saveDir != null) + task = new DownloadUtil(fileURL, saveDir); + else + task = new DownloadUtil(fileURL); // Tries to use system temp directory + if (indicator != null) { + indicator.progressProperty().unbind(); + indicator.progressProperty().bind(task.progressProperty()); + } + Thread th = new Thread(task); + th.start(); + } + + public static void printSystemLoad() { + Runtime runtime = Runtime.getRuntime(); + long free = runtime.freeMemory() / 1024 / 1024; + long total = runtime.totalMemory() / 1024 / 1024; + long used = total - free; + log.info("System load (no. threads/used memory (MB)): " + Thread.activeCount() + "/" + used); + } + + public static void copyToClipboard(String content) { + try { + if (content != null && content.length() > 0) { + Clipboard clipboard = Clipboard.getSystemClipboard(); + ClipboardContent clipboardContent = new ClipboardContent(); + clipboardContent.putString(content); + clipboard.setContent(clipboardContent); + } + } catch (Throwable e) { + log.error("copyToClipboard failed " + e.getMessage()); + e.printStackTrace(); + } + } + + public static byte[] concatByteArrays(byte[]... arrays) { + int totalLength = 0; + for (byte[] array : arrays) { + totalLength += array.length; + } + + byte[] result = new byte[totalLength]; + int currentIndex = 0; + for (byte[] array : arrays) { + System.arraycopy(array, 0, result, currentIndex, array.length); + currentIndex += array.length; + } + return result; + } + + public static T jsonToObject(String jsonString, Class classOfT) { + Gson gson = + new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE).setPrettyPrinting().create(); + return gson.fromJson(jsonString, classOfT); + } + + +/* public static Object deserializeHexStringToObject(String serializedHexString) { + Object result = null; + try { + ByteArrayInputStream byteInputStream = + new ByteArrayInputStream(org.bitcoinj.core.Utils.parseAsHexOrBase58(serializedHexString)); + + try (ObjectInputStream objectInputStream = new LookAheadObjectInputStream(byteInputStream)) { + result = objectInputStream.readObject(); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } finally { + byteInputStream.close(); + + } + + } catch (IOException i) { + i.printStackTrace(); + } + return result; + } + + + public static String serializeObjectToHexString(Serializable serializable) { + String result = null; + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + try { + ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); + objectOutputStream.writeObject(serializable); + + result = org.bitcoinj.core.Utils.HEX.encode(byteArrayOutputStream.toByteArray()); + byteArrayOutputStream.close(); + objectOutputStream.close(); + + } catch (IOException e) { + e.printStackTrace(); + } + return result; + }*/ + + public static T deserialize(byte[] data) { + ByteArrayInputStream bis = new ByteArrayInputStream(data); + ObjectInput in = null; + Object result = null; + try { + in = new LookAheadObjectInputStream(bis, true); + result = in.readObject(); + if (!(result instanceof Serializable)) + throw new RuntimeException("Object not of type Serializable"); + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + bis.close(); + } catch (IOException ex) { + // ignore close exception + } + try { + if (in != null) { + in.close(); + } + } catch (IOException ex) { + // ignore close exception + } + } + return (T) result; + } + + public static byte[] serialize(Serializable object) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutput out = null; + byte[] result = null; + try { + out = new ObjectOutputStream(bos); + out.writeObject(object); + out.flush(); + result = bos.toByteArray().clone(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (out != null) { + out.close(); + } + } catch (IOException ignore) { + } + try { + bos.close(); + } catch (IOException ignore) { + } + } + return result; + } + + private static void printElapsedTime(String msg) { + if (!msg.isEmpty()) { + msg += " / "; + } + long timeStamp = System.currentTimeMillis(); + log.debug(msg + "Elapsed: " + String.valueOf(timeStamp - lastTimeStamp)); + lastTimeStamp = timeStamp; + } + + public static void printElapsedTime() { + printElapsedTime(""); + } + + + public static Object copy(Serializable orig) throws IOException, ClassNotFoundException { + try { + // Write the object out to a byte array + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream out = new ObjectOutputStream(bos); + out.writeObject(orig); + out.flush(); + out.close(); + + // Make an input stream from the byte array and read + // a copy of the object back in. + ObjectInputStream in = new LookAheadObjectInputStream(new ByteArrayInputStream(bos.toByteArray()), true); + Object obj = in.readObject(); + return obj; + } catch (IOException | ClassNotFoundException e) { + e.printStackTrace(); + throw e; + } + } + + public static String readTextFileFromServer(String url, String userAgent) throws IOException { + URLConnection connection = URI.create(url).toURL().openConnection(); + connection.setDoOutput(true); + connection.setUseCaches(false); + connection.setConnectTimeout((int) TimeUnit.SECONDS.toMillis(10)); + connection.addRequestProperty("User-Agent", userAgent); + connection.connect(); + try (InputStream inputStream = connection.getInputStream()) { + return CharStreams.toString(new InputStreamReader(inputStream, Charsets.UTF_8)); + } catch (IOException e) { + e.printStackTrace(); + throw e; + } + } + + public static void setThreadName(String name) { + Thread.currentThread().setName(name + "-" + new Random().nextInt(10000)); + } + + private static class AnnotationExclusionStrategy implements ExclusionStrategy { + @Override + public boolean shouldSkipField(FieldAttributes f) { + return f.getAnnotation(JsonExclude.class) != null; + } + + @Override + public boolean shouldSkipClass(Class clazz) { + return false; + } + } + + public static void checkCryptoPolicySetup() throws NoSuchAlgorithmException, LimitedKeyStrengthException { + if (Cipher.getMaxAllowedKeyLength("AES") > 128) + log.debug("Congratulations, you have unlimited key length support!"); + else + throw new LimitedKeyStrengthException(); + } + + 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); + } + + public static String getRandomPrefix(int minLength, int maxLength) { + int length = minLength + new Random().nextInt(maxLength - minLength + 1); + String result; + switch (new Random().nextInt(3)) { + case 0: + result = RandomStringUtils.randomAlphabetic(length); + break; + case 1: + result = RandomStringUtils.randomNumeric(length); + break; + case 2: + default: + result = RandomStringUtils.randomAlphanumeric(length); + } + + switch (new Random().nextInt(3)) { + case 0: + result = result.toUpperCase(); + break; + case 1: + result = result.toLowerCase(); + break; + case 2: + default: + } + + return result; + } + + public static String getShortId(String id) { + return getShortId(id, "-"); + } + + public static String getShortId(String id, String sep) { + String[] chunks = id.split(sep); + if (chunks.length > 0) + return chunks[0]; + else + return id.substring(0, Math.min(8, id.length())); + } +} diff --git a/core/src/main/java/io/bitsquare/arbitration/ArbitratorManager.java b/core/src/main/java/io/bitsquare/arbitration/ArbitratorManager.java new file mode 100644 index 0000000000..fe521e9719 --- /dev/null +++ b/core/src/main/java/io/bitsquare/arbitration/ArbitratorManager.java @@ -0,0 +1,299 @@ +/* + * 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.arbitration; + +import com.google.inject.Inject; +import io.bitsquare.common.Timer; +import io.bitsquare.common.UserThread; +import io.bitsquare.common.crypto.KeyRing; +import io.bitsquare.common.handlers.ErrorMessageHandler; +import io.bitsquare.common.handlers.ResultHandler; +import io.bitsquare.p2p.BootstrapListener; +import io.bitsquare.p2p.NodeAddress; +import io.bitsquare.p2p.P2PService; +import io.bitsquare.p2p.storage.HashMapChangedListener; +import io.bitsquare.p2p.storage.storageentry.ProtectedStorageEntry; +import io.bitsquare.user.Preferences; +import io.bitsquare.user.User; +import javafx.collections.FXCollections; +import javafx.collections.ObservableMap; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import java.math.BigInteger; +import java.security.PublicKey; +import java.security.SignatureException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.bitcoinj.core.Utils.HEX; + +public class ArbitratorManager { + private static final Logger log = LoggerFactory.getLogger(ArbitratorManager.class); + + /////////////////////////////////////////////////////////////////////////////////////////// + // Static + /////////////////////////////////////////////////////////////////////////////////////////// + + private static final long REPUBLISH_MILLIS = Arbitrator.TTL / 2; + private static final long RETRY_REPUBLISH_SEC = 5; + private static final long REPEATED_REPUBLISH_AT_STARTUP_SEC = 60; + + // Keys for invited arbitrators in bootstrapping phase (before registration is open to anyone and security payment is implemented) + // For developers we add here 2 test keys so one can setup an arbitrator by adding that test pubKey + // to the publicKeys list and use the test PrivKey for arbitrator registration. + // PrivKey for dev testing: 6ac43ea1df2a290c1c8391736aa42e4339c5cb4f110ff0257a13b63211977b7a + // Matching pubKey for dev testing: 027a381b5333a56e1cc3d90d3a7d07f26509adf7029ed06fc997c656621f8da1ee + private static final List publicKeys = new ArrayList<>(Arrays.asList( + "03697a499d24f497b3c46bf716318231e46c4e6a685a4e122d8e2a2b229fa1f4b8", + "0365c6af94681dbee69de1851f98d4684063bf5c2d64b1c73ed5d90434f375a054", + "031c502a60f9dbdb5ae5e438a79819e4e1f417211dd537ac12c9bc23246534c4bd", + "02c1e5a242387b6d5319ce27246cea6edaaf51c3550591b528d2578a4753c56c2c", + "025c319faf7067d9299590dd6c97fe7e56cd4dac61205ccee1cd1fc390142390a2", + "038f6e24c2bfe5d51d0a290f20a9a657c270b94ef2b9c12cd15ca3725fa798fc55", + "0255256ff7fb615278c4544a9bbd3f5298b903b8a011cd7889be19b6b1c45cbefe", + "024a3a37289f08c910fbd925ebc72b946f33feaeff451a4738ee82037b4cda2e95", + "02a88b75e9f0f8afba1467ab26799dcc38fd7a6468fb2795444b425eb43e2c10bd", + "02349a51512c1c04c67118386f4d27d768c5195a83247c150a4b722d161722ba81", + "03f718a2e0dc672c7cdec0113e72c3322efc70412bb95870750d25c32cd98de17d", + "028ff47ee2c56e66313928975c58fa4f1b19a0f81f3a96c4e9c9c3c6768075509e", + "02b517c0cbc3a49548f448ddf004ed695c5a1c52ec110be1bfd65fa0ca0761c94b", + "03df837a3a0f3d858e82f3356b71d1285327f101f7c10b404abed2abc1c94e7169", + "0203a90fb2ab698e524a5286f317a183a84327b8f8c3f7fa4a98fec9e1cefd6b72", + "023c99cc073b851c892d8c43329ca3beb5d2213ee87111af49884e3ce66cbd5ba5", + "027a381b5333a56e1cc3d90d3a7d07f26509adf7029ed06fc997c656621f8da1ee" + )); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Instance fields + /////////////////////////////////////////////////////////////////////////////////////////// + + private final KeyRing keyRing; + private final ArbitratorService arbitratorService; + private final User user; + private Preferences preferences; + private final ObservableMap arbitratorsObservableMap = FXCollections.observableHashMap(); + private final List persistedAcceptedArbitrators; + private Timer republishArbitratorTimer, retryRepublishArbitratorTimer; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public ArbitratorManager(KeyRing keyRing, ArbitratorService arbitratorService, User user, Preferences preferences) { + this.keyRing = keyRing; + this.arbitratorService = arbitratorService; + this.user = user; + this.preferences = preferences; + + persistedAcceptedArbitrators = new ArrayList<>(user.getAcceptedArbitrators()); + user.clearAcceptedArbitrators(); + + arbitratorService.addHashSetChangedListener(new HashMapChangedListener() { + @Override + public void onAdded(ProtectedStorageEntry data) { + if (data.getStoragePayload() instanceof Arbitrator) + updateArbitratorMap(); + } + + @Override + public void onRemoved(ProtectedStorageEntry data) { + if (data.getStoragePayload() instanceof Arbitrator) + updateArbitratorMap(); + } + }); + } + + public void shutDown() { + stopRepublishArbitratorTimer(); + stopRetryRepublishArbitratorTimer(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onAllServicesInitialized() { + if (user.getRegisteredArbitrator() != null) { + P2PService p2PService = arbitratorService.getP2PService(); + if (p2PService.isBootstrapped()) + isBootstrapped(); + else + p2PService.addP2PServiceListener(new BootstrapListener() { + @Override + public void onBootstrapComplete() { + isBootstrapped(); + } + }); + } + + updateArbitratorMap(); + } + + private void isBootstrapped() { + if (republishArbitratorTimer == null) { + republishArbitratorTimer = UserThread.runPeriodically(this::republishArbitrator, REPUBLISH_MILLIS, TimeUnit.MILLISECONDS); + UserThread.runAfter(this::republishArbitrator, REPEATED_REPUBLISH_AT_STARTUP_SEC); + republishArbitrator(); + } + } + + public void updateArbitratorMap() { + Map map = arbitratorService.getArbitrators(); + arbitratorsObservableMap.clear(); + Map filtered = map.values().stream() + .filter(e -> isPublicKeyInList(Utils.HEX.encode(e.getRegistrationPubKey())) + && verifySignature(e.getPubKeyRing().getSignaturePubKey(), e.getRegistrationPubKey(), e.getRegistrationSignature())) + .collect(Collectors.toMap(Arbitrator::getArbitratorNodeAddress, Function.identity())); + + arbitratorsObservableMap.putAll(filtered); + arbitratorsObservableMap.values().stream() + .filter(arbitrator -> persistedAcceptedArbitrators.contains(arbitrator)) + .forEach(user::addAcceptedArbitrator); + + if (preferences.getAutoSelectArbitrators()) { + arbitratorsObservableMap.values().stream() + .filter(user::hasMatchingLanguage) + .forEach(user::addAcceptedArbitrator); + } else { + // if we don't have any arbitrator we set all matching + // we use a delay as we might get our matching arbitrator a bit delayed (first we get one we did not selected + // then we get our selected one - we don't want to activate the first in that case) + UserThread.runAfter(() -> { + if (user.getAcceptedArbitrators().isEmpty()) { + arbitratorsObservableMap.values().stream() + .filter(user::hasMatchingLanguage) + .forEach(user::addAcceptedArbitrator); + } + }, 100, TimeUnit.MILLISECONDS); + } + } + + public void addArbitrator(Arbitrator arbitrator, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + user.setRegisteredArbitrator(arbitrator); + arbitratorsObservableMap.put(arbitrator.getArbitratorNodeAddress(), arbitrator); + arbitratorService.addArbitrator(arbitrator, + () -> { + log.debug("Arbitrator successfully saved in P2P network"); + resultHandler.handleResult(); + + if (arbitratorsObservableMap.size() > 0) + UserThread.runAfter(this::updateArbitratorMap, 100, TimeUnit.MILLISECONDS); + }, + errorMessageHandler::handleErrorMessage); + } + + public void removeArbitrator(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + Arbitrator registeredArbitrator = user.getRegisteredArbitrator(); + if (registeredArbitrator != null) { + user.setRegisteredArbitrator(null); + arbitratorsObservableMap.remove(registeredArbitrator.getArbitratorNodeAddress()); + arbitratorService.removeArbitrator(registeredArbitrator, + () -> { + log.debug("Arbitrator successfully removed from P2P network"); + resultHandler.handleResult(); + }, + errorMessageHandler::handleErrorMessage); + } + } + + public ObservableMap getArbitratorsObservableMap() { + return arbitratorsObservableMap; + } + + // A private key is handed over to selected arbitrators for registration. + // An invited arbitrator will sign at registration his storageSignaturePubKey with that private key and attach the signature and pubKey to his data. + // Other users will check the signature with the list of public keys hardcoded in the app. + public String signStorageSignaturePubKey(ECKey key) { + String keyToSignAsHex = Utils.HEX.encode(keyRing.getPubKeyRing().getSignaturePubKey().getEncoded()); + return key.signMessage(keyToSignAsHex); + } + + @Nullable + public ECKey getRegistrationKey(String privKeyBigIntString) { + try { + return ECKey.fromPrivate(new BigInteger(1, HEX.decode(privKeyBigIntString))); + } catch (Throwable t) { + return null; + } + } + + public boolean isPublicKeyInList(String pubKeyAsHex) { + return publicKeys.contains(pubKeyAsHex); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void republishArbitrator() { + Arbitrator registeredArbitrator = user.getRegisteredArbitrator(); + if (registeredArbitrator != null) { + addArbitrator(registeredArbitrator, + this::updateArbitratorMap, + errorMessage -> { + if (retryRepublishArbitratorTimer == null) + retryRepublishArbitratorTimer = UserThread.runPeriodically(() -> { + stopRetryRepublishArbitratorTimer(); + republishArbitrator(); + }, RETRY_REPUBLISH_SEC); + } + ); + } + } + + private boolean verifySignature(PublicKey storageSignaturePubKey, byte[] registrationPubKey, String signature) { + String keyToSignAsHex = Utils.HEX.encode(storageSignaturePubKey.getEncoded()); + try { + ECKey key = ECKey.fromPublicOnly(registrationPubKey); + key.verifyMessage(keyToSignAsHex, signature); + return true; + } catch (SignatureException e) { + log.warn("verifySignature failed"); + return false; + } + } + + + private void stopRetryRepublishArbitratorTimer() { + if (retryRepublishArbitratorTimer != null) { + retryRepublishArbitratorTimer.stop(); + retryRepublishArbitratorTimer = null; + } + } + + private void stopRepublishArbitratorTimer() { + if (republishArbitratorTimer != null) { + republishArbitratorTimer.stop(); + republishArbitratorTimer = null; + } + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/main/MainViewModel.java b/gui/src/main/java/io/bitsquare/gui/main/MainViewModel.java new file mode 100644 index 0000000000..8ebad4eb9d --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/MainViewModel.java @@ -0,0 +1,1044 @@ +/* + * 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; + +import com.google.inject.Inject; +import io.bitsquare.alert.Alert; +import io.bitsquare.alert.AlertManager; +import io.bitsquare.alert.PrivateNotification; +import io.bitsquare.alert.PrivateNotificationManager; +import io.bitsquare.app.BitsquareApp; +import io.bitsquare.app.DevFlags; +import io.bitsquare.app.Log; +import io.bitsquare.app.Version; +import io.bitsquare.arbitration.ArbitratorManager; +import io.bitsquare.arbitration.Dispute; +import io.bitsquare.arbitration.DisputeManager; +import io.bitsquare.btc.AddressEntry; +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.PriceFeedService; +import io.bitsquare.common.Clock; +import io.bitsquare.common.Timer; +import io.bitsquare.common.UserThread; +import io.bitsquare.common.crypto.*; +import io.bitsquare.filter.FilterManager; +import io.bitsquare.gui.Navigation; +import io.bitsquare.gui.common.model.ViewModel; +import io.bitsquare.gui.components.BalanceTextField; +import io.bitsquare.gui.components.BalanceWithConfirmationTextField; +import io.bitsquare.gui.components.TxIdTextField; +import io.bitsquare.gui.main.overlays.notifications.NotificationCenter; +import io.bitsquare.gui.main.overlays.popups.Popup; +import io.bitsquare.gui.main.overlays.windows.*; +import io.bitsquare.gui.util.BSFormatter; +import io.bitsquare.gui.util.GUIUtil; +import io.bitsquare.locale.CurrencyUtil; +import io.bitsquare.locale.TradeCurrency; +import io.bitsquare.p2p.P2PService; +import io.bitsquare.p2p.P2PServiceListener; +import io.bitsquare.p2p.network.CloseConnectionReason; +import io.bitsquare.p2p.network.Connection; +import io.bitsquare.p2p.network.ConnectionListener; +import io.bitsquare.p2p.peers.keepalive.messages.Ping; +import io.bitsquare.payment.CryptoCurrencyAccount; +import io.bitsquare.payment.OKPayAccount; +import io.bitsquare.payment.PaymentAccount; +import io.bitsquare.trade.Trade; +import io.bitsquare.trade.TradeManager; +import io.bitsquare.trade.offer.OpenOffer; +import io.bitsquare.trade.offer.OpenOfferManager; +import io.bitsquare.user.Preferences; +import io.bitsquare.user.User; +import javafx.beans.property.*; +import javafx.beans.value.ChangeListener; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.collections.SetChangeListener; +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.Wallet; +import org.bitcoinj.store.BlockStoreException; +import org.fxmisc.easybind.EasyBind; +import org.fxmisc.easybind.Subscription; +import org.fxmisc.easybind.monadic.MonadicBinding; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import java.security.Security; +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +public class MainViewModel implements ViewModel { + private static final Logger log = LoggerFactory.getLogger(MainViewModel.class); + + private final WalletService walletService; + private final TradeWalletService tradeWalletService; + private final ArbitratorManager arbitratorManager; + private final P2PService p2PService; + private final TradeManager tradeManager; + private final OpenOfferManager openOfferManager; + private final DisputeManager disputeManager; + final Preferences preferences; + private final AlertManager alertManager; + private PrivateNotificationManager privateNotificationManager; + private FilterManager filterManager; + private final WalletPasswordWindow walletPasswordWindow; + private AddBitcoinNodesWindow addBitcoinNodesWindow; + private final NotificationCenter notificationCenter; + private final TacWindow tacWindow; + private Clock clock; + private KeyRing keyRing; + private final Navigation navigation; + private final BSFormatter formatter; + + // BTC network + final StringProperty btcInfo = new SimpleStringProperty("Initializing"); + final DoubleProperty btcSyncProgress = new SimpleDoubleProperty(DevFlags.STRESS_TEST_MODE ? 0 : -1); + final StringProperty walletServiceErrorMsg = new SimpleStringProperty(); + final StringProperty btcSplashSyncIconId = new SimpleStringProperty(); + final StringProperty marketPriceCurrencyCode = new SimpleStringProperty(""); + 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); + final StringProperty availableBalance = new SimpleStringProperty(); + final StringProperty reservedBalance = new SimpleStringProperty(); + final StringProperty lockedBalance = new SimpleStringProperty(); + private MonadicBinding btcInfoBinding; + + final StringProperty marketPrice = new SimpleStringProperty("N/A"); + + // P2P network + final StringProperty p2PNetworkInfo = new SimpleStringProperty(); + private MonadicBinding p2PNetworkInfoBinding; + final BooleanProperty splashP2PNetworkAnimationVisible = new SimpleBooleanProperty(true); + final StringProperty p2pNetworkWarnMsg = new SimpleStringProperty(); + final StringProperty p2PNetworkIconId = new SimpleStringProperty(); + final BooleanProperty bootstrapComplete = new SimpleBooleanProperty(); + + // software update + final String version = "v" + Version.VERSION; + + final BooleanProperty showAppScreen = new SimpleBooleanProperty(); + final StringProperty numPendingTradesAsString = new SimpleStringProperty(); + final BooleanProperty showPendingTradesNotification = new SimpleBooleanProperty(); + final StringProperty numOpenDisputesAsString = new SimpleStringProperty(); + final BooleanProperty showOpenDisputesNotification = new SimpleBooleanProperty(); + private final BooleanProperty isSplashScreenRemoved = new SimpleBooleanProperty(); + private final String btcNetworkAsString; + final StringProperty p2pNetworkLabelId = new SimpleStringProperty("footer-pane"); + + private MonadicBinding allServicesDone, tradesAndUIReady; + final PriceFeedService priceFeedService; + private final User user; + private int numBtcPeers = 0; + private Timer checkNumberOfBtcPeersTimer; + private Timer checkNumberOfP2pNetworkPeersTimer; + private final Map disputeIsClosedSubscriptionsMap = new HashMap<>(); + final ObservableList priceFeedComboBoxItems = FXCollections.observableArrayList(); + private MonadicBinding marketPriceBinding; + private Subscription priceFeedAllLoadedSubscription; + private Popup startupTimeoutPopup; + private BooleanProperty p2pNetWorkReady; + private final BooleanProperty walletInitialized = new SimpleBooleanProperty(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public MainViewModel(WalletService walletService, TradeWalletService tradeWalletService, + PriceFeedService priceFeedService, + ArbitratorManager arbitratorManager, P2PService p2PService, TradeManager tradeManager, + OpenOfferManager openOfferManager, DisputeManager disputeManager, Preferences preferences, + User user, AlertManager alertManager, PrivateNotificationManager privateNotificationManager, + FilterManager filterManager, WalletPasswordWindow walletPasswordWindow, AddBitcoinNodesWindow addBitcoinNodesWindow, + NotificationCenter notificationCenter, TacWindow tacWindow, Clock clock, + KeyRing keyRing, Navigation navigation, BSFormatter formatter) { + this.priceFeedService = priceFeedService; + this.user = user; + this.walletService = walletService; + this.tradeWalletService = tradeWalletService; + this.arbitratorManager = arbitratorManager; + this.p2PService = p2PService; + this.tradeManager = tradeManager; + this.openOfferManager = openOfferManager; + this.disputeManager = disputeManager; + this.preferences = preferences; + this.alertManager = alertManager; + this.privateNotificationManager = privateNotificationManager; + this.filterManager = filterManager; // Reference so it's initialized and eventlistener gets registered + this.walletPasswordWindow = walletPasswordWindow; + this.addBitcoinNodesWindow = addBitcoinNodesWindow; + this.notificationCenter = notificationCenter; + this.tacWindow = tacWindow; + this.clock = clock; + this.keyRing = keyRing; + this.navigation = navigation; + this.formatter = formatter; + + btcNetworkAsString = formatter.formatBitcoinNetwork(preferences.getBitcoinNetwork()) + + (preferences.getUseTorForBitcoinJ() ? " (using Tor)" : ""); + + TxIdTextField.setPreferences(preferences); + TxIdTextField.setWalletService(walletService); + BalanceTextField.setWalletService(walletService); + BalanceWithConfirmationTextField.setWalletService(walletService); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void start() { + // TODO need more though how to improve privacy without annoying first time users. + /* String key = "showAddBitcoinNodesWindowKey"; + if (preferences.showAgain(key)) + addBitcoinNodesWindow.dontShowAgainId(key, preferences) + .onClose(() -> { + preferences.dontShowAgain(key, true); + initializeAllServices(); + }) + .onAction(() -> { + preferences.dontShowAgain(key, true); + initializeAllServices(); + }) + .show(); + else + initializeAllServices(); + } + + private void initializeAllServices() {*/ + + Log.traceCall(); + + UserThread.runAfter(tacWindow::showIfNeeded, 2); + + + ChangeListener walletInitializedListener = (observable, oldValue, newValue) -> { + if (newValue && !p2pNetWorkReady.get()) + showStartupTimeoutPopup(); + }; + + Timer startupTimeout = UserThread.runAfter(() -> { + log.warn("startupTimeout called"); + Wallet wallet = walletService.getWallet(); + if (wallet != null && wallet.isEncrypted()) + walletInitialized.addListener(walletInitializedListener); + else + showStartupTimeoutPopup(); + }, 4, TimeUnit.MINUTES); + + p2pNetWorkReady = initP2PNetwork(); + initBitcoinWallet(); + + // need to store it to not get garbage collected + allServicesDone = EasyBind.combine(walletInitialized, p2pNetWorkReady, (a, b) -> a && b); + allServicesDone.subscribe((observable, oldValue, newValue) -> { + if (newValue) { + startupTimeout.stop(); + walletInitialized.removeListener(walletInitializedListener); + onAllServicesInitialized(); + if (startupTimeoutPopup != null) + startupTimeoutPopup.hide(); + } + }); + } + + private void showStartupTimeoutPopup() { + MainView.blur(); + String details; + if (!walletInitialized.get()) { + details = "You still did not get connected to the bitcoin network.\n" + + "If you use Tor for Bitcoin it might be that you got an unstable Tor path.\n" + + "You can wait longer or try to restart."; + } else if (!p2pNetWorkReady.get()) { + details = "You still did not get connected to the P2P network.\n" + + "That can happen sometimes when you got an unstable Tor path.\n" + + "You can wait longer or try to restart."; + } else { + log.error("Startup timeout with unknown problem."); + details = "There is an unknown problem at startup.\n" + + "Please restart and if the problem continues file a bug report."; + } + startupTimeoutPopup = new Popup(); + startupTimeoutPopup.warning("The application could not startup after 4 minutes.\n\n" + + details) + .actionButtonText("Shut down") + .onAction(BitsquareApp.shutDownHandler::run) + .show(); + } + + public void shutDown() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Initialisation + /////////////////////////////////////////////////////////////////////////////////////////// + + private BooleanProperty initP2PNetwork() { + Log.traceCall(); + StringProperty bootstrapState = new SimpleStringProperty(); + StringProperty bootstrapWarning = new SimpleStringProperty(); + BooleanProperty hiddenServicePublished = new SimpleBooleanProperty(); + BooleanProperty initialP2PNetworkDataReceived = new SimpleBooleanProperty(); + + p2PNetworkInfoBinding = EasyBind.combine(bootstrapState, bootstrapWarning, p2PService.getNumConnectedPeers(), hiddenServicePublished, initialP2PNetworkDataReceived, + (state, warning, numPeers, hiddenService, dataReceived) -> { + String result = ""; + int peers = (int) numPeers; + if (warning != null && peers == 0) { + result = warning; + } else { + if (dataReceived && hiddenService) + result = "P2P network peers: " + numPeers; + else if (peers == 0) + result = state; + else + result = state + " / P2P network peers: " + numPeers; + } + return result; + }); + p2PNetworkInfoBinding.subscribe((observable, oldValue, newValue) -> { + p2PNetworkInfo.set(newValue); + }); + + bootstrapState.set("Connecting to Tor network..."); + + p2PService.getNetworkNode().addConnectionListener(new ConnectionListener() { + @Override + public void onConnection(Connection connection) { + } + + @Override + public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) { + // We only check at seed nodes as they are running the latest version + // Other disconnects might be caused by peers running an older version + if (connection.getPeerType() == Connection.PeerType.SEED_NODE && + closeConnectionReason == CloseConnectionReason.RULE_VIOLATION) { + log.warn("RULE_VIOLATION onDisconnect closeConnectionReason=" + closeConnectionReason); + log.warn("RULE_VIOLATION onDisconnect connection=" + connection); + //TODO + /* new Popup() + .warning("You got disconnected from a seed node.\n\n" + + "Reason for getting disconnected: " + connection.getRuleViolation().name() + "\n\n" + + "It might be that your installed version is not compatible with " + + "the network.\n\n" + + "Please check if you run the latest software version.\n" + + "You can download the latest version of Bitsquare at:\n" + + "https://github.com/bitsquare/bitsquare/releases") + .show();*/ + } + } + + @Override + public void onError(Throwable throwable) { + } + }); + + final BooleanProperty p2pNetworkInitialized = new SimpleBooleanProperty(); + p2PService.start(new P2PServiceListener() { + @Override + public void onTorNodeReady() { + bootstrapState.set("Tor node created"); + p2PNetworkIconId.set("image-connection-tor"); + + if (preferences.getUseTorForBitcoinJ()) + initWalletService(); + } + + @Override + public void onHiddenServicePublished() { + hiddenServicePublished.set(true); + bootstrapState.set("Hidden Service published"); + } + + @Override + public void onRequestingDataCompleted() { + initialP2PNetworkDataReceived.set(true); + bootstrapState.set("Initial data received"); + splashP2PNetworkAnimationVisible.set(false); + p2pNetworkInitialized.set(true); + } + + @Override + public void onNoSeedNodeAvailable() { + if (p2PService.getNumConnectedPeers().get() == 0) + bootstrapWarning.set("No seed nodes available"); + else + bootstrapWarning.set(null); + + splashP2PNetworkAnimationVisible.set(false); + p2pNetworkInitialized.set(true); + } + + @Override + public void onNoPeersAvailable() { + if (p2PService.getNumConnectedPeers().get() == 0) { + p2pNetworkWarnMsg.set("There are no seed nodes or persisted peers available for requesting data.\n" + + "Please check your internet connection or try to restart the application."); + bootstrapWarning.set("No seed nodes and peers available"); + p2pNetworkLabelId.set("splash-error-state-msg"); + } else { + bootstrapWarning.set(null); + p2pNetworkLabelId.set("footer-pane"); + } + splashP2PNetworkAnimationVisible.set(false); + p2pNetworkInitialized.set(true); + } + + @Override + public void onBootstrapComplete() { + splashP2PNetworkAnimationVisible.set(false); + bootstrapComplete.set(true); + } + + @Override + public void onSetupFailed(Throwable throwable) { + p2pNetworkWarnMsg.set("Connecting to the P2P network failed (reported error: " + + throwable.getMessage() + ").\n" + + "Please check your internet connection or try to restart the application."); + splashP2PNetworkAnimationVisible.set(false); + bootstrapWarning.set("Bootstrapping to P2P network failed"); + p2pNetworkLabelId.set("splash-error-state-msg"); + } + }); + + return p2pNetworkInitialized; + } + + 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) -> { + String result = ""; + if (exception == null) { + double percentage = (double) downloadPercentage; + int peers = (int) numPeers; + String numPeersString = "Bitcoin network peers: " + peers; + + btcSyncProgress.set(percentage); + if (percentage == 1) { + result = numPeersString + " / synchronized with " + btcNetworkAsString; + btcSplashSyncIconId.set("image-connection-synced"); + } else if (percentage > 0.0) { + result = numPeersString + " / synchronizing with " + btcNetworkAsString + ": " + formatter.formatToPercentWithSymbol(percentage); + } else { + result = numPeersString + " / connecting to " + btcNetworkAsString; + } + } else { + result = "Bitcoin network peers: " + numBtcPeers + " / connecting to " + btcNetworkAsString + " failed"; + if (exception instanceof TimeoutException) { + walletServiceErrorMsg.set("Connecting to the bitcoin network failed because of a timeout."); + } else if (exception.getCause() instanceof BlockStoreException) { + log.error(exception.getMessage()); + // Ugly, but no other way to cover that specific case + if (exception.getMessage().equals("Store file is already locked by another process")) + new Popup().warning("Bitsquare is already running. You cannot run two instances of Bitsquare.") + .closeButtonText("Shut down") + .onClose(BitsquareApp.shutDownHandler::run) + .show(); + else + new Popup().error("Cannot open wallet because of an exception:\n" + exception.getMessage()) + .show(); + } else if (exception.getMessage() != null) { + walletServiceErrorMsg.set("Connection to the bitcoin network failed because of an error:" + exception.getMessage()); + } else { + walletServiceErrorMsg.set("Connection to the bitcoin network failed because of an error:" + exception.toString()); + } + } + return result; + + }); + btcInfoBinding.subscribe((observable, oldValue, newValue) -> { + btcInfo.set(newValue); + }); + + walletService.initialize(null, + () -> { + numBtcPeers = walletService.numPeersProperty().get(); + + if (walletService.getWallet().isEncrypted()) { + if (p2pNetWorkReady.get()) + splashP2PNetworkAnimationVisible.set(false); + + walletPasswordWindow + .onAesKey(aesKey -> { + walletService.setAesKey(aesKey); + tradeWalletService.setAesKey(aesKey); + walletInitialized.set(true); + }) + .hideCloseButton() + .show(); + } else { + walletInitialized.set(true); + } + }, + walletServiceException::set); + } + + private void onAllServicesInitialized() { + Log.traceCall(); + + clock.start(); + + // disputeManager + disputeManager.onAllServicesInitialized(); + disputeManager.getDisputesAsObservableList().addListener((ListChangeListener) change -> { + change.next(); + onDisputesChangeListener(change.getAddedSubList(), change.getRemoved()); + }); + onDisputesChangeListener(disputeManager.getDisputesAsObservableList(), null); + + // tradeManager + tradeManager.onAllServicesInitialized(); + tradeManager.getTrades().addListener((ListChangeListener) c -> updateBalance()); + tradeManager.getTrades().addListener((ListChangeListener) change -> onTradesChanged()); + onTradesChanged(); + // We handle the trade period here as we display a global popup if we reached dispute time + tradesAndUIReady = EasyBind.combine(isSplashScreenRemoved, tradeManager.pendingTradesInitializedProperty(), (a, b) -> a && b); + tradesAndUIReady.subscribe((observable, oldValue, newValue) -> { + if (newValue) + applyTradePeriodState(); + }); + + // walletService + walletService.addBalanceListener(new BalanceListener() { + @Override + public void onBalanceChanged(Coin balance, Transaction tx) { + updateBalance(); + } + }); + + openOfferManager.getOpenOffers().addListener((ListChangeListener) c -> updateBalance()); + tradeManager.getTrades().addListener((ListChangeListener) c -> updateBalance()); + openOfferManager.onAllServicesInitialized(); + arbitratorManager.onAllServicesInitialized(); + alertManager.alertMessageProperty().addListener((observable, oldValue, newValue) -> displayAlertIfPresent(newValue)); + privateNotificationManager.privateNotificationProperty().addListener((observable, oldValue, newValue) -> displayPrivateNotification(newValue)); + displayAlertIfPresent(alertManager.alertMessageProperty().get()); + + p2PService.onAllServicesInitialized(); + + setupBtcNumPeersWatcher(); + setupP2PNumPeersWatcher(); + updateBalance(); + if (DevFlags.DEV_MODE) { + preferences.setShowOwnOffersInOfferBook(true); + if (user.getPaymentAccounts().isEmpty()) + setupDevDummyPaymentAccounts(); + } + setupMarketPriceFeed(); + swapPendingOfferFundingEntries(); + fillPriceFeedComboBoxItems(); + + showAppScreen.set(true); + + + // We want to test if the client is compiled with the correct crypto provider (BountyCastle) + // and if the unlimited Strength for cryptographic keys is set. + // If users compile themselves they might miss that step and then would get an exception in the trade. + // To avoid that we add here at startup a sample encryption and signing to see if it don't causes an exception. + // See: https://github.com/bitsquare/bitsquare/blob/master/doc/build.md#7-enable-unlimited-strength-for-cryptographic-keys + Thread checkCryptoThread = new Thread() { + @Override + public void run() { + try { + Thread.currentThread().setName("checkCryptoThread"); + log.trace("Run crypto test"); + // just use any simple dummy msg + io.bitsquare.p2p.peers.keepalive.messages.Ping payload = new Ping(1, 1); + SealedAndSigned sealedAndSigned = Encryption.encryptHybridWithSignature(payload, + keyRing.getSignatureKeyPair(), keyRing.getPubKeyRing().getEncryptionPubKey()); + DecryptedDataTuple tuple = Encryption.decryptHybridWithSignature(sealedAndSigned, keyRing.getEncryptionKeyPair().getPrivate()); + if (tuple.payload instanceof Ping && + ((Ping) tuple.payload).nonce == payload.nonce && + ((Ping) tuple.payload).lastRoundTripTime == payload.lastRoundTripTime) + log.debug("Crypto test succeeded"); + else + throw new CryptoException("Payload not correct after decryption"); + } catch (CryptoException e) { + e.printStackTrace(); + String msg = "Seems that you use a self compiled binary and have not following the build " + + "instructions in https://github.com/bitsquare/bitsquare/blob/master/doc/build.md#7-enable-unlimited-strength-for-cryptographic-keys.\n\n" + + "If that is not the case and you use the official Bitsquare binary, " + + "please file a bug report to the Github page.\n" + + "Error=" + e.getMessage(); + log.error(msg); + UserThread.execute(() -> new Popup<>().warning(msg) + .actionButtonText("Shut down") + .onAction(BitsquareApp.shutDownHandler::run) + .closeButtonText("Report bug at Github issues") + .onClose(() -> GUIUtil.openWebPage("https://github.com/bitsquare/bitsquare/issues")) + .show()); + } + } + }; + checkCryptoThread.start(); + + if (Security.getProvider("BC") == null) { + new Popup<>().warning("There is a problem with the crypto libraries. BountyCastle is not available.") + .actionButtonText("Shut down") + .onAction(BitsquareApp.shutDownHandler::run) + .closeButtonText("Report bug at Github issues") + .onClose(() -> GUIUtil.openWebPage("https://github.com/bitsquare/bitsquare/issues")) + .show(); + } + + String remindPasswordAndBackupKey = "remindPasswordAndBackup"; + user.getPaymentAccountsAsObservable().addListener((SetChangeListener) change -> { + if (!walletService.getWallet().isEncrypted() && preferences.showAgain(remindPasswordAndBackupKey) && change.wasAdded()) { + new Popup<>().headLine("Important security recommendation") + .information("We would like to remind you to consider using password protection for your wallet if you have not already enabled that.\n\n" + + "It is also highly recommended to write down the wallet seed words. Those seed words are like a master password for recovering your Bitcoin wallet.\n" + + "At the \"Wallet Seed\" section you find more information.\n\n" + + "Additionally you can backup the complete application data folder at the \"Backup\" section.\n" + + "Please note, that this backup is not encrypted!") + .dontShowAgainId(remindPasswordAndBackupKey, preferences) + .show(); + } + }); + + checkIfOpenOffersMatchTradeProtocolVersion(); + } + + private void checkIfOpenOffersMatchTradeProtocolVersion() { + List outDatedOffers = openOfferManager.getOpenOffers() + .stream() + .filter(e -> e.getOffer().getProtocolVersion() != Version.TRADE_PROTOCOL_VERSION) + .collect(Collectors.toList()); + if (!outDatedOffers.isEmpty()) { + new Popup<>() + .warning("You have open offers which have been created with an older version of Bitsquare.\n" + + "Please remove those offers as they are not valid anymore.\n\n" + + "Offers (ID): " + + outDatedOffers.stream() + .map(e -> e.getId() + "\n") + .collect(Collectors.toList()).toString() + .replace("[", "").replace("]", "")) + .actionButtonText("Remove outdated offer(s)") + .onAction(() -> openOfferManager.removeOpenOffers(outDatedOffers, null)) + .closeButtonText("Shut down") + .onClose(BitsquareApp.shutDownHandler::run) + .show(); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // UI handlers + /////////////////////////////////////////////////////////////////////////////////////////// + + // After showAppScreen is set and splash screen is faded out + void onSplashScreenRemoved() { + isSplashScreenRemoved.set(true); + + // Delay that as we want to know what is the current path of the navigation which is set + // in MainView showAppScreen handler + notificationCenter.onAllServicesAndViewsInitialized(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // States + /////////////////////////////////////////////////////////////////////////////////////////// + + private void applyTradePeriodState() { + updateTradePeriodState(); + clock.addListener(new Clock.Listener() { + @Override + public void onSecondTick() { + } + + @Override + public void onMinuteTick() { + updateTradePeriodState(); + } + + @Override + public void onMissedSecondTick(long missed) { + } + }); + } + + private void updateTradePeriodState() { + tradeManager.getTrades().stream().forEach(trade -> { + if (trade.getState().getPhase().ordinal() < Trade.Phase.PAYOUT_PAID.ordinal()) { + Date maxTradePeriodDate = trade.getMaxTradePeriodDate(); + Date halfTradePeriodDate = trade.getHalfTradePeriodDate(); + if (maxTradePeriodDate != null && halfTradePeriodDate != null) { + Date now = new Date(); + if (now.after(maxTradePeriodDate)) + trade.setTradePeriodState(Trade.TradePeriodState.TRADE_PERIOD_OVER); + else if (now.after(halfTradePeriodDate)) + trade.setTradePeriodState(Trade.TradePeriodState.HALF_REACHED); + + String key; + switch (trade.getTradePeriodState()) { + case NORMAL: + break; + case HALF_REACHED: + key = "displayHalfTradePeriodOver" + trade.getId(); + if (preferences.showAgain(key)) { + preferences.dontShowAgain(key, true); + new Popup().warning("Your trade with ID " + trade.getShortId() + + " has reached the half of the max. allowed trading period and " + + "is still not completed.\n\n" + + "The trade period ends on " + formatter.formatDateTime(maxTradePeriodDate) + "\n\n" + + "Please check your trade state at \"Portfolio/Open trades\" for further information.") + .show(); + } + break; + case TRADE_PERIOD_OVER: + key = "displayTradePeriodOver" + trade.getId(); + if (preferences.showAgain(key)) { + preferences.dontShowAgain(key, true); + new Popup().warning("Your trade with ID " + trade.getShortId() + + " has reached the max. allowed trading period and is " + + "not completed.\n\n" + + "The trade period ended on " + formatter.formatDateTime(maxTradePeriodDate) + "\n\n" + + "Please check your trade at \"Portfolio/Open trades\" for contacting " + + "the arbitrator.") + .show(); + } + break; + } + } + } + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void setupP2PNumPeersWatcher() { + p2PService.getNumConnectedPeers().addListener((observable, oldValue, newValue) -> { + int numPeers = (int) newValue; + if ((int) oldValue > 0 && numPeers == 0) { + // give a bit of tolerance + if (checkNumberOfP2pNetworkPeersTimer != null) + checkNumberOfP2pNetworkPeersTimer.stop(); + + checkNumberOfP2pNetworkPeersTimer = UserThread.runAfter(() -> { + // check again numPeers + if (p2PService.getNumConnectedPeers().get() == 0) { + p2pNetworkWarnMsg.set("You lost the connection to all P2P network peers.\n" + + "Maybe you lost your internet connection or your computer was in standby mode."); + p2pNetworkLabelId.set("splash-error-state-msg"); + } else { + p2pNetworkWarnMsg.set(null); + p2pNetworkLabelId.set("footer-pane"); + } + }, 5); + } else if ((int) oldValue == 0 && numPeers > 0) { + if (checkNumberOfP2pNetworkPeersTimer != null) + checkNumberOfP2pNetworkPeersTimer.stop(); + + p2pNetworkWarnMsg.set(null); + p2pNetworkLabelId.set("footer-pane"); + } + }); + } + + private void setupBtcNumPeersWatcher() { + walletService.numPeersProperty().addListener((observable, oldValue, newValue) -> { + int numPeers = (int) newValue; + if ((int) oldValue > 0 && numPeers == 0) { + if (checkNumberOfBtcPeersTimer != null) + checkNumberOfBtcPeersTimer.stop(); + + checkNumberOfBtcPeersTimer = UserThread.runAfter(() -> { + // check again numPeers + if (walletService.numPeersProperty().get() == 0) { + walletServiceErrorMsg.set("You lost the connection to all bitcoin network peers.\n" + + "Maybe you lost your internet connection or your computer was in standby mode."); + } else { + walletServiceErrorMsg.set(null); + } + }, 5); + } else if ((int) oldValue == 0 && numPeers > 0) { + if (checkNumberOfBtcPeersTimer != null) + checkNumberOfBtcPeersTimer.stop(); + walletServiceErrorMsg.set(null); + } + }); + } + + private void setupMarketPriceFeed() { + 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, priceFeedService.getCurrencyCode())), + (errorMessage, throwable) -> marketPrice.set("N/A")); + marketPriceCurrencyCode.bind(priceFeedService.currencyCodeProperty()); + typeProperty.bind(priceFeedService.typeProperty()); + + marketPriceBinding = EasyBind.combine( + marketPriceCurrencyCode, marketPrice, + (currencyCode, price) -> formatter.getCurrencyPair(currencyCode) + ": " + price); + + marketPriceBinding.subscribe((observable, oldValue, newValue) -> { + if (newValue != null && !newValue.equals(oldValue)) { + setMarketPriceInItems(); + + String code = preferences.getUseStickyMarketPrice() ? + preferences.getPreferredTradeCurrency().getCode() : + priceFeedService.currencyCodeProperty().get(); + Optional itemOptional = findPriceFeedComboBoxItem(code); + if (itemOptional.isPresent()) { + if (selectedPriceFeedComboBoxItemProperty.get() == null || !preferences.getUseStickyMarketPrice()) { + itemOptional.get().setDisplayString(newValue); + selectedPriceFeedComboBoxItemProperty.set(itemOptional.get()); + } + } else { + if (CurrencyUtil.isCryptoCurrency(code)) { + CurrencyUtil.getCryptoCurrency(code).ifPresent(cryptoCurrency -> { + preferences.addCryptoCurrency(cryptoCurrency); + fillPriceFeedComboBoxItems(); + }); + } else { + CurrencyUtil.getFiatCurrency(code).ifPresent(fiatCurrency -> { + preferences.addFiatCurrency(fiatCurrency); + fillPriceFeedComboBoxItems(); + }); + } + } + + if (selectedPriceFeedComboBoxItemProperty.get() != null) + selectedPriceFeedComboBoxItemProperty.get().setDisplayString(newValue); + } + }); + + priceFeedAllLoadedSubscription = EasyBind.subscribe(priceFeedService.currenciesUpdateFlagProperty(), newPriceUpdate -> setMarketPriceInItems()); + + preferences.getTradeCurrenciesAsObservable().addListener((ListChangeListener) c -> { + UserThread.runAfter(() -> { + fillPriceFeedComboBoxItems(); + setMarketPriceInItems(); + }, 100, TimeUnit.MILLISECONDS); + }); + } + + private void setMarketPriceInItems() { + priceFeedComboBoxItems.stream().forEach(item -> { + String currencyCode = item.currencyCode; + MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); + String priceString; + if (marketPrice != null) { + double price = marketPrice.getPrice(priceFeedService.getType()); + if (price != 0) { + priceString = formatter.formatMarketPrice(price, currencyCode); + item.setIsPriceAvailable(true); + } else { + priceString = "N/A"; + item.setIsPriceAvailable(false); + } + } else { + priceString = "N/A"; + item.setIsPriceAvailable(false); + } + item.setDisplayString(formatter.getCurrencyPair(currencyCode) + ": " + priceString); + }); + } + + public void setPriceFeedComboBoxItem(PriceFeedComboBoxItem item) { + if (!preferences.getUseStickyMarketPrice() && item != null) { + Optional itemOptional = findPriceFeedComboBoxItem(priceFeedService.currencyCodeProperty().get()); + if (itemOptional.isPresent()) + selectedPriceFeedComboBoxItemProperty.set(itemOptional.get()); + else + findPriceFeedComboBoxItem(preferences.getPreferredTradeCurrency().getCode()) + .ifPresent(item2 -> selectedPriceFeedComboBoxItemProperty.set(item2)); + + priceFeedService.setCurrencyCode(item.currencyCode); + } else if (item != null) { + selectedPriceFeedComboBoxItemProperty.set(item); + priceFeedService.setCurrencyCode(item.currencyCode); + } else { + findPriceFeedComboBoxItem(preferences.getPreferredTradeCurrency().getCode()) + .ifPresent(item2 -> selectedPriceFeedComboBoxItemProperty.set(item2)); + } + + // Need a delay a bit as we get item.isPriceAvailable() set after that call. + // (In case we add a new currency in settings) + UserThread.runAfter(() -> { + if (item != null) { + String code = item.currencyCode; + isFiatCurrencyPriceFeedSelected.set(CurrencyUtil.isFiatCurrency(code) && CurrencyUtil.getFiatCurrency(code).isPresent() && item.isPriceAvailable()); + isCryptoCurrencyPriceFeedSelected.set(CurrencyUtil.isCryptoCurrency(code) && CurrencyUtil.getCryptoCurrency(code).isPresent() && item.isPriceAvailable()); + } + }, 100, TimeUnit.MILLISECONDS); + } + + Optional findPriceFeedComboBoxItem(String currencyCode) { + return priceFeedComboBoxItems.stream() + .filter(item -> item.currencyCode.equals(currencyCode)) + .findAny(); + } + + private void fillPriceFeedComboBoxItems() { + List currencyItems = preferences.getTradeCurrenciesAsObservable() + .stream() + .map(tradeCurrency -> new PriceFeedComboBoxItem(tradeCurrency.getCode())) + .collect(Collectors.toList()); + priceFeedComboBoxItems.setAll(currencyItems); + } + + private void displayAlertIfPresent(Alert alert) { + boolean alreadyDisplayed = alert != null && alert.equals(user.getDisplayedAlert()); + user.setDisplayedAlert(alert); + if (alert != null && !alreadyDisplayed) + if (alert.isUpdateInfo || alert.isNewVersion()) + new DisplayUpdateDownloadWindow().alertMessage(alert).show(); + else + new DisplayAlertMessageWindow().alertMessage(alert).show(); + } + + private void displayPrivateNotification(PrivateNotification privateNotification) { + new Popup<>().headLine("Important private notification!") + .attention(privateNotification.message) + .setHeadlineStyle("-fx-text-fill: -bs-error-red; -fx-font-weight: bold; -fx-font-size: 16;") + .onClose(() -> privateNotificationManager.removePrivateNotification()) + .closeButtonText("I understand") + .show(); + } + + private void swapPendingOfferFundingEntries() { + tradeManager.getAddressEntriesForAvailableBalanceStream() + .filter(addressEntry -> addressEntry.getOfferId() != null) + .forEach(addressEntry -> walletService.swapTradeEntryToAvailableEntry(addressEntry.getOfferId(), AddressEntry.Context.OFFER_FUNDING)); + } + + private void updateBalance() { + // Without delaying to the next cycle it does not update. + // Seems order of events we are listening on causes that... + UserThread.execute(() -> { + updateAvailableBalance(); + updateReservedBalance(); + updateLockedBalance(); + }); + } + + private void updateAvailableBalance() { + Coin totalAvailableBalance = Coin.valueOf(tradeManager.getAddressEntriesForAvailableBalanceStream() + .mapToLong(addressEntry -> walletService.getBalanceForAddress(addressEntry.getAddress()).getValue()) + .sum()); + availableBalance.set(formatter.formatCoinWithCode(totalAvailableBalance)); + } + + private void updateReservedBalance() { + Coin sum = Coin.valueOf(openOfferManager.getOpenOffers().stream() + .map(openOffer -> { + Address address = walletService.getOrCreateAddressEntry(openOffer.getId(), AddressEntry.Context.RESERVED_FOR_TRADE).getAddress(); + return walletService.getBalanceForAddress(address); + }) + .mapToLong(Coin::getValue) + .sum()); + + reservedBalance.set(formatter.formatCoinWithCode(sum)); + } + + private void updateLockedBalance() { + Coin sum = Coin.valueOf(tradeManager.getLockedTradeStream() + .mapToLong(trade -> { + Coin lockedTradeAmount = walletService.getOrCreateAddressEntry(trade.getId(), AddressEntry.Context.MULTI_SIG).getCoinLockedInMultiSig(); + return lockedTradeAmount != null ? lockedTradeAmount.getValue() : 0; + }) + .sum()); + lockedBalance.set(formatter.formatCoinWithCode(sum)); + } + + private void onDisputesChangeListener(List addedList, @Nullable List removedList) { + if (removedList != null) { + removedList.stream().forEach(dispute -> { + String id = dispute.getId(); + if (disputeIsClosedSubscriptionsMap.containsKey(id)) { + disputeIsClosedSubscriptionsMap.get(id).unsubscribe(); + disputeIsClosedSubscriptionsMap.remove(id); + } + }); + } + addedList.stream().forEach(dispute -> { + String id = dispute.getId(); + Subscription disputeStateSubscription = EasyBind.subscribe(dispute.isClosedProperty(), + isClosed -> { + // We get event before list gets updated, so we execute on next frame + UserThread.execute(() -> { + int openDisputes = disputeManager.getDisputesAsObservableList().stream() + .filter(e -> !e.isClosed()) + .collect(Collectors.toList()).size(); + if (openDisputes > 0) + numOpenDisputesAsString.set(String.valueOf(openDisputes)); + if (openDisputes > 9) + numOpenDisputesAsString.set("★"); + + showOpenDisputesNotification.set(openDisputes > 0); + }); + }); + disputeIsClosedSubscriptionsMap.put(id, disputeStateSubscription); + }); + } + + private void onTradesChanged() { + long numPendingTrades = tradeManager.getTrades().size(); + if (numPendingTrades > 0) + numPendingTradesAsString.set(String.valueOf(numPendingTrades)); + if (numPendingTrades > 9) + numPendingTradesAsString.set("★"); + + showPendingTradesNotification.set(numPendingTrades > 0); + } + + private void setupDevDummyPaymentAccounts() { + OKPayAccount okPayAccount = new OKPayAccount(); + okPayAccount.setAccountNr("dummy_" + new Random().nextInt(100)); + okPayAccount.setAccountName("OKPay dummy"); + okPayAccount.setSelectedTradeCurrency(CurrencyUtil.getDefaultTradeCurrency()); + user.addPaymentAccount(okPayAccount); + + CryptoCurrencyAccount cryptoCurrencyAccount = new CryptoCurrencyAccount(); + cryptoCurrencyAccount.setAccountName("ETH dummy"); + cryptoCurrencyAccount.setAddress("0x" + new Random().nextInt(1000000)); + cryptoCurrencyAccount.setSingleTradeCurrency(CurrencyUtil.getCryptoCurrency("ETH").get()); + user.addPaymentAccount(cryptoCurrencyAccount); + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/main/overlays/windows/DisplayUpdateDownloadWindow.java b/gui/src/main/java/io/bitsquare/gui/main/overlays/windows/DisplayUpdateDownloadWindow.java new file mode 100644 index 0000000000..32990d4660 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/overlays/windows/DisplayUpdateDownloadWindow.java @@ -0,0 +1,131 @@ +/* + * 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.overlays.windows; + +import io.bitsquare.alert.Alert; +import io.bitsquare.common.util.Utilities; +import io.bitsquare.gui.components.HyperlinkWithIcon; +import io.bitsquare.gui.main.overlays.Overlay; +import javafx.geometry.Insets; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.layout.GridPane; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.bitsquare.gui.util.FormBuilder.addButton; +import static io.bitsquare.gui.util.FormBuilder.addLabelHyperlinkWithIcon; +import static io.bitsquare.gui.util.FormBuilder.addMultilineLabel; + +public class DisplayUpdateDownloadWindow extends Overlay { + private static final Logger log = LoggerFactory.getLogger(DisplayUpdateDownloadWindow.class); + private Alert alert; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public API + /////////////////////////////////////////////////////////////////////////////////////////// + + public DisplayUpdateDownloadWindow() { + type = Type.Attention; + } + + public void show() { + width = 700; + // need to set headLine, otherwise the fields will not be created in addHeadLine + headLine = "Update available!"; + createGridPane(); + addHeadLine(); + addSeparator(); + addContent(); + applyStyles(); + display(); + } + + public DisplayUpdateDownloadWindow alertMessage(Alert alert) { + this.alert = alert; + return this; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + private void addContent() { + checkNotNull(alert, "alertMessage must not be null"); + Label messageLabel = addMultilineLabel(gridPane, ++rowIndex, alert.message, 10); + headLine = "Important update information!"; + headLineLabel.setStyle("-fx-text-fill: -fx-accent; -fx-font-weight: bold; -fx-font-size: 22;"); + + + + switch (Utilities.get) + ProgressIndicator indicator = new ProgressIndicator(0L); + indicator.setVisible(false); + GridPane.setRowIndex(indicator, ++rowIndex); + GridPane.setColumnIndex(indicator, 1); + gridPane.getChildren().add(indicator); + + Button downloadButton = addButton(gridPane, ++rowIndex, "Download now"); + + // TODO How do we get the right URL for the download? + String url = "https://bitsquare.io/downloads" + File.separator + alert.version + File.separator; + String fileName; + if (Utilities.isOSX()) + fileName = "Bitsquare-" + alert.version + ".dmg"; + else if (Utilities.isWindows()) + fileName = "Bitsquare-" + Utilities.getOSArchitecture() + "bit-" + alert.version + ".exe"; + else if (Utilities.isLinux()) + fileName = "Bitsquare-" + Utilities.getOSArchitecture() + "bit-" + alert.version + ".deb"; + else { + downloadButton.setDisable(true); + messageLabel.setText("Unable to determine the correct installer. Pleaase manually download and verify " + + "the correct file from https://bitsquare.io/downloads"); + } + + downloadButton.setOnAction(e -> { + indicator.setVisible(true); + try { + Utilities.downloadFile(url, null, indicator); + } catch (IOException e) { + messageLabel.setText("Unable to download files.\n" + + "Please manually download and verify the file from https://bitsquare.io/downloads"); + downloadButton.setDisable(true); + + } + }); + + closeButton = new Button("Close"); + closeButton.setOnAction(e -> { + hide(); + closeHandlerOptional.ifPresent(closeHandler -> closeHandler.run()); + }); + + GridPane.setRowIndex(closeButton, ++rowIndex); + GridPane.setColumnIndex(closeButton, 1); + gridPane.getChildren().add(closeButton); + GridPane.setMargin(closeButton, new Insets(10, 0, 0, 0)); + } + + +}