diff --git a/bootstrap/src/main/java/io/bitsquare/p2p/seed/SeedNodeMain.java b/bootstrap/src/main/java/io/bitsquare/p2p/seed/SeedNodeMain.java new file mode 100644 index 0000000000..8d722278ea --- /dev/null +++ b/bootstrap/src/main/java/io/bitsquare/p2p/seed/SeedNodeMain.java @@ -0,0 +1,16 @@ +package io.bitsquare.p2p.seed; + +import java.security.NoSuchAlgorithmException; + +public class SeedNodeMain { + + // args: port useLocalhost seedNodes + // eg. 4444 true localhost:7777 localhost:8888 + // To stop enter: q + public static void main(String[] args) throws NoSuchAlgorithmException { + SeedNode seedNode = new SeedNode(); + seedNode.processArgs(args); + seedNode.createAndStartP2PService(); + seedNode.listenForExitCommand(); + } +} diff --git a/common/pom.xml b/common/pom.xml new file mode 100644 index 0000000000..82a4d7df32 --- /dev/null +++ b/common/pom.xml @@ -0,0 +1,28 @@ + + + + parent + io.bitsquare + 0.3.2-SNAPSHOT + + 4.0.0 + + common + + + + com.google.code.gson + gson + 2.2.4 + + + + org.springframework + spring-core + 4.1.1.RELEASE + + + + \ No newline at end of file diff --git a/common/src/main/java/io/bitsquare/app/AppModule.java b/common/src/main/java/io/bitsquare/app/AppModule.java new file mode 100644 index 0000000000..9965d80bce --- /dev/null +++ b/common/src/main/java/io/bitsquare/app/AppModule.java @@ -0,0 +1,65 @@ +/* + * 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.app; + +import com.google.common.base.Preconditions; +import com.google.inject.AbstractModule; +import com.google.inject.Injector; +import org.springframework.core.env.Environment; + +import java.util.ArrayList; +import java.util.List; + +public abstract class AppModule extends AbstractModule { + protected final Environment env; + + private final List modules = new ArrayList<>(); + + protected AppModule(Environment env) { + Preconditions.checkNotNull(env, "Environment must not be null"); + this.env = env; + } + + protected void install(AppModule module) { + super.install(module); + modules.add(module); + } + + /** + * Close any instances this module is responsible for and recursively close any + * sub-modules installed via {@link #install(AppModule)}. This method + * must be called manually, e.g. at the end of a main() method or in the stop() method + * of a JavaFX Application; alternatively it may be registered as a JVM shutdown hook. + * + * @param injector the Injector originally initialized with this module + * @see #doClose(com.google.inject.Injector) + */ + public final void close(Injector injector) { + modules.forEach(module -> module.close(injector)); + doClose(injector); + } + + /** + * Actually perform closing of any instances this module is responsible for. Called by + * {@link #close(Injector)}. + * + * @param injector the Injector originally initialized with this module + */ + protected void doClose(Injector injector) { + } +} diff --git a/common/src/main/java/io/bitsquare/app/ProgramArguments.java b/common/src/main/java/io/bitsquare/app/ProgramArguments.java new file mode 100644 index 0000000000..c2a12dd6b0 --- /dev/null +++ b/common/src/main/java/io/bitsquare/app/ProgramArguments.java @@ -0,0 +1,19 @@ +package io.bitsquare.app; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +// TODO too app specific for common... +public class ProgramArguments { + // program arg names + public static final String TOR_DIR = "torDir"; + public static final String USE_LOCALHOST = "useLocalhost"; + public static final String DEV_TEST = "devTest"; + + + public static final String NAME_KEY = "node.name"; + public static final String PORT_KEY = "node.port"; + + + private static final Logger log = LoggerFactory.getLogger(ProgramArguments.class); +} diff --git a/common/src/main/java/io/bitsquare/app/Version.java b/common/src/main/java/io/bitsquare/app/Version.java new file mode 100644 index 0000000000..1ad878648e --- /dev/null +++ b/common/src/main/java/io/bitsquare/app/Version.java @@ -0,0 +1,45 @@ +/* + * 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.app; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Version { + private static final Logger log = LoggerFactory.getLogger(Version.class); + + // The application versions + private static final int MAJOR_VERSION = 0; + private static final int MINOR_VERSION = 3; + // used as updateFX index + public static final int PATCH_VERSION = 2; + + public static final String VERSION = MAJOR_VERSION + "." + MINOR_VERSION + "." + PATCH_VERSION; + + // 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. + public static final long NETWORK_PROTOCOL_VERSION = 1; + + // The version nr. of the serialized data stored to disc. A change will break the serialization of old objects. + public static final long LOCAL_DB_VERSION = 1; + + // The version nr. of the current protocol. The offer holds that version. A taker will check the version of the offers to see if he his version is + // compatible. + public static final long PROTOCOL_VERSION = 1; + +} diff --git a/common/src/main/java/io/bitsquare/common/ByteArrayUtils.java b/common/src/main/java/io/bitsquare/common/ByteArrayUtils.java new file mode 100644 index 0000000000..d9478ede8d --- /dev/null +++ b/common/src/main/java/io/bitsquare/common/ByteArrayUtils.java @@ -0,0 +1,81 @@ +/* + * 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; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; + +public class ByteArrayUtils { + private static final Logger log = LoggerFactory.getLogger(ByteArrayUtils.class); + private static long lastTimeStamp = System.currentTimeMillis(); + + public static T byteArrayToObject(byte[] data) { + ByteArrayInputStream bis = new ByteArrayInputStream(data); + ObjectInput in = null; + Object result = null; + try { + in = new ObjectInputStream(bis); + result = in.readObject(); + } 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[] objectToByteArray(Object object) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutput out = null; + byte[] result = null; + try { + out = new ObjectOutputStream(bos); + out.writeObject(object); + result = bos.toByteArray(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (out != null) { + out.close(); + } + } catch (IOException ex) { + // ignore close exception + } + try { + bos.close(); + } catch (IOException ex) { + // ignore close exception + } + } + return result; + } +} diff --git a/common/src/main/java/io/bitsquare/common/UserThread.java b/common/src/main/java/io/bitsquare/common/UserThread.java new file mode 100644 index 0000000000..bce70d270b --- /dev/null +++ b/common/src/main/java/io/bitsquare/common/UserThread.java @@ -0,0 +1,38 @@ +/* + * 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; + +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +public class UserThread { + + public static Executor getExecutor() { + return executor; + } + + public static void setExecutor(Executor executor) { + UserThread.executor = executor; + } + + public static Executor executor = Executors.newSingleThreadExecutor(); + + public static void execute(Runnable command) { + UserThread.executor.execute(command); + } +} diff --git a/common/src/main/java/io/bitsquare/common/handlers/ErrorMessageHandler.java b/common/src/main/java/io/bitsquare/common/handlers/ErrorMessageHandler.java new file mode 100644 index 0000000000..b9972f2913 --- /dev/null +++ b/common/src/main/java/io/bitsquare/common/handlers/ErrorMessageHandler.java @@ -0,0 +1,25 @@ +/* + * 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.handlers; + +/** + * For reporting error message only (UI) + */ +public interface ErrorMessageHandler { + void handleErrorMessage(String errorMessage); +} diff --git a/common/src/main/java/io/bitsquare/common/handlers/ExceptionHandler.java b/common/src/main/java/io/bitsquare/common/handlers/ExceptionHandler.java new file mode 100644 index 0000000000..c1122c1236 --- /dev/null +++ b/common/src/main/java/io/bitsquare/common/handlers/ExceptionHandler.java @@ -0,0 +1,25 @@ +/* + * 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.handlers; + +/** + * For reporting throwable objects only + */ +public interface ExceptionHandler { + void handleException(Throwable throwable); +} diff --git a/common/src/main/java/io/bitsquare/common/handlers/FaultHandler.java b/common/src/main/java/io/bitsquare/common/handlers/FaultHandler.java new file mode 100644 index 0000000000..3d2913dfa5 --- /dev/null +++ b/common/src/main/java/io/bitsquare/common/handlers/FaultHandler.java @@ -0,0 +1,25 @@ +/* + * 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.handlers; + +/** + * For reporting a description message and throwable + */ +public interface FaultHandler { + void handleFault(String errorMessage, Throwable throwable); +} diff --git a/common/src/main/java/io/bitsquare/common/handlers/ResultHandler.java b/common/src/main/java/io/bitsquare/common/handlers/ResultHandler.java new file mode 100644 index 0000000000..60ac05be77 --- /dev/null +++ b/common/src/main/java/io/bitsquare/common/handlers/ResultHandler.java @@ -0,0 +1,22 @@ +/* + * 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.handlers; + +public interface ResultHandler extends Runnable { + void handleResult(); +} diff --git a/common/src/main/java/io/bitsquare/common/taskrunner/InterceptTaskException.java b/common/src/main/java/io/bitsquare/common/taskrunner/InterceptTaskException.java new file mode 100644 index 0000000000..f3f19af231 --- /dev/null +++ b/common/src/main/java/io/bitsquare/common/taskrunner/InterceptTaskException.java @@ -0,0 +1,30 @@ +/* + * 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.taskrunner; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class InterceptTaskException extends RuntimeException { + private static final Logger log = LoggerFactory.getLogger(InterceptTaskException.class); + private static final long serialVersionUID = 5216202440370333534L; + + public InterceptTaskException(String message) { + super(message); + } +} diff --git a/common/src/main/java/io/bitsquare/common/taskrunner/Model.java b/common/src/main/java/io/bitsquare/common/taskrunner/Model.java new file mode 100644 index 0000000000..5c10f6a735 --- /dev/null +++ b/common/src/main/java/io/bitsquare/common/taskrunner/Model.java @@ -0,0 +1,24 @@ +/* + * 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.taskrunner; + +public interface Model { + void persist(); + + void onComplete(); +} diff --git a/common/src/main/java/io/bitsquare/common/taskrunner/Task.java b/common/src/main/java/io/bitsquare/common/taskrunner/Task.java new file mode 100644 index 0000000000..280ff0b940 --- /dev/null +++ b/common/src/main/java/io/bitsquare/common/taskrunner/Task.java @@ -0,0 +1,74 @@ +/* + * 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.taskrunner; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class Task { + private static final Logger log = LoggerFactory.getLogger(Task.class); + + public static Class taskToIntercept; + + private final TaskRunner taskHandler; + protected final T model; + protected String errorMessage = "An error occurred at task: " + getClass().getSimpleName(); + + public Task(TaskRunner taskHandler, T model) { + this.taskHandler = taskHandler; + this.model = model; + } + + abstract protected void run(); + + protected void runInterceptHook() { + if (getClass() == taskToIntercept) + throw new InterceptTaskException("Task intercepted for testing purpose. Task = " + getClass().getSimpleName()); + } + + protected void appendToErrorMessage(String message) { + errorMessage += "\n" + message; + } + + protected void appendExceptionToErrorMessage(Throwable t) { + if (t.getMessage() != null) + errorMessage += "\nException message: " + t.getMessage(); + else + errorMessage += "\nException: " + t.toString(); + } + + protected void complete() { + taskHandler.handleComplete(); + } + + protected void failed(String message) { + appendToErrorMessage(message); + failed(); + } + + protected void failed(Throwable t) { + t.printStackTrace(); + appendExceptionToErrorMessage(t); + failed(); + } + + protected void failed() { + taskHandler.handleErrorMessage(errorMessage); + } + +} diff --git a/common/src/main/java/io/bitsquare/common/taskrunner/TaskRunner.java b/common/src/main/java/io/bitsquare/common/taskrunner/TaskRunner.java new file mode 100644 index 0000000000..cfba7dd077 --- /dev/null +++ b/common/src/main/java/io/bitsquare/common/taskrunner/TaskRunner.java @@ -0,0 +1,95 @@ +/* + * 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.taskrunner; + +import io.bitsquare.common.handlers.ErrorMessageHandler; +import io.bitsquare.common.handlers.ResultHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.Queue; +import java.util.concurrent.LinkedBlockingQueue; + +public class TaskRunner { + private static final Logger log = LoggerFactory.getLogger(TaskRunner.class); + + private final Queue> tasks = new LinkedBlockingQueue<>(); + private final T sharedModel; + private final Class sharedModelClass; + private final ResultHandler resultHandler; + private final ErrorMessageHandler errorMessageHandler; + private boolean failed = false; + private boolean isCanceled; + + private Class currentTask; + + + public TaskRunner(T sharedModel, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + this(sharedModel, (Class) sharedModel.getClass(), resultHandler, errorMessageHandler); + } + + public TaskRunner(T sharedModel, Class sharedModelClass, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + this.sharedModel = sharedModel; + this.resultHandler = resultHandler; + this.errorMessageHandler = errorMessageHandler; + this.sharedModelClass = sharedModelClass; + } + + public final void addTasks(Class>... items) { + tasks.addAll(Arrays.asList(items)); + } + + public void run() { + next(); + } + + private void next() { + if (!failed && !isCanceled) { + if (tasks.size() > 0) { + try { + currentTask = tasks.poll(); + log.trace("Run task: " + currentTask.getSimpleName()); + currentTask.getDeclaredConstructor(TaskRunner.class, sharedModelClass).newInstance(this, sharedModel).run(); + } catch (Throwable throwable) { + throwable.printStackTrace(); + handleErrorMessage("Error at taskRunner: " + throwable.getMessage()); + } + } else { + resultHandler.handleResult(); + } + } + } + + public void cancel() { + isCanceled = true; + } + + void handleComplete() { + log.trace("Task completed: " + currentTask.getSimpleName()); + sharedModel.persist(); + next(); + } + + void handleErrorMessage(String errorMessage) { + log.error("Task failed: " + currentTask.getSimpleName()); + log.error("errorMessage: " + errorMessage); + failed = true; + errorMessageHandler.handleErrorMessage(errorMessage); + } +} diff --git a/common/src/main/java/io/bitsquare/common/util/JsonExclude.java b/common/src/main/java/io/bitsquare/common/util/JsonExclude.java new file mode 100644 index 0000000000..a30483e8c6 --- /dev/null +++ b/common/src/main/java/io/bitsquare/common/util/JsonExclude.java @@ -0,0 +1,28 @@ +/* + * 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 java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface JsonExclude { +} \ No newline at end of file diff --git a/common/src/main/java/io/bitsquare/common/util/Profiler.java b/common/src/main/java/io/bitsquare/common/util/Profiler.java new file mode 100644 index 0000000000..fc041446bf --- /dev/null +++ b/common/src/main/java/io/bitsquare/common/util/Profiler.java @@ -0,0 +1,34 @@ +/* + * 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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Profiler { + private static final Logger log = LoggerFactory.getLogger(Profiler.class); + + public static void printSystemLoad(Logger log) { + Runtime runtime = Runtime.getRuntime(); + long free = runtime.freeMemory() / 1024 / 1024; + long total = runtime.totalMemory() / 1024 / 1024; + long used = total - free; + log.info("System load (nr. threads/used memory (MB)): " + Thread.activeCount() + "/" + used); + } + +} diff --git a/common/src/main/java/io/bitsquare/common/util/Tuple2.java b/common/src/main/java/io/bitsquare/common/util/Tuple2.java new file mode 100644 index 0000000000..8d2530f9e9 --- /dev/null +++ b/common/src/main/java/io/bitsquare/common/util/Tuple2.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.common.util; + +public class Tuple2 { + final public A first; + final public B second; + + public Tuple2(A first, B second) { + this.first = first; + this.second = second; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Tuple2)) return false; + + Tuple2 tuple2 = (Tuple2) o; + + if (first != null ? !first.equals(tuple2.first) : tuple2.first != null) return false; + return !(second != null ? !second.equals(tuple2.second) : tuple2.second != null); + + } + + @Override + public int hashCode() { + int result = first != null ? first.hashCode() : 0; + result = 31 * result + (second != null ? second.hashCode() : 0); + return result; + } +} diff --git a/common/src/main/java/io/bitsquare/common/util/Tuple3.java b/common/src/main/java/io/bitsquare/common/util/Tuple3.java new file mode 100644 index 0000000000..3229f98fd7 --- /dev/null +++ b/common/src/main/java/io/bitsquare/common/util/Tuple3.java @@ -0,0 +1,51 @@ +/* + * This file is part of Bitsquare. + * + * Bitsquare is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bitsquare is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bitsquare. If not, see . + */ + +package io.bitsquare.common.util; + +public class Tuple3 { + final public A first; + final public B second; + final public C third; + + public Tuple3(A first, B second, C third) { + this.first = first; + this.second = second; + this.third = third; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Tuple3)) return false; + + Tuple3 tuple3 = (Tuple3) o; + + if (first != null ? !first.equals(tuple3.first) : tuple3.first != null) return false; + if (second != null ? !second.equals(tuple3.second) : tuple3.second != null) return false; + return !(third != null ? !third.equals(tuple3.third) : tuple3.third != null); + + } + + @Override + public int hashCode() { + int result = first != null ? first.hashCode() : 0; + result = 31 * result + (second != null ? second.hashCode() : 0); + result = 31 * result + (third != null ? third.hashCode() : 0); + return result; + } +} diff --git a/core/src/main/java/io/bitsquare/alert/Alert.java b/core/src/main/java/io/bitsquare/alert/Alert.java new file mode 100644 index 0000000000..1a0a3751d2 --- /dev/null +++ b/core/src/main/java/io/bitsquare/alert/Alert.java @@ -0,0 +1,84 @@ +/* + * 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.alert; + +import io.bitsquare.app.Version; +import io.bitsquare.p2p.storage.data.PubKeyProtectedExpirablePayload; + +import java.security.PublicKey; + +public final class Alert implements PubKeyProtectedExpirablePayload { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + + public static final long TTL = 10 * 24 * 60 * 60 * 1000; // 10 days + + public final String message; + private String signatureAsBase64; + private PublicKey storagePublicKey; + + public Alert(String message) { + this.message = message; + } + + public void setSigAndStoragePubKey(String signatureAsBase64, PublicKey storagePublicKey) { + this.signatureAsBase64 = signatureAsBase64; + this.storagePublicKey = storagePublicKey; + } + + public String getSignatureAsBase64() { + return signatureAsBase64; + } + + @Override + public long getTTL() { + return TTL; + } + + @Override + public PublicKey getPubKey() { + return storagePublicKey; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Alert)) return false; + + Alert that = (Alert) o; + + if (message != null ? !message.equals(that.message) : that.message != null) return false; + return !(getSignatureAsBase64() != null ? !getSignatureAsBase64().equals(that.getSignatureAsBase64()) : that.getSignatureAsBase64() != null); + + } + + @Override + public int hashCode() { + int result = message != null ? message.hashCode() : 0; + result = 31 * result + (getSignatureAsBase64() != null ? getSignatureAsBase64().hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "AlertMessage{" + + "message='" + message + '\'' + + ", signature.hashCode()='" + signatureAsBase64.hashCode() + '\'' + + '}'; + } +} diff --git a/core/src/main/java/io/bitsquare/alert/AlertManager.java b/core/src/main/java/io/bitsquare/alert/AlertManager.java new file mode 100644 index 0000000000..fec210fd0c --- /dev/null +++ b/core/src/main/java/io/bitsquare/alert/AlertManager.java @@ -0,0 +1,144 @@ +/* + * 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.alert; + +import com.google.inject.Inject; +import io.bitsquare.common.crypto.KeyRing; +import io.bitsquare.p2p.storage.HashSetChangedListener; +import io.bitsquare.p2p.storage.data.ProtectedData; +import io.bitsquare.user.User; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Serializable; +import java.math.BigInteger; +import java.security.SignatureException; + +import static org.bitcoinj.core.Utils.HEX; + +public class AlertManager { + transient private static final Logger log = LoggerFactory.getLogger(AlertManager.class); + + private final AlertService alertService; + private KeyRing keyRing; + private User user; + private final ObjectProperty alertMessageProperty = new SimpleObjectProperty<>(); + + // Pub key for developer global alert message + private static final String devPubKeyAsHex = "02682880ae61fc1ea9375198bf2b5594fc3ed28074d3f5f0ed907e38acc5fb1fdc"; + private ECKey alertSigningKey; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, Initialization + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public AlertManager(AlertService alertService, KeyRing keyRing, User user) { + this.alertService = alertService; + this.keyRing = keyRing; + this.user = user; + + alertService.addHashSetChangedListener(new HashSetChangedListener() { + @Override + public void onAdded(ProtectedData entry) { + Serializable data = entry.expirablePayload; + if (data instanceof Alert) { + Alert alert = (Alert) data; + if (verifySignature(alert)) + alertMessageProperty.set(alert); + } + } + + @Override + public void onRemoved(ProtectedData entry) { + Serializable data = entry.expirablePayload; + if (data instanceof Alert) { + Alert alert = (Alert) data; + if (verifySignature(alert)) + alertMessageProperty.set(null); + } + } + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public ReadOnlyObjectProperty alertMessageProperty() { + return alertMessageProperty; + } + + public boolean addAlertMessageIfKeyIsValid(Alert alert, String privKeyString) { + // if there is a previous message we remove that first + if (user.getDevelopersAlert() != null) + removeAlertMessageIfKeyIsValid(privKeyString); + + boolean isKeyValid = isKeyValid(privKeyString); + if (isKeyValid) { + signAndAddSignatureToAlertMessage(alert); + user.setDevelopersAlert(alert); + alertService.addAlertMessage(alert, null, null); + } + return isKeyValid; + } + + public boolean removeAlertMessageIfKeyIsValid(String privKeyString) { + Alert developersAlert = user.getDevelopersAlert(); + if (isKeyValid(privKeyString) && developersAlert != null) { + alertService.removeAlertMessage(developersAlert, null, null); + user.setDevelopersAlert(null); + return true; + } else { + return false; + } + } + + private boolean isKeyValid(String privKeyString) { + try { + alertSigningKey = ECKey.fromPrivate(new BigInteger(1, HEX.decode(privKeyString))); + return devPubKeyAsHex.equals(Utils.HEX.encode(alertSigningKey.getPubKey())); + } catch (Throwable t) { + return false; + } + } + + private void signAndAddSignatureToAlertMessage(Alert alert) { + String alertMessageAsHex = Utils.HEX.encode(alert.message.getBytes()); + String signatureAsBase64 = alertSigningKey.signMessage(alertMessageAsHex); + alert.setSigAndStoragePubKey(signatureAsBase64, keyRing.getStorageSignatureKeyPair().getPublic()); + } + + private boolean verifySignature(Alert alert) { + String alertMessageAsHex = Utils.HEX.encode(alert.message.getBytes()); + try { + ECKey.fromPublicOnly(HEX.decode(devPubKeyAsHex)).verifyMessage(alertMessageAsHex, alert.getSignatureAsBase64()); + return true; + } catch (SignatureException e) { + log.warn("verifySignature failed"); + return false; + } + } +} diff --git a/core/src/main/java/io/bitsquare/alert/AlertModule.java b/core/src/main/java/io/bitsquare/alert/AlertModule.java new file mode 100644 index 0000000000..15224b2e48 --- /dev/null +++ b/core/src/main/java/io/bitsquare/alert/AlertModule.java @@ -0,0 +1,38 @@ +/* + * 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.alert; + +import com.google.inject.Singleton; +import io.bitsquare.app.AppModule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.env.Environment; + +public class AlertModule extends AppModule { + private static final Logger log = LoggerFactory.getLogger(AlertModule.class); + + public AlertModule(Environment env) { + super(env); + } + + @Override + protected final void configure() { + bind(AlertManager.class).in(Singleton.class); + bind(AlertService.class).in(Singleton.class); + } +} diff --git a/core/src/main/java/io/bitsquare/alert/AlertService.java b/core/src/main/java/io/bitsquare/alert/AlertService.java new file mode 100644 index 0000000000..da1f8b43c2 --- /dev/null +++ b/core/src/main/java/io/bitsquare/alert/AlertService.java @@ -0,0 +1,71 @@ +/* + * 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.alert; + +import io.bitsquare.common.handlers.ErrorMessageHandler; +import io.bitsquare.common.handlers.ResultHandler; +import io.bitsquare.p2p.P2PService; +import io.bitsquare.p2p.storage.HashSetChangedListener; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; + +/** + * Used to load global alert messages. + * The message is signed by the project developers private key and use data protection. + */ +public class AlertService { + private static final Logger log = LoggerFactory.getLogger(AlertService.class); + private P2PService p2PService; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, Initialization + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public AlertService(P2PService p2PService) { + this.p2PService = p2PService; + } + + public void addHashSetChangedListener(HashSetChangedListener hashSetChangedListener) { + p2PService.addHashSetChangedListener(hashSetChangedListener); + } + + public void addAlertMessage(Alert alert, @Nullable ResultHandler resultHandler, @Nullable ErrorMessageHandler errorMessageHandler) { + boolean result = p2PService.addData(alert); + if (result) { + log.trace("Add alertMessage to network was successful. AlertMessage = " + alert); + if (resultHandler != null) resultHandler.handleResult(); + } else { + if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage("Add alertMessage failed"); + } + } + + public void removeAlertMessage(Alert alert, @Nullable ResultHandler resultHandler, @Nullable ErrorMessageHandler errorMessageHandler) { + if (p2PService.removeData(alert)) { + log.trace("Remove alertMessage from network was successful. AlertMessage = " + alert); + if (resultHandler != null) resultHandler.handleResult(); + } else { + if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage("Remove alertMessage failed"); + } + } + +} 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..86a706f8c8 --- /dev/null +++ b/core/src/main/java/io/bitsquare/arbitration/ArbitratorManager.java @@ -0,0 +1,264 @@ +/* + * 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 com.google.inject.name.Named; +import io.bitsquare.app.ProgramArguments; +import io.bitsquare.common.crypto.KeyRing; +import io.bitsquare.common.handlers.ErrorMessageHandler; +import io.bitsquare.common.handlers.ResultHandler; +import io.bitsquare.p2p.Address; +import io.bitsquare.p2p.P2PService; +import io.bitsquare.p2p.P2PServiceListener; +import io.bitsquare.p2p.storage.HashSetChangedListener; +import io.bitsquare.p2p.storage.data.ProtectedData; +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.reactfx.util.FxTimer; +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.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.bitcoinj.core.Utils.HEX; + +public class ArbitratorManager { + transient private static final Logger log = LoggerFactory.getLogger(ArbitratorManager.class); + + private final KeyRing keyRing; + private final ArbitratorService arbitratorService; + private final User user; + private final ObservableMap arbitratorsObservableMap = FXCollections.observableHashMap(); + + // Keys for invited arbitrators in bootstrapping phase (before registration is open to anyone and security payment is implemented) + // For testing purpose here is a private key so anyone can setup an arbitrator for now. + // The matching pubkey will be removed once we use real arbitrators. + // PrivKey for testing: 6ac43ea1df2a290c1c8391736aa42e4339c5cb4f110ff0257a13b63211977b7a + // Matching pubKey: 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", + "0274f772a98d23e7a0251ab30d7121897b5aebd11a2f1e45ab654aa57503173245", + "036d8a1dfcb406886037d2381da006358722823e1940acc2598c844bbc0fd1026f" + )); + private static final String publicKeyForTesting = "027a381b5333a56e1cc3d90d3a7d07f26509adf7029ed06fc997c656621f8da1ee"; + private final boolean isDevTest; + private P2PServiceListener p2PServiceListener; + + @Inject + public ArbitratorManager(@Named(ProgramArguments.DEV_TEST) boolean isDevTest, KeyRing keyRing, ArbitratorService arbitratorService, User user) { + this.isDevTest = isDevTest; + this.keyRing = keyRing; + this.arbitratorService = arbitratorService; + this.user = user; + + arbitratorService.addHashSetChangedListener(new HashSetChangedListener() { + @Override + public void onAdded(ProtectedData entry) { + applyArbitrators(); + } + + @Override + public void onRemoved(ProtectedData entry) { + applyArbitrators(); + } + }); + } + + public void onAllServicesInitialized() { + if (user.getRegisteredArbitrator() != null) { + + P2PService p2PService = arbitratorService.getP2PService(); + if (!p2PService.isAuthenticated()) { + p2PServiceListener = new P2PServiceListener() { + @Override + public void onTorNodeReady() { + } + + @Override + public void onHiddenServiceReady() { + } + + @Override + public void onSetupFailed(Throwable throwable) { + } + + @Override + public void onAllDataReceived() { + } + + @Override + public void onAuthenticated() { + republishArbitrator(); + } + }; + p2PService.addP2PServiceListener(p2PServiceListener); + + } else { + republishArbitrator(); + } + + // re-publish periodically + FxTimer.runPeriodically( + Duration.ofMillis(Arbitrator.TTL / 2), + () -> republishArbitrator() + ); + } + + applyArbitrators(); + } + + private void republishArbitrator() { + if (p2PServiceListener != null) arbitratorService.getP2PService().removeP2PServiceListener(p2PServiceListener); + + Arbitrator registeredArbitrator = user.getRegisteredArbitrator(); + if (registeredArbitrator != null) { + addArbitrator(registeredArbitrator, + this::applyArbitrators, + log::error + ); + } + } + + public void applyArbitrators() { + Map map = arbitratorService.getArbitrators(); + log.trace("Arbitrators . size=" + (map.values() != null ? map.values().size() : "0")); + arbitratorsObservableMap.clear(); + Map filtered = map.values().stream() + .filter(e -> isPublicKeyInList(Utils.HEX.encode(e.getRegistrationPubKey())) + && verifySignature(e.getPubKeyRing().getStorageSignaturePubKey(), e.getRegistrationPubKey(), e.getRegistrationSignature())) + .collect(Collectors.toMap(Arbitrator::getArbitratorAddress, Function.identity())); + + arbitratorsObservableMap.putAll(filtered); + + log.debug("filtered arbitrators: " + arbitratorsObservableMap.values()); + log.trace("user.getAcceptedArbitrators(): " + user.getAcceptedArbitrators().toString()); + + // we need to remove accepted arbitrators which are not available anymore + if (user.getAcceptedArbitrators() != null) { + List removeList = user.getAcceptedArbitrators().stream() + .filter(e -> !arbitratorsObservableMap.containsValue(e)) + .collect(Collectors.toList()); + removeList.stream().forEach(user::removeAcceptedArbitrator); + log.trace("removeList arbitrators: " + removeList.toString()); + log.trace("user.getAcceptedArbitrators(): " + user.getAcceptedArbitrators().toString()); + + // if we don't have any arbitrator anymore we set all matching + if (user.getAcceptedArbitrators().isEmpty()) { + arbitratorsObservableMap.values().stream() + .filter(arbitrator -> user.hasMatchingLanguage(arbitrator)) + .forEach(arbitrator -> user.addAcceptedArbitrator(arbitrator)); + } + + log.trace("user.getAcceptedArbitrators(): " + user.getAcceptedArbitrators().toString()); + } + } + + public void addArbitrator(Arbitrator arbitrator, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + user.setRegisteredArbitrator(arbitrator); + arbitratorsObservableMap.put(arbitrator.getArbitratorAddress(), arbitrator); + arbitratorService.addArbitrator(arbitrator, + () -> { + log.debug("Arbitrator successfully saved in P2P network"); + resultHandler.handleResult(); + + if (arbitratorsObservableMap.size() > 0) + FxTimer.runLater(Duration.ofMillis(1000), this::applyArbitrators); + }, + errorMessageHandler::handleErrorMessage); + } + + public void removeArbitrator(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + Arbitrator registeredArbitrator = user.getRegisteredArbitrator(); + if (registeredArbitrator != null) { + user.setRegisteredArbitrator(null); + arbitratorsObservableMap.remove(registeredArbitrator.getArbitratorAddress()); + 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().getStorageSignaturePubKey().getEncoded()); + return key.signMessage(keyToSignAsHex); + } + + 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; + } + } + + @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 isDevTest && pubKeyAsHex.equals(publicKeyForTesting) || publicKeys.contains(pubKeyAsHex); + } +} diff --git a/core/src/main/java/io/bitsquare/arbitration/Dispute.java b/core/src/main/java/io/bitsquare/arbitration/Dispute.java new file mode 100644 index 0000000000..aa1f1ecaf1 --- /dev/null +++ b/core/src/main/java/io/bitsquare/arbitration/Dispute.java @@ -0,0 +1,354 @@ +/* + * 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 io.bitsquare.app.Version; +import io.bitsquare.arbitration.messages.DisputeMailMessage; +import io.bitsquare.common.crypto.PubKeyRing; +import io.bitsquare.storage.Storage; +import io.bitsquare.trade.Contract; +import javafx.beans.property.*; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +public class Dispute implements Serializable { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + transient private static final Logger log = LoggerFactory.getLogger(Dispute.class); + + /////////////////////////////////////////////////////////////////////////////////////////// + // Fields + /////////////////////////////////////////////////////////////////////////////////////////// + + private final String tradeId; + private final int traderId; + private final boolean disputeOpenerIsBuyer; + private final boolean disputeOpenerIsOfferer; + private final long openingDate; + private final PubKeyRing traderPubKeyRing; + private final long tradeDate; + private final Contract contract; + private final byte[] contractHash; + @Nullable + private final byte[] depositTxSerialized; + @Nullable + private final byte[] payoutTxSerialized; + @Nullable + private final String depositTxId; + @Nullable + private final String payoutTxId; + private final String contractAsJson; + private final String offererContractSignature; + private final String takerContractSignature; + private final PubKeyRing arbitratorPubKeyRing; + private final boolean isSupportTicket; + + private final List disputeMailMessages = new ArrayList<>(); + + private boolean isClosed; + private DisputeResult disputeResult; + + transient private Storage> storage; + transient private ObservableList disputeMailMessagesAsObservableList = FXCollections.observableArrayList(disputeMailMessages); + transient private BooleanProperty isClosedProperty = new SimpleBooleanProperty(isClosed); + transient private ObjectProperty disputeResultProperty = new SimpleObjectProperty<>(disputeResult); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public Dispute(Storage> storage, + String tradeId, + int traderId, + boolean disputeOpenerIsBuyer, + boolean disputeOpenerIsOfferer, + PubKeyRing traderPubKeyRing, + Date tradeDate, + Contract contract, + byte[] contractHash, + @Nullable byte[] depositTxSerialized, + @Nullable byte[] payoutTxSerialized, + @Nullable String depositTxId, + @Nullable String payoutTxId, + String contractAsJson, + String offererContractSignature, + String takerContractSignature, + PubKeyRing arbitratorPubKeyRing, + boolean isSupportTicket) { + this.storage = storage; + this.tradeId = tradeId; + this.traderId = traderId; + this.disputeOpenerIsBuyer = disputeOpenerIsBuyer; + this.disputeOpenerIsOfferer = disputeOpenerIsOfferer; + this.traderPubKeyRing = traderPubKeyRing; + this.tradeDate = tradeDate.getTime(); + this.contract = contract; + this.contractHash = contractHash; + this.depositTxSerialized = depositTxSerialized; + this.payoutTxSerialized = payoutTxSerialized; + this.depositTxId = depositTxId; + this.payoutTxId = payoutTxId; + this.contractAsJson = contractAsJson; + this.offererContractSignature = offererContractSignature; + this.takerContractSignature = takerContractSignature; + this.arbitratorPubKeyRing = arbitratorPubKeyRing; + this.isSupportTicket = isSupportTicket; + this.openingDate = new Date().getTime(); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + try { + in.defaultReadObject(); + disputeMailMessagesAsObservableList = FXCollections.observableArrayList(disputeMailMessages); + disputeResultProperty = new SimpleObjectProperty<>(disputeResult); + isClosedProperty = new SimpleBooleanProperty(isClosed); + } catch (Throwable t) { + log.trace("Cannot be deserialized." + t.getMessage()); + } + } + + public void addDisputeMessage(DisputeMailMessage disputeMailMessage) { + if (!disputeMailMessages.contains(disputeMailMessage)) { + disputeMailMessages.add(disputeMailMessage); + disputeMailMessagesAsObservableList.add(disputeMailMessage); + storage.queueUpForSave(); + } else { + log.error("disputeMailMessage already exists"); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Setters + /////////////////////////////////////////////////////////////////////////////////////////// + + // In case we get the object via the network storage is not set as its transient, so we need to set it. + public void setStorage(Storage> storage) { + this.storage = storage; + } + + public void setIsClosed(boolean isClosed) { + this.isClosed = isClosed; + isClosedProperty.set(isClosed); + storage.queueUpForSave(); + } + + public void setDisputeResult(DisputeResult disputeResult) { + this.disputeResult = disputeResult; + disputeResultProperty.set(disputeResult); + storage.queueUpForSave(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + public String getTradeId() { + return tradeId; + } + + public String getShortTradeId() { + return tradeId.substring(0, 8); + } + + public int getTraderId() { + return traderId; + } + + public boolean isDisputeOpenerIsBuyer() { + return disputeOpenerIsBuyer; + } + + public boolean isDisputeOpenerIsOfferer() { + return disputeOpenerIsOfferer; + } + + public Date getOpeningDate() { + return new Date(openingDate); + } + + public PubKeyRing getTraderPubKeyRing() { + return traderPubKeyRing; + } + + public Contract getContract() { + return contract; + } + + @Nullable + public byte[] getDepositTxSerialized() { + return depositTxSerialized; + } + + @Nullable + public byte[] getPayoutTxSerialized() { + return payoutTxSerialized; + } + + @Nullable + public String getDepositTxId() { + return depositTxId; + } + + @Nullable + public String getPayoutTxId() { + return payoutTxId; + } + + public String getContractAsJson() { + return contractAsJson; + } + + public String getOffererContractSignature() { + return offererContractSignature; + } + + public String getTakerContractSignature() { + return takerContractSignature; + } + + public ObservableList getDisputeMailMessagesAsObservableList() { + return disputeMailMessagesAsObservableList; + } + + public boolean isClosed() { + return isClosedProperty.get(); + } + + public ReadOnlyBooleanProperty isClosedProperty() { + return isClosedProperty; + } + + public PubKeyRing getArbitratorPubKeyRing() { + return arbitratorPubKeyRing; + } + + public ObjectProperty disputeResultProperty() { + return disputeResultProperty; + } + + public boolean isSupportTicket() { + return isSupportTicket; + } + + public byte[] getContractHash() { + return contractHash; + } + + public Date getTradeDate() { + return new Date(tradeDate); + } + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Dispute)) return false; + + Dispute dispute = (Dispute) o; + + if (traderId != dispute.traderId) return false; + if (disputeOpenerIsBuyer != dispute.disputeOpenerIsBuyer) return false; + if (disputeOpenerIsOfferer != dispute.disputeOpenerIsOfferer) return false; + if (openingDate != dispute.openingDate) return false; + if (tradeDate != dispute.tradeDate) return false; + if (isSupportTicket != dispute.isSupportTicket) return false; + if (isClosed != dispute.isClosed) return false; + if (tradeId != null ? !tradeId.equals(dispute.tradeId) : dispute.tradeId != null) return false; + if (traderPubKeyRing != null ? !traderPubKeyRing.equals(dispute.traderPubKeyRing) : dispute.traderPubKeyRing != null) + return false; + if (contract != null ? !contract.equals(dispute.contract) : dispute.contract != null) return false; + if (!Arrays.equals(contractHash, dispute.contractHash)) return false; + if (!Arrays.equals(depositTxSerialized, dispute.depositTxSerialized)) return false; + if (!Arrays.equals(payoutTxSerialized, dispute.payoutTxSerialized)) return false; + if (depositTxId != null ? !depositTxId.equals(dispute.depositTxId) : dispute.depositTxId != null) return false; + if (payoutTxId != null ? !payoutTxId.equals(dispute.payoutTxId) : dispute.payoutTxId != null) return false; + if (contractAsJson != null ? !contractAsJson.equals(dispute.contractAsJson) : dispute.contractAsJson != null) + return false; + if (offererContractSignature != null ? !offererContractSignature.equals(dispute.offererContractSignature) : dispute.offererContractSignature != null) + return false; + if (takerContractSignature != null ? !takerContractSignature.equals(dispute.takerContractSignature) : dispute.takerContractSignature != null) + return false; + if (arbitratorPubKeyRing != null ? !arbitratorPubKeyRing.equals(dispute.arbitratorPubKeyRing) : dispute.arbitratorPubKeyRing != null) + return false; + if (disputeMailMessages != null ? !disputeMailMessages.equals(dispute.disputeMailMessages) : dispute.disputeMailMessages != null) + return false; + return !(disputeResult != null ? !disputeResult.equals(dispute.disputeResult) : dispute.disputeResult != null); + + } + + @Override + public int hashCode() { + int result = tradeId != null ? tradeId.hashCode() : 0; + result = 31 * result + traderId; + result = 31 * result + (disputeOpenerIsBuyer ? 1 : 0); + result = 31 * result + (disputeOpenerIsOfferer ? 1 : 0); + result = 31 * result + (int) (openingDate ^ (openingDate >>> 32)); + result = 31 * result + (traderPubKeyRing != null ? traderPubKeyRing.hashCode() : 0); + result = 31 * result + (int) (tradeDate ^ (tradeDate >>> 32)); + result = 31 * result + (contract != null ? contract.hashCode() : 0); + result = 31 * result + (contractHash != null ? Arrays.hashCode(contractHash) : 0); + result = 31 * result + (depositTxSerialized != null ? Arrays.hashCode(depositTxSerialized) : 0); + result = 31 * result + (payoutTxSerialized != null ? Arrays.hashCode(payoutTxSerialized) : 0); + result = 31 * result + (depositTxId != null ? depositTxId.hashCode() : 0); + result = 31 * result + (payoutTxId != null ? payoutTxId.hashCode() : 0); + result = 31 * result + (contractAsJson != null ? contractAsJson.hashCode() : 0); + result = 31 * result + (offererContractSignature != null ? offererContractSignature.hashCode() : 0); + result = 31 * result + (takerContractSignature != null ? takerContractSignature.hashCode() : 0); + result = 31 * result + (arbitratorPubKeyRing != null ? arbitratorPubKeyRing.hashCode() : 0); + result = 31 * result + (isSupportTicket ? 1 : 0); + result = 31 * result + (disputeMailMessages != null ? disputeMailMessages.hashCode() : 0); + result = 31 * result + (isClosed ? 1 : 0); + result = 31 * result + (disputeResult != null ? disputeResult.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "Dispute{" + + ", tradeId='" + tradeId + '\'' + + ", traderId='" + traderId + '\'' + + ", disputeOpenerIsBuyer=" + disputeOpenerIsBuyer + + ", disputeOpenerIsOfferer=" + disputeOpenerIsOfferer + + ", openingDate=" + openingDate + + ", traderPubKeyRing=" + traderPubKeyRing + + ", contract=" + contract + + ", contractAsJson='" + contractAsJson + '\'' + + ", buyerContractSignature='" + offererContractSignature + '\'' + + ", sellerContractSignature='" + takerContractSignature + '\'' + + ", arbitratorPubKeyRing=" + arbitratorPubKeyRing + + ", disputeMailMessages=" + disputeMailMessages + + ", disputeMailMessagesAsObservableList=" + disputeMailMessagesAsObservableList + + ", isClosed=" + isClosed + + ", disputeResult=" + disputeResult + + ", disputeResultProperty=" + disputeResultProperty + + '}'; + } +} diff --git a/core/src/main/java/io/bitsquare/arbitration/DisputeList.java b/core/src/main/java/io/bitsquare/arbitration/DisputeList.java new file mode 100644 index 0000000000..0855a80c25 --- /dev/null +++ b/core/src/main/java/io/bitsquare/arbitration/DisputeList.java @@ -0,0 +1,93 @@ +/* + * 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 io.bitsquare.app.Version; +import io.bitsquare.storage.Storage; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.util.ArrayList; + +public class DisputeList extends ArrayList implements Serializable { + // That object is saved to disc. We need to take care of changes to not break deserialization. + private static final long serialVersionUID = Version.LOCAL_DB_VERSION; + + private static final Logger log = LoggerFactory.getLogger(DisputeList.class); + + final transient private Storage> storage; + transient private ObservableList observableList; + + public DisputeList(Storage> storage) { + this.storage = storage; + + DisputeList persisted = storage.initAndGetPersisted(this); + if (persisted != null) { + this.addAll(persisted); + } + observableList = FXCollections.observableArrayList(this); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + try { + in.defaultReadObject(); + } catch (Throwable t) { + log.trace("Cannot be deserialized." + t.getMessage()); + } + } + + @Override + public boolean add(DisputeCase disputeCase) { + if (!super.contains(disputeCase)) { + boolean result = super.add(disputeCase); + getObservableList().add(disputeCase); + storage.queueUpForSave(); + return result; + } else { + return false; + } + } + + @Override + public boolean remove(Object disputeCase) { + boolean result = super.remove(disputeCase); + getObservableList().remove(disputeCase); + storage.queueUpForSave(); + return result; + } + + private ObservableList getObservableList() { + if (observableList == null) + observableList = FXCollections.observableArrayList(this); + return observableList; + } + + @NotNull + @Override + public String toString() { + return "DisputeList{" + + ", observableList=" + observableList + + '}'; + } +} diff --git a/core/src/main/java/io/bitsquare/arbitration/DisputeManager.java b/core/src/main/java/io/bitsquare/arbitration/DisputeManager.java new file mode 100644 index 0000000000..6ff800ca0e --- /dev/null +++ b/core/src/main/java/io/bitsquare/arbitration/DisputeManager.java @@ -0,0 +1,608 @@ +/* + * 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.common.util.concurrent.FutureCallback; +import com.google.inject.Inject; +import io.bitsquare.arbitration.messages.*; +import io.bitsquare.btc.TradeWalletService; +import io.bitsquare.btc.WalletService; +import io.bitsquare.btc.exceptions.TransactionVerificationException; +import io.bitsquare.btc.exceptions.WalletException; +import io.bitsquare.common.crypto.KeyRing; +import io.bitsquare.common.crypto.PubKeyRing; +import io.bitsquare.p2p.Address; +import io.bitsquare.p2p.Message; +import io.bitsquare.p2p.P2PService; +import io.bitsquare.p2p.P2PServiceListener; +import io.bitsquare.p2p.messaging.DecryptedMessageWithPubKey; +import io.bitsquare.p2p.messaging.SendMailboxMessageListener; +import io.bitsquare.storage.Storage; +import io.bitsquare.trade.Contract; +import io.bitsquare.trade.Trade; +import io.bitsquare.trade.TradeManager; +import io.bitsquare.trade.offer.OpenOffer; +import io.bitsquare.trade.offer.OpenOfferManager; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import org.bitcoinj.core.AddressFormatException; +import org.bitcoinj.core.Transaction; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Named; +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; + +public class DisputeManager { + private static final Logger log = LoggerFactory.getLogger(DisputeManager.class); + + private final TradeWalletService tradeWalletService; + private final WalletService walletService; + private final TradeManager tradeManager; + private final OpenOfferManager openOfferManager; + private final P2PService p2PService; + private final KeyRing keyRing; + private final Storage> disputeStorage; + private final DisputeList disputes; + transient private final ObservableList disputesObservableList; + private final String disputeInfo; + private final P2PServiceListener p2PServiceListener; + private final List decryptedMailboxMessageWithPubKeys = new CopyOnWriteArrayList<>(); + private final List decryptedMailMessageWithPubKeys = new CopyOnWriteArrayList<>(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public DisputeManager(P2PService p2PService, + TradeWalletService tradeWalletService, + WalletService walletService, + TradeManager tradeManager, + OpenOfferManager openOfferManager, + KeyRing keyRing, + @Named("storage.dir") File storageDir) { + this.p2PService = p2PService; + this.tradeWalletService = tradeWalletService; + this.walletService = walletService; + this.tradeManager = tradeManager; + this.openOfferManager = openOfferManager; + this.keyRing = keyRing; + + disputeStorage = new Storage<>(storageDir); + disputes = new DisputeList<>(disputeStorage); + disputesObservableList = FXCollections.observableArrayList(disputes); + disputes.stream().forEach(e -> e.setStorage(getDisputeStorage())); + + disputeInfo = "Please note the basic rules for the dispute process:\n" + + "1. You need to respond to the arbitrators requests in between 2 days.\n" + + "2. The maximum period for the dispute is 14 days.\n" + + "3. You need to fulfill what the arbitrator is requesting from you to deliver evidence for your case.\n" + + "4. You accepted the rules outlined in the wiki in the user agreement when you first started the application.\n\n" + + "Please read more in detail about the dispute process in our wiki:\nhttps://github" + + ".com/bitsquare/bitsquare/wiki/Dispute-process"; + + p2PService.addDecryptedMailListener((decryptedMessageWithPubKey, senderAddress) -> { + decryptedMailMessageWithPubKeys.add(decryptedMessageWithPubKey); + if (p2PService.isAuthenticated()) + applyMessages(); + }); + p2PService.addDecryptedMailboxListener((decryptedMessageWithPubKey, senderAddress) -> { + decryptedMailboxMessageWithPubKeys.add(decryptedMessageWithPubKey); + if (p2PService.isAuthenticated()) + applyMessages(); + }); + + p2PServiceListener = new P2PServiceListener() { + @Override + public void onTorNodeReady() { + } + + @Override + public void onHiddenServiceReady() { + } + + @Override + public void onSetupFailed(Throwable throwable) { + } + + @Override + public void onAllDataReceived() { + } + + @Override + public void onAuthenticated() { + applyMessages(); + } + }; + p2PService.addP2PServiceListener(p2PServiceListener); + } + + private void applyMessages() { + decryptedMailMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> { + Message message = decryptedMessageWithPubKey.message; + if (message instanceof DisputeMessage) + dispatchMessage((DisputeMessage) message); + }); + decryptedMailMessageWithPubKeys.clear(); + + decryptedMailboxMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> { + Message message = decryptedMessageWithPubKey.message; + log.debug("decryptedMessageWithPubKey.message " + message); + if (message instanceof DisputeMessage) { + dispatchMessage((DisputeMessage) message); + p2PService.removeEntryFromMailbox(decryptedMessageWithPubKey); + } + }); + decryptedMailboxMessageWithPubKeys.clear(); + + p2PService.removeP2PServiceListener(p2PServiceListener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onAllServicesInitialized() { + } + + private void dispatchMessage(DisputeMessage message) { + if (message instanceof OpenNewDisputeMessage) + onOpenNewDisputeMessage((OpenNewDisputeMessage) message); + else if (message instanceof PeerOpenedDisputeMessage) + onPeerOpenedDisputeMessage((PeerOpenedDisputeMessage) message); + else if (message instanceof DisputeMailMessage) + onDisputeMailMessage((DisputeMailMessage) message); + else if (message instanceof DisputeResultMessage) + onDisputeResultMessage((DisputeResultMessage) message); + else if (message instanceof PeerPublishedPayoutTxMessage) + onDisputedPayoutTxMessage((PeerPublishedPayoutTxMessage) message); + } + + public void sendOpenNewDisputeMessage(Dispute dispute) { + if (!disputes.contains(dispute)) { + DisputeMailMessage disputeMailMessage = new DisputeMailMessage(dispute.getTradeId(), + keyRing.getPubKeyRing().hashCode(), + true, + "System message: " + (dispute.isSupportTicket() ? + "You opened a request for support." + : "You opened a request for a dispute.\n\n" + disputeInfo), + p2PService.getAddress()); + disputeMailMessage.setIsSystemMessage(true); + dispute.addDisputeMessage(disputeMailMessage); + disputes.add(dispute); + disputesObservableList.add(dispute); + + p2PService.sendEncryptedMailboxMessage(dispute.getContract().arbitratorAddress, + dispute.getArbitratorPubKeyRing(), + new OpenNewDisputeMessage(dispute, p2PService.getAddress()), + new SendMailboxMessageListener() { + @Override + public void onArrived() { + disputeMailMessage.setArrived(true); + } + + @Override + public void onStoredInMailbox() { + disputeMailMessage.setStoredInMailbox(true); + } + + @Override + public void onFault() { + log.error("sendEncryptedMessage failed"); + } + } + ); + + } else { + log.warn("We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId()); + } + } + + // arbitrator sends that to trading peer when he received openDispute request + private void sendPeerOpenedDisputeMessage(Dispute disputeFromOpener) { + Contract contractFromOpener = disputeFromOpener.getContract(); + PubKeyRing pubKeyRing = disputeFromOpener.isDisputeOpenerIsBuyer() ? contractFromOpener.getSellerPubKeyRing() : contractFromOpener.getBuyerPubKeyRing(); + Dispute dispute = new Dispute( + disputeStorage, + disputeFromOpener.getTradeId(), + pubKeyRing.hashCode(), + !disputeFromOpener.isDisputeOpenerIsBuyer(), + !disputeFromOpener.isDisputeOpenerIsOfferer(), + pubKeyRing, + disputeFromOpener.getTradeDate(), + contractFromOpener, + disputeFromOpener.getContractHash(), + disputeFromOpener.getDepositTxSerialized(), + disputeFromOpener.getPayoutTxSerialized(), + disputeFromOpener.getDepositTxId(), + disputeFromOpener.getPayoutTxId(), + disputeFromOpener.getContractAsJson(), + disputeFromOpener.getOffererContractSignature(), + disputeFromOpener.getTakerContractSignature(), + disputeFromOpener.getArbitratorPubKeyRing(), + disputeFromOpener.isSupportTicket() + ); + DisputeMailMessage disputeMailMessage = new DisputeMailMessage(dispute.getTradeId(), + keyRing.getPubKeyRing().hashCode(), + true, + "System message: " + (dispute.isSupportTicket() ? + "Your trading peer has requested support due technical problems. Please wait for further instructions." + : "Your trading peer has requested a dispute.\n\n" + disputeInfo), + p2PService.getAddress()); + disputeMailMessage.setIsSystemMessage(true); + dispute.addDisputeMessage(disputeMailMessage); + disputes.add(dispute); + disputesObservableList.add(dispute); + + // we mirrored dispute already! + Contract contract = dispute.getContract(); + PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing(); + Address peerAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerAddress() : contract.getSellerAddress(); + log.trace("sendPeerOpenedDisputeMessage to peerAddress " + peerAddress); + p2PService.sendEncryptedMailboxMessage(peerAddress, + peersPubKeyRing, + new PeerOpenedDisputeMessage(dispute, p2PService.getAddress()), + new SendMailboxMessageListener() { + @Override + public void onArrived() { + disputeMailMessage.setArrived(true); + } + + @Override + public void onStoredInMailbox() { + disputeMailMessage.setStoredInMailbox(true); + } + + @Override + public void onFault() { + log.error("sendEncryptedMessage failed"); + } + } + ); + } + + // traders send msg to the arbitrator or arbitrator to 1 trader (trader to trader is not allowed) + public DisputeMailMessage sendDisputeMailMessage(Dispute dispute, String text, ArrayList attachments) { + DisputeMailMessage disputeMailMessage = new DisputeMailMessage(dispute.getTradeId(), + dispute.getTraderPubKeyRing().hashCode(), + isTrader(dispute), + text, + p2PService.getAddress()); + disputeMailMessage.addAllAttachments(attachments); + PubKeyRing receiverPubKeyRing = null; + Address peerAddress = null; + if (isTrader(dispute)) { + dispute.addDisputeMessage(disputeMailMessage); + receiverPubKeyRing = dispute.getArbitratorPubKeyRing(); + peerAddress = dispute.getContract().arbitratorAddress; + } else if (isArbitrator(dispute)) { + if (!disputeMailMessage.isSystemMessage()) + dispute.addDisputeMessage(disputeMailMessage); + receiverPubKeyRing = dispute.getTraderPubKeyRing(); + Contract contract = dispute.getContract(); + if (contract.getBuyerPubKeyRing().equals(receiverPubKeyRing)) + peerAddress = contract.getBuyerAddress(); + else + peerAddress = contract.getSellerAddress(); + } else { + log.error("That must not happen. Trader cannot communicate to other trader."); + } + if (receiverPubKeyRing != null) { + log.trace("sendDisputeMailMessage to peerAddress " + peerAddress); + p2PService.sendEncryptedMailboxMessage(peerAddress, + receiverPubKeyRing, + disputeMailMessage, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + disputeMailMessage.setArrived(true); + } + + @Override + public void onStoredInMailbox() { + disputeMailMessage.setStoredInMailbox(true); + } + + @Override + public void onFault() { + log.error("sendEncryptedMessage failed"); + } + } + ); + } + + return disputeMailMessage; + } + + // arbitrator send result to trader + public void sendDisputeResultMessage(DisputeResult disputeResult, Dispute dispute, String text) { + DisputeMailMessage disputeMailMessage = new DisputeMailMessage(dispute.getTradeId(), + dispute.getTraderPubKeyRing().hashCode(), + false, + text, + p2PService.getAddress()); + dispute.addDisputeMessage(disputeMailMessage); + disputeResult.setResultMailMessage(disputeMailMessage); + + Address peerAddress; + Contract contract = dispute.getContract(); + if (contract.getBuyerPubKeyRing().equals(dispute.getTraderPubKeyRing())) + peerAddress = contract.getBuyerAddress(); + else + peerAddress = contract.getSellerAddress(); + p2PService.sendEncryptedMailboxMessage(peerAddress, + dispute.getTraderPubKeyRing(), + new DisputeResultMessage(disputeResult, p2PService.getAddress()), + new SendMailboxMessageListener() { + @Override + public void onArrived() { + disputeMailMessage.setArrived(true); + } + + @Override + public void onStoredInMailbox() { + disputeMailMessage.setStoredInMailbox(true); + } + + @Override + public void onFault() { + log.error("sendEncryptedMessage failed"); + } + } + ); + } + + // winner (or buyer in case of 50/50) sends tx to other peer + private void sendPeerPublishedPayoutTxMessage(Transaction transaction, Dispute dispute, Contract contract) { + PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing(); + Address peerAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerAddress() : contract.getBuyerAddress(); + log.trace("sendPeerPublishedPayoutTxMessage to peerAddress " + peerAddress); + p2PService.sendEncryptedMailboxMessage(peerAddress, + peersPubKeyRing, + new PeerPublishedPayoutTxMessage(transaction.bitcoinSerialize(), dispute.getTradeId(), p2PService.getAddress()), + new SendMailboxMessageListener() { + @Override + public void onArrived() { + + } + + @Override + public void onStoredInMailbox() { + + } + + @Override + public void onFault() { + log.error("sendEncryptedMessage failed"); + } + } + ); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Incoming message + /////////////////////////////////////////////////////////////////////////////////////////// + + // arbitrator receives that from trader who opens dispute + private void onOpenNewDisputeMessage(OpenNewDisputeMessage openNewDisputeMessage) { + Dispute dispute = openNewDisputeMessage.dispute; + if (isArbitrator(dispute)) { + if (!disputes.contains(dispute)) { + dispute.setStorage(getDisputeStorage()); + disputes.add(dispute); + disputesObservableList.add(dispute); + sendPeerOpenedDisputeMessage(dispute); + } else { + log.warn("We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId()); + } + } else { + log.error("Trader received openNewDisputeMessage. That must never happen."); + } + } + + // not dispute requester receives that from arbitrator + private void onPeerOpenedDisputeMessage(PeerOpenedDisputeMessage peerOpenedDisputeMessage) { + Dispute dispute = peerOpenedDisputeMessage.dispute; + if (!isArbitrator(dispute)) { + Optional tradeOptional = tradeManager.getTradeById(dispute.getTradeId()); + if (tradeOptional.isPresent()) + tradeOptional.get().setDisputeState(Trade.DisputeState.DISPUTE_STARTED_BY_PEER); + + if (!disputes.contains(dispute)) { + dispute.setStorage(getDisputeStorage()); + disputes.add(dispute); + disputesObservableList.add(dispute); + } else { + log.warn("We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId()); + } + } else { + log.error("Arbitrator received peerOpenedDisputeMessage. That must never happen."); + } + } + + // a trader can receive a msg from the arbitrator or the arbitrator form a trader. Trader to trader is not allowed. + private void onDisputeMailMessage(DisputeMailMessage disputeMailMessage) { + log.debug("onDisputeMailMessage " + disputeMailMessage); + Optional disputeOptional = findDispute(disputeMailMessage.getTradeId(), disputeMailMessage.getTraderId()); + if (disputeOptional.isPresent()) { + Dispute dispute = disputeOptional.get(); + if (!dispute.getDisputeMailMessagesAsObservableList().contains(disputeMailMessage)) + dispute.addDisputeMessage(disputeMailMessage); + else + log.warn("We got a dispute mail msg what we have already stored. TradeId = " + disputeMailMessage.getTradeId()); + } else { + log.warn("We got a dispute mail msg but we don't have a matching dispute. TradeId = " + disputeMailMessage.getTradeId()); + } + } + + // We get that message at both peers. The dispute object is in context of the trader + private void onDisputeResultMessage(DisputeResultMessage disputeResultMessage) { + DisputeResult disputeResult = disputeResultMessage.disputeResult; + if (!isArbitrator(disputeResult)) { + Optional disputeOptional = findDispute(disputeResult.tradeId, disputeResult.traderId); + if (disputeOptional.isPresent()) { + Dispute dispute = disputeOptional.get(); + + DisputeMailMessage disputeMailMessage = disputeResult.getResultMailMessage(); + if (!dispute.getDisputeMailMessagesAsObservableList().contains(disputeMailMessage)) + dispute.addDisputeMessage(disputeMailMessage); + else + log.warn("We got a dispute mail msg what we have already stored. TradeId = " + disputeMailMessage.getTradeId()); + + dispute.setIsClosed(true); + if (tradeManager.getTradeById(dispute.getTradeId()).isPresent()) + tradeManager.closeDisputedTrade(dispute.getTradeId()); + else { + Optional openOfferOptional = openOfferManager.getOpenOfferById(dispute.getTradeId()); + if (openOfferOptional.isPresent()) + openOfferManager.closeOpenOffer(openOfferOptional.get().getOffer()); + } + + if (dispute.disputeResultProperty().get() == null) { + dispute.setDisputeResult(disputeResult); + + // We need to avoid publishing the tx from both traders as it would create problems with zero confirmation withdrawals + // There would be different transactions if both sign and publish (signers: once buyer+arb, once seller+arb) + // The tx publisher is the winner or in case both get 50% the buyer, as the buyer has more inventive to publish the tx as he receives + // more BTC as he has deposited + final Contract contract = dispute.getContract(); + + boolean isBuyer = keyRing.getPubKeyRing().equals(contract.getBuyerPubKeyRing()); + if ((isBuyer && disputeResult.getWinner() == DisputeResult.Winner.BUYER) + || (!isBuyer && disputeResult.getWinner() == DisputeResult.Winner.SELLER) + || (isBuyer && disputeResult.getWinner() == DisputeResult.Winner.STALE_MATE)) { + + if (dispute.getDepositTxSerialized() != null) { + try { + log.debug("do payout Transaction "); + + Transaction signedDisputedPayoutTx = tradeWalletService.signAndFinalizeDisputedPayoutTx( + dispute.getDepositTxSerialized(), + disputeResult.getArbitratorSignature(), + disputeResult.getBuyerPayoutAmount(), + disputeResult.getSellerPayoutAmount(), + disputeResult.getArbitratorPayoutAmount(), + contract.getBuyerPayoutAddressString(), + contract.getSellerPayoutAddressString(), + disputeResult.getArbitratorAddressAsString(), + walletService.getAddressEntryByOfferId(dispute.getTradeId()), + contract.getBuyerBtcPubKey(), + contract.getSellerBtcPubKey(), + disputeResult.getArbitratorPubKey() + ); + Transaction committedDisputedPayoutTx = tradeWalletService.addTransactionToWallet(signedDisputedPayoutTx); + log.debug("broadcast committedDisputedPayoutTx"); + tradeWalletService.broadcastTx(committedDisputedPayoutTx, new FutureCallback() { + @Override + public void onSuccess(Transaction transaction) { + log.debug("BroadcastTx succeeded. Transaction:" + transaction); + + // after successful publish we send peer the tx + + sendPeerPublishedPayoutTxMessage(transaction, dispute, contract); + } + + @Override + public void onFailure(@NotNull Throwable t) { + // TODO error handling + log.error(t.getMessage()); + } + }); + } catch (AddressFormatException | WalletException | TransactionVerificationException e) { + e.printStackTrace(); + } + } else { + log.warn("DepositTx is null. TradeId = " + disputeResult.tradeId); + } + } + } else { + log.warn("We got a dispute msg what we have already stored. TradeId = " + disputeResult.tradeId); + } + + /* DisputeMailMessage disputeMailMessage = disputeResult.getResultMailMessage(); + if (!dispute.getDisputeMailMessagesAsObservableList().contains(disputeMailMessage)) + dispute.addDisputeMessage(disputeMailMessage); + else + log.warn("We got a dispute mail msg what we have already stored. TradeId = " + disputeMailMessage.getTradeId());*/ + + } else { + log.warn("We got a dispute result msg but we don't have a matching dispute. TradeId = " + disputeResult.tradeId); + } + } else { + log.error("Arbitrator received disputeResultMessage. That must never happen."); + } + } + + // losing trader or in case of 50/50 the seller gets the tx sent from the winner or buyer + private void onDisputedPayoutTxMessage(PeerPublishedPayoutTxMessage peerPublishedPayoutTxMessage) { + tradeWalletService.addTransactionToWallet(peerPublishedPayoutTxMessage.transaction); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + public Storage> getDisputeStorage() { + return disputeStorage; + } + + public ObservableList getDisputesAsObservableList() { + return disputesObservableList; + } + + public boolean isTrader(Dispute dispute) { + return keyRing.getPubKeyRing().equals(dispute.getTraderPubKeyRing()); + } + + private boolean isArbitrator(Dispute dispute) { + return keyRing.getPubKeyRing().equals(dispute.getArbitratorPubKeyRing()); + } + + private boolean isArbitrator(DisputeResult disputeResult) { + return walletService.getArbitratorAddressEntry().getAddressString().equals(disputeResult.getArbitratorAddressAsString()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////////////////////// + + private Optional findDispute(String tradeId, int traderId) { + return disputes.stream().filter(e -> e.getTradeId().equals(tradeId) && e.getTraderId() == traderId).findFirst(); + } + + public Optional findOwnDispute(String tradeId) { + return disputes.stream().filter(e -> e.getTradeId().equals(tradeId)).findFirst(); + } + + public List findDisputesByTradeId(String tradeId) { + return disputes.stream().filter(e -> e.getTradeId().equals(tradeId)).collect(Collectors.toList()); + } + +} diff --git a/core/src/main/java/io/bitsquare/arbitration/DisputeResult.java b/core/src/main/java/io/bitsquare/arbitration/DisputeResult.java new file mode 100644 index 0000000000..85f50a35d9 --- /dev/null +++ b/core/src/main/java/io/bitsquare/arbitration/DisputeResult.java @@ -0,0 +1,265 @@ +/* + * 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 io.bitsquare.app.Version; +import io.bitsquare.arbitration.messages.DisputeMailMessage; +import javafx.beans.property.*; +import org.bitcoinj.core.Coin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.util.Arrays; +import java.util.Date; + +public class DisputeResult implements Serializable { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + transient private static final Logger log = LoggerFactory.getLogger(DisputeResult.class); + + public enum FeePaymentPolicy { + LOSER, + SPLIT, + WAIVE + } + + public enum Winner { + BUYER, + SELLER, + STALE_MATE + } + + public final String tradeId; + public final int traderId; + private FeePaymentPolicy feePaymentPolicy; + + private boolean tamperProofEvidence; + private boolean idVerification; + private boolean screenCast; + private String summaryNotes; + private DisputeMailMessage resultMailMessage; + private byte[] arbitratorSignature; + private long buyerPayoutAmount; + private long sellerPayoutAmount; + private long arbitratorPayoutAmount; + private String arbitratorAddressAsString; + private byte[] arbitratorPubKey; + private long closeDate; + private Winner winner; + + transient private BooleanProperty tamperProofEvidenceProperty = new SimpleBooleanProperty(); + transient private BooleanProperty idVerificationProperty = new SimpleBooleanProperty(); + transient private BooleanProperty screenCastProperty = new SimpleBooleanProperty(); + transient private ObjectProperty feePaymentPolicyProperty = new SimpleObjectProperty<>(); + transient private StringProperty summaryNotesProperty = new SimpleStringProperty(); + + public DisputeResult(String tradeId, int traderId) { + this.tradeId = tradeId; + this.traderId = traderId; + + feePaymentPolicy = FeePaymentPolicy.LOSER; + init(); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + try { + in.defaultReadObject(); + init(); + } catch (Throwable t) { + log.trace("Cannot be deserialized." + t.getMessage()); + } + } + + private void init() { + tamperProofEvidenceProperty = new SimpleBooleanProperty(tamperProofEvidence); + idVerificationProperty = new SimpleBooleanProperty(idVerification); + screenCastProperty = new SimpleBooleanProperty(screenCast); + feePaymentPolicyProperty = new SimpleObjectProperty<>(feePaymentPolicy); + summaryNotesProperty = new SimpleStringProperty(summaryNotes); + + tamperProofEvidenceProperty.addListener((observable, oldValue, newValue) -> { + tamperProofEvidence = newValue; + }); + idVerificationProperty.addListener((observable, oldValue, newValue) -> { + idVerification = newValue; + }); + screenCastProperty.addListener((observable, oldValue, newValue) -> { + screenCast = newValue; + }); + feePaymentPolicyProperty.addListener((observable, oldValue, newValue) -> { + feePaymentPolicy = newValue; + }); + summaryNotesProperty.addListener((observable, oldValue, newValue) -> { + summaryNotes = newValue; + }); + } + + public BooleanProperty tamperProofEvidenceProperty() { + return tamperProofEvidenceProperty; + } + + public BooleanProperty idVerificationProperty() { + return idVerificationProperty; + } + + public BooleanProperty screenCastProperty() { + return screenCastProperty; + } + + public void setFeePaymentPolicy(FeePaymentPolicy feePaymentPolicy) { + this.feePaymentPolicy = feePaymentPolicy; + feePaymentPolicyProperty.set(feePaymentPolicy); + } + + public ReadOnlyObjectProperty feePaymentPolicyProperty() { + return feePaymentPolicyProperty; + } + + public FeePaymentPolicy getFeePaymentPolicy() { + return feePaymentPolicy; + } + + + public StringProperty summaryNotesProperty() { + return summaryNotesProperty; + } + + public void setResultMailMessage(DisputeMailMessage resultMailMessage) { + this.resultMailMessage = resultMailMessage; + } + + public DisputeMailMessage getResultMailMessage() { + return resultMailMessage; + } + + public void setArbitratorSignature(byte[] arbitratorSignature) { + this.arbitratorSignature = arbitratorSignature; + } + + public byte[] getArbitratorSignature() { + return arbitratorSignature; + } + + public void setBuyerPayoutAmount(Coin buyerPayoutAmount) { + this.buyerPayoutAmount = buyerPayoutAmount.value; + } + + public Coin getBuyerPayoutAmount() { + return Coin.valueOf(buyerPayoutAmount); + } + + public void setSellerPayoutAmount(Coin sellerPayoutAmount) { + this.sellerPayoutAmount = sellerPayoutAmount.value; + } + + public Coin getSellerPayoutAmount() { + return Coin.valueOf(sellerPayoutAmount); + } + + public void setArbitratorPayoutAmount(Coin arbitratorPayoutAmount) { + this.arbitratorPayoutAmount = arbitratorPayoutAmount.value; + } + + public Coin getArbitratorPayoutAmount() { + return Coin.valueOf(arbitratorPayoutAmount); + } + + public void setArbitratorAddressAsString(String arbitratorAddressAsString) { + this.arbitratorAddressAsString = arbitratorAddressAsString; + } + + public String getArbitratorAddressAsString() { + return arbitratorAddressAsString; + } + + public void setArbitratorPubKey(byte[] arbitratorPubKey) { + this.arbitratorPubKey = arbitratorPubKey; + } + + public byte[] getArbitratorPubKey() { + return arbitratorPubKey; + } + + public void setCloseDate(Date closeDate) { + this.closeDate = closeDate.getTime(); + } + + public Date getCloseDate() { + return new Date(closeDate); + } + + public void setWinner(Winner winner) { + this.winner = winner; + } + + public Winner getWinner() { + return winner; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof DisputeResult)) return false; + + DisputeResult that = (DisputeResult) o; + + if (traderId != that.traderId) return false; + if (tamperProofEvidence != that.tamperProofEvidence) return false; + if (idVerification != that.idVerification) return false; + if (screenCast != that.screenCast) return false; + if (buyerPayoutAmount != that.buyerPayoutAmount) return false; + if (sellerPayoutAmount != that.sellerPayoutAmount) return false; + if (arbitratorPayoutAmount != that.arbitratorPayoutAmount) return false; + if (closeDate != that.closeDate) return false; + if (tradeId != null ? !tradeId.equals(that.tradeId) : that.tradeId != null) return false; + if (feePaymentPolicy != that.feePaymentPolicy) return false; + if (summaryNotes != null ? !summaryNotes.equals(that.summaryNotes) : that.summaryNotes != null) return false; + if (resultMailMessage != null ? !resultMailMessage.equals(that.resultMailMessage) : that.resultMailMessage != null) + return false; + if (!Arrays.equals(arbitratorSignature, that.arbitratorSignature)) return false; + if (arbitratorAddressAsString != null ? !arbitratorAddressAsString.equals(that.arbitratorAddressAsString) : that.arbitratorAddressAsString != null) + return false; + if (!Arrays.equals(arbitratorPubKey, that.arbitratorPubKey)) return false; + return winner == that.winner; + + } + + @Override + public int hashCode() { + int result = tradeId != null ? tradeId.hashCode() : 0; + result = 31 * result + traderId; + result = 31 * result + (feePaymentPolicy != null ? feePaymentPolicy.hashCode() : 0); + result = 31 * result + (tamperProofEvidence ? 1 : 0); + result = 31 * result + (idVerification ? 1 : 0); + result = 31 * result + (screenCast ? 1 : 0); + result = 31 * result + (summaryNotes != null ? summaryNotes.hashCode() : 0); + result = 31 * result + (resultMailMessage != null ? resultMailMessage.hashCode() : 0); + result = 31 * result + (arbitratorSignature != null ? Arrays.hashCode(arbitratorSignature) : 0); + result = 31 * result + (int) (buyerPayoutAmount ^ (buyerPayoutAmount >>> 32)); + result = 31 * result + (int) (sellerPayoutAmount ^ (sellerPayoutAmount >>> 32)); + result = 31 * result + (int) (arbitratorPayoutAmount ^ (arbitratorPayoutAmount >>> 32)); + result = 31 * result + (arbitratorAddressAsString != null ? arbitratorAddressAsString.hashCode() : 0); + result = 31 * result + (arbitratorPubKey != null ? Arrays.hashCode(arbitratorPubKey) : 0); + result = 31 * result + (int) (closeDate ^ (closeDate >>> 32)); + result = 31 * result + (winner != null ? winner.hashCode() : 0); + return result; + } +} diff --git a/core/src/main/java/io/bitsquare/arbitration/messages/DisputeMailMessage.java b/core/src/main/java/io/bitsquare/arbitration/messages/DisputeMailMessage.java new file mode 100644 index 0000000000..e66fca6d7c --- /dev/null +++ b/core/src/main/java/io/bitsquare/arbitration/messages/DisputeMailMessage.java @@ -0,0 +1,237 @@ +/* + * 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.messages; + +import io.bitsquare.app.Version; +import io.bitsquare.p2p.Address; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +public class DisputeMailMessage implements DisputeMessage { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + transient private static final Logger log = LoggerFactory.getLogger(DisputeMailMessage.class); + + private final long date; + private final String tradeId; + + private final int traderId; + private final boolean senderIsTrader; + private final String message; + private final List attachments = new ArrayList<>(); + private boolean arrived; + private boolean storedInMailbox; + private boolean isSystemMessage; + private final Address myAddress; + + transient private BooleanProperty arrivedProperty = new SimpleBooleanProperty(); + transient private BooleanProperty storedInMailboxProperty = new SimpleBooleanProperty(); + + public DisputeMailMessage(String tradeId, int traderId, boolean senderIsTrader, String message, Address myAddress) { + this.tradeId = tradeId; + this.traderId = traderId; + this.senderIsTrader = senderIsTrader; + this.message = message; + this.myAddress = myAddress; + date = new Date().getTime(); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + try { + in.defaultReadObject(); + arrivedProperty = new SimpleBooleanProperty(arrived); + storedInMailboxProperty = new SimpleBooleanProperty(storedInMailbox); + } catch (Throwable t) { + log.trace("Cannot be deserialized." + t.getMessage()); + } + } + + @Override + public Address getSenderAddress() { + return myAddress; + } + + public void addAttachment(Attachment attachment) { + attachments.add(attachment); + } + + public void addAllAttachments(List attachments) { + this.attachments.addAll(attachments); + } + + public void setArrived(boolean arrived) { + this.arrived = arrived; + this.arrivedProperty.set(arrived); + } + + public void setStoredInMailbox(boolean storedInMailbox) { + this.storedInMailbox = storedInMailbox; + this.storedInMailboxProperty.set(storedInMailbox); + } + + public Date getDate() { + return new Date(date); + } + + public boolean isSenderIsTrader() { + return senderIsTrader; + } + + public String getMessage() { + return message; + } + + public int getTraderId() { + return traderId; + } + + public BooleanProperty arrivedProperty() { + return arrivedProperty; + } + + public BooleanProperty storedInMailboxProperty() { + return storedInMailboxProperty; + } + + public List getAttachments() { + return attachments; + } + + public String getTradeId() { + return tradeId; + } + + public boolean isSystemMessage() { + return isSystemMessage; + } + + public void setIsSystemMessage(boolean isSystemMessage) { + this.isSystemMessage = isSystemMessage; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof DisputeMailMessage)) return false; + + DisputeMailMessage that = (DisputeMailMessage) o; + + if (date != that.date) return false; + if (traderId != that.traderId) return false; + if (senderIsTrader != that.senderIsTrader) return false; + if (arrived != that.arrived) return false; + if (storedInMailbox != that.storedInMailbox) return false; + if (isSystemMessage != that.isSystemMessage) return false; + if (tradeId != null ? !tradeId.equals(that.tradeId) : that.tradeId != null) return false; + if (message != null ? !message.equals(that.message) : that.message != null) return false; + if (attachments != null ? !attachments.equals(that.attachments) : that.attachments != null) return false; + return !(myAddress != null ? !myAddress.equals(that.myAddress) : that.myAddress != null); + + } + + @Override + public int hashCode() { + int result = (int) (date ^ (date >>> 32)); + result = 31 * result + (tradeId != null ? tradeId.hashCode() : 0); + result = 31 * result + traderId; + result = 31 * result + (senderIsTrader ? 1 : 0); + result = 31 * result + (message != null ? message.hashCode() : 0); + result = 31 * result + (attachments != null ? attachments.hashCode() : 0); + result = 31 * result + (arrived ? 1 : 0); + result = 31 * result + (storedInMailbox ? 1 : 0); + result = 31 * result + (isSystemMessage ? 1 : 0); + result = 31 * result + (myAddress != null ? myAddress.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "DisputeMailMessage{" + + "date=" + date + + ", tradeId='" + tradeId + '\'' + + ", traderId='" + traderId + '\'' + + ", senderIsTrader=" + senderIsTrader + + ", message='" + message + '\'' + + ", attachments=" + attachments + + '}'; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Static classes + /////////////////////////////////////////////////////////////////////////////////////////// + + public static class Attachment implements Serializable { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + transient private static final Logger log = LoggerFactory.getLogger(Attachment.class); + + private final byte[] bytes; + private final String fileName; + + public Attachment(String fileName, byte[] bytes) { + this.fileName = fileName; + this.bytes = bytes; + } + + public byte[] getBytes() { + return bytes; + } + + public String getFileName() { + return fileName; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Attachment)) return false; + + Attachment that = (Attachment) o; + + if (!Arrays.equals(bytes, that.bytes)) return false; + return !(fileName != null ? !fileName.equals(that.fileName) : that.fileName != null); + + } + + @Override + public int hashCode() { + int result = bytes != null ? Arrays.hashCode(bytes) : 0; + result = 31 * result + (fileName != null ? fileName.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "Attachment{" + + "description=" + fileName + + ", data=" + Arrays.toString(bytes) + + '}'; + } + } +} diff --git a/core/src/main/java/io/bitsquare/arbitration/messages/DisputeMessage.java b/core/src/main/java/io/bitsquare/arbitration/messages/DisputeMessage.java new file mode 100644 index 0000000000..e676af9f3b --- /dev/null +++ b/core/src/main/java/io/bitsquare/arbitration/messages/DisputeMessage.java @@ -0,0 +1,23 @@ +/* + * 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.messages; + +import io.bitsquare.p2p.messaging.MailboxMessage; + +public interface DisputeMessage extends MailboxMessage { +} diff --git a/core/src/main/java/io/bitsquare/arbitration/messages/DisputeResultMessage.java b/core/src/main/java/io/bitsquare/arbitration/messages/DisputeResultMessage.java new file mode 100644 index 0000000000..b111020bce --- /dev/null +++ b/core/src/main/java/io/bitsquare/arbitration/messages/DisputeResultMessage.java @@ -0,0 +1,60 @@ +/* + * 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.messages; + +import io.bitsquare.app.Version; +import io.bitsquare.arbitration.DisputeResult; +import io.bitsquare.p2p.Address; + +public class DisputeResultMessage implements DisputeMessage { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + + public final DisputeResult disputeResult; + private final Address myAddress; + + public DisputeResultMessage(DisputeResult disputeResult, Address myAddress) { + this.disputeResult = disputeResult; + this.myAddress = myAddress; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof DisputeResultMessage)) return false; + + DisputeResultMessage that = (DisputeResultMessage) o; + + if (disputeResult != null ? !disputeResult.equals(that.disputeResult) : that.disputeResult != null) + return false; + return !(myAddress != null ? !myAddress.equals(that.myAddress) : that.myAddress != null); + + } + + @Override + public int hashCode() { + int result = disputeResult != null ? disputeResult.hashCode() : 0; + result = 31 * result + (myAddress != null ? myAddress.hashCode() : 0); + return result; + } + + @Override + public Address getSenderAddress() { + return myAddress; + } +} diff --git a/core/src/main/java/io/bitsquare/arbitration/messages/OpenNewDisputeMessage.java b/core/src/main/java/io/bitsquare/arbitration/messages/OpenNewDisputeMessage.java new file mode 100644 index 0000000000..73545d3e48 --- /dev/null +++ b/core/src/main/java/io/bitsquare/arbitration/messages/OpenNewDisputeMessage.java @@ -0,0 +1,59 @@ +/* + * 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.messages; + +import io.bitsquare.app.Version; +import io.bitsquare.arbitration.Dispute; +import io.bitsquare.p2p.Address; + +public class OpenNewDisputeMessage implements DisputeMessage { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + + public final Dispute dispute; + private final Address myAddress; + + public OpenNewDisputeMessage(Dispute dispute, Address myAddress) { + this.dispute = dispute; + this.myAddress = myAddress; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof OpenNewDisputeMessage)) return false; + + OpenNewDisputeMessage that = (OpenNewDisputeMessage) o; + + if (dispute != null ? !dispute.equals(that.dispute) : that.dispute != null) return false; + return !(myAddress != null ? !myAddress.equals(that.myAddress) : that.myAddress != null); + + } + + @Override + public int hashCode() { + int result = dispute != null ? dispute.hashCode() : 0; + result = 31 * result + (myAddress != null ? myAddress.hashCode() : 0); + return result; + } + + @Override + public Address getSenderAddress() { + return myAddress; + } +} diff --git a/core/src/main/java/io/bitsquare/arbitration/messages/PeerOpenedDisputeMessage.java b/core/src/main/java/io/bitsquare/arbitration/messages/PeerOpenedDisputeMessage.java new file mode 100644 index 0000000000..36ec20c33f --- /dev/null +++ b/core/src/main/java/io/bitsquare/arbitration/messages/PeerOpenedDisputeMessage.java @@ -0,0 +1,58 @@ +/* + * 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.messages; + +import io.bitsquare.app.Version; +import io.bitsquare.arbitration.Dispute; +import io.bitsquare.p2p.Address; + +public class PeerOpenedDisputeMessage implements DisputeMessage { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + public final Dispute dispute; + private final Address myAddress; + + public PeerOpenedDisputeMessage(Dispute dispute, Address myAddress) { + this.dispute = dispute; + this.myAddress = myAddress; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PeerOpenedDisputeMessage)) return false; + + PeerOpenedDisputeMessage that = (PeerOpenedDisputeMessage) o; + + if (dispute != null ? !dispute.equals(that.dispute) : that.dispute != null) return false; + return !(myAddress != null ? !myAddress.equals(that.myAddress) : that.myAddress != null); + + } + + @Override + public int hashCode() { + int result = dispute != null ? dispute.hashCode() : 0; + result = 31 * result + (myAddress != null ? myAddress.hashCode() : 0); + return result; + } + + @Override + public Address getSenderAddress() { + return myAddress; + } +} diff --git a/core/src/main/java/io/bitsquare/arbitration/messages/PeerPublishedPayoutTxMessage.java b/core/src/main/java/io/bitsquare/arbitration/messages/PeerPublishedPayoutTxMessage.java new file mode 100644 index 0000000000..be32f2cdad --- /dev/null +++ b/core/src/main/java/io/bitsquare/arbitration/messages/PeerPublishedPayoutTxMessage.java @@ -0,0 +1,65 @@ +/* + * 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.messages; + +import io.bitsquare.app.Version; +import io.bitsquare.p2p.Address; + +import java.util.Arrays; + +public class PeerPublishedPayoutTxMessage implements DisputeMessage { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + + public final byte[] transaction; + public final String tradeId; + private final Address myAddress; + + public PeerPublishedPayoutTxMessage(byte[] transaction, String tradeId, Address myAddress) { + this.transaction = transaction; + this.tradeId = tradeId; + this.myAddress = myAddress; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PeerPublishedPayoutTxMessage)) return false; + + PeerPublishedPayoutTxMessage that = (PeerPublishedPayoutTxMessage) o; + + if (!Arrays.equals(transaction, that.transaction)) return false; + if (tradeId != null ? !tradeId.equals(that.tradeId) : that.tradeId != null) return false; + return !(myAddress != null ? !myAddress.equals(that.myAddress) : that.myAddress != null); + + } + + @Override + public int hashCode() { + int result = transaction != null ? Arrays.hashCode(transaction) : 0; + result = 31 * result + (tradeId != null ? tradeId.hashCode() : 0); + result = 31 * result + (myAddress != null ? myAddress.hashCode() : 0); + return result; + } + + @Override + public Address getSenderAddress() { + return myAddress; + } + +} diff --git a/core/src/main/java/io/bitsquare/btc/data/InputsAndChangeOutput.java b/core/src/main/java/io/bitsquare/btc/data/InputsAndChangeOutput.java new file mode 100644 index 0000000000..f171d7d7a5 --- /dev/null +++ b/core/src/main/java/io/bitsquare/btc/data/InputsAndChangeOutput.java @@ -0,0 +1,40 @@ +/* + * 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.data; + +import javax.annotation.Nullable; +import java.util.List; + +import static com.google.common.base.Preconditions.checkArgument; + +public class InputsAndChangeOutput { + public final List rawInputs; + + // Is set to 0L in case we don't have an output + public final long changeOutputValue; + @Nullable + public final String changeOutputAddress; + + public InputsAndChangeOutput(List rawInputs, long changeOutputValue, @Nullable String changeOutputAddress) { + checkArgument(!rawInputs.isEmpty(), "rawInputs.isEmpty()"); + + this.rawInputs = rawInputs; + this.changeOutputValue = changeOutputValue; + this.changeOutputAddress = changeOutputAddress; + } +} diff --git a/core/src/main/java/io/bitsquare/btc/data/PreparedDepositTxAndOffererInputs.java b/core/src/main/java/io/bitsquare/btc/data/PreparedDepositTxAndOffererInputs.java new file mode 100644 index 0000000000..3e6e00ab6a --- /dev/null +++ b/core/src/main/java/io/bitsquare/btc/data/PreparedDepositTxAndOffererInputs.java @@ -0,0 +1,30 @@ +/* + * 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.data; + +import java.util.List; + +public class PreparedDepositTxAndOffererInputs { + public final List rawOffererInputs; + public final byte[] depositTransaction; + + public PreparedDepositTxAndOffererInputs(List rawOffererInputs, byte[] depositTransaction) { + this.rawOffererInputs = rawOffererInputs; + this.depositTransaction = depositTransaction; + } +} diff --git a/core/src/main/java/io/bitsquare/btc/data/RawInput.java b/core/src/main/java/io/bitsquare/btc/data/RawInput.java new file mode 100644 index 0000000000..b4da3d320d --- /dev/null +++ b/core/src/main/java/io/bitsquare/btc/data/RawInput.java @@ -0,0 +1,59 @@ +/* + * 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.data; + +import io.bitsquare.app.Version; + +import java.io.Serializable; +import java.util.Arrays; + +public class RawInput implements Serializable { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + + public final long index; + public final byte[] parentTransaction; + public final long value; + + public RawInput(long index, byte[] parentTransaction, long value) { + this.index = index; + this.parentTransaction = parentTransaction; + this.value = value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof RawInput)) return false; + + RawInput rawInput = (RawInput) o; + + if (index != rawInput.index) return false; + if (value != rawInput.value) return false; + return Arrays.equals(parentTransaction, rawInput.parentTransaction); + + } + + @Override + public int hashCode() { + int result = (int) (index ^ (index >>> 32)); + result = 31 * result + (parentTransaction != null ? Arrays.hashCode(parentTransaction) : 0); + result = 31 * result + (int) (value ^ (value >>> 32)); + return result; + } +} diff --git a/core/src/main/java/io/bitsquare/locale/CryptoCurrency.java b/core/src/main/java/io/bitsquare/locale/CryptoCurrency.java new file mode 100644 index 0000000000..436813ad52 --- /dev/null +++ b/core/src/main/java/io/bitsquare/locale/CryptoCurrency.java @@ -0,0 +1,36 @@ +/* + * 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.locale; + +import io.bitsquare.app.Version; + +import java.io.Serializable; + +public class CryptoCurrency extends TradeCurrency implements Serializable { + // That object is saved to disc. We need to take care of changes to not break deserialization. + private static final long serialVersionUID = Version.LOCAL_DB_VERSION; + + public CryptoCurrency(String currencyCode, String name) { + super(currencyCode, name); + } + + public CryptoCurrency(String currencyCode, String name, String symbol) { + super(currencyCode, name, symbol); + } + +} diff --git a/core/src/main/java/io/bitsquare/locale/FiatCurrency.java b/core/src/main/java/io/bitsquare/locale/FiatCurrency.java new file mode 100644 index 0000000000..2c59b70600 --- /dev/null +++ b/core/src/main/java/io/bitsquare/locale/FiatCurrency.java @@ -0,0 +1,54 @@ +/* + * 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.locale; + +import io.bitsquare.app.Version; +import io.bitsquare.user.Preferences; + +import java.io.Serializable; +import java.util.Currency; + +public class FiatCurrency extends TradeCurrency implements Serializable { + // That object is saved to disc. We need to take care of changes to not break deserialization. + private static final long serialVersionUID = Version.LOCAL_DB_VERSION; + + private final Currency currency; + + public FiatCurrency(String currencyCode) { + this(Currency.getInstance(currencyCode)); + } + + public FiatCurrency(Currency currency) { + super(currency.getCurrencyCode(), currency.getDisplayName(Preferences.getDefaultLocale()), currency.getSymbol()); + this.currency = currency; + } + + public Currency getCurrency() { + return currency; + } + + @Override + public String toString() { + return "FiatCurrency{" + + "currency=" + currency + + ", code='" + code + '\'' + + ", name='" + name + '\'' + + ", symbol='" + symbol + '\'' + + '}'; + } +} diff --git a/core/src/main/java/io/bitsquare/locale/TradeCurrency.java b/core/src/main/java/io/bitsquare/locale/TradeCurrency.java new file mode 100644 index 0000000000..4bd0c07df9 --- /dev/null +++ b/core/src/main/java/io/bitsquare/locale/TradeCurrency.java @@ -0,0 +1,89 @@ +/* + * 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.locale; + +import io.bitsquare.app.Version; + +import java.io.Serializable; + +public class TradeCurrency implements Serializable { + // That object is saved to disc. We need to take care of changes to not break deserialization. + private static final long serialVersionUID = Version.LOCAL_DB_VERSION; + + protected final String code; + protected final String name; + protected String symbol; + + + public TradeCurrency(String code) { + this.code = code; + this.name = CurrencyUtil.getNameByCode(code); + } + + protected TradeCurrency(String code, String name) { + this.code = code; + this.name = name; + } + + public TradeCurrency(String code, String name, String symbol) { + this.code = code; + this.name = name; + this.symbol = symbol; + } + + public String getCode() { + return code; + } + + public String getName() { + return name; + } + + public String getSymbol() { + return symbol; + } + + public String getCodeAndName() { + return code + " (" + name + ")"; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof TradeCurrency)) return false; + + TradeCurrency that = (TradeCurrency) o; + + return !(getCode() != null ? !getCode().equals(that.getCode()) : that.getCode() != null); + + } + + @Override + public int hashCode() { + return getCode() != null ? getCode().hashCode() : 0; + } + + @Override + public String toString() { + return "TradeCurrency{" + + "code='" + code + '\'' + + ", name='" + name + '\'' + + ", symbol='" + symbol + '\'' + + '}'; + } +} diff --git a/core/src/main/java/io/bitsquare/payment/AliPayAccount.java b/core/src/main/java/io/bitsquare/payment/AliPayAccount.java new file mode 100644 index 0000000000..af6e35fd37 --- /dev/null +++ b/core/src/main/java/io/bitsquare/payment/AliPayAccount.java @@ -0,0 +1,43 @@ +/* + * 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.payment; + +import io.bitsquare.app.Version; +import io.bitsquare.locale.FiatCurrency; + +import java.io.Serializable; + +public class AliPayAccount extends PaymentAccount implements Serializable { + // That object is saved to disc. We need to take care of changes to not break deserialization. + private static final long serialVersionUID = Version.LOCAL_DB_VERSION; + + public AliPayAccount() { + super(PaymentMethod.ALI_PAY); + setSingleTradeCurrency(new FiatCurrency("CNY")); + + contractData = new AliPayAccountContractData(paymentMethod.getId(), id, paymentMethod.getMaxTradePeriod()); + } + + public void setAccountNr(String accountNr) { + ((AliPayAccountContractData) contractData).setAccountNr(accountNr); + } + + public String getAccountNr() { + return ((AliPayAccountContractData) contractData).getAccountNr(); + } +} diff --git a/core/src/main/java/io/bitsquare/payment/AliPayAccountContractData.java b/core/src/main/java/io/bitsquare/payment/AliPayAccountContractData.java new file mode 100644 index 0000000000..d6de629953 --- /dev/null +++ b/core/src/main/java/io/bitsquare/payment/AliPayAccountContractData.java @@ -0,0 +1,53 @@ +/* + * 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.payment; + +import io.bitsquare.app.Version; + +import java.io.Serializable; + +public class AliPayAccountContractData extends PaymentAccountContractData implements Serializable { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + + private String accountNr; + + public AliPayAccountContractData(String paymentMethod, String id, int maxTradePeriod) { + super(paymentMethod, id, maxTradePeriod); + } + + public void setAccountNr(String accountNr) { + this.accountNr = accountNr; + } + + public String getAccountNr() { + return accountNr; + } + + @Override + public String getPaymentDetails() { + return "AliPay - Account nr.: " + accountNr; + } + + @Override + public String toString() { + return "AliPayAccountContractData{" + + "accountNr='" + accountNr + '\'' + + '}'; + } +} diff --git a/core/src/main/java/io/bitsquare/payment/BlockChainAccount.java b/core/src/main/java/io/bitsquare/payment/BlockChainAccount.java new file mode 100644 index 0000000000..d39c4e4de4 --- /dev/null +++ b/core/src/main/java/io/bitsquare/payment/BlockChainAccount.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.payment; + +import io.bitsquare.app.Version; + +import java.io.Serializable; + +public class BlockChainAccount extends PaymentAccount implements Serializable { + // That object is saved to disc. We need to take care of changes to not break deserialization. + private static final long serialVersionUID = Version.LOCAL_DB_VERSION; + + + public BlockChainAccount() { + super(PaymentMethod.BLOCK_CHAINS); + + contractData = new BlockChainAccountContractData(paymentMethod.getId(), id, paymentMethod.getMaxTradePeriod()); + } + + public void setAddress(String address) { + ((BlockChainAccountContractData) contractData).setAddress(address); + } + + public String getAddress() { + return ((BlockChainAccountContractData) contractData).getAddress(); + } +} diff --git a/core/src/main/java/io/bitsquare/payment/BlockChainAccountContractData.java b/core/src/main/java/io/bitsquare/payment/BlockChainAccountContractData.java new file mode 100644 index 0000000000..37435e162b --- /dev/null +++ b/core/src/main/java/io/bitsquare/payment/BlockChainAccountContractData.java @@ -0,0 +1,55 @@ +/* + * 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.payment; + +import io.bitsquare.app.Version; + +import java.io.Serializable; + +public class BlockChainAccountContractData extends PaymentAccountContractData implements Serializable { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + + private String address; + private String paymentId; + + public BlockChainAccountContractData(String paymentMethod, String id, int maxTradePeriod) { + super(paymentMethod, id, maxTradePeriod); + } + + public void setAddress(String address) { + this.address = address; + } + + public String getAddress() { + return address; + } + + @Override + public String getPaymentDetails() { + return "Address: " + address; + } + + public void setPaymentId(String paymentId) { + this.paymentId = paymentId; + } + + public String getPaymentId() { + return paymentId; + } +} diff --git a/core/src/main/java/io/bitsquare/payment/OKPayAccount.java b/core/src/main/java/io/bitsquare/payment/OKPayAccount.java new file mode 100644 index 0000000000..08eadf4d14 --- /dev/null +++ b/core/src/main/java/io/bitsquare/payment/OKPayAccount.java @@ -0,0 +1,46 @@ +/* + * 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.payment; + +import io.bitsquare.app.Version; +import io.bitsquare.locale.CurrencyUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Serializable; + +public class OKPayAccount extends PaymentAccount implements Serializable { + // That object is saved to disc. We need to take care of changes to not break deserialization. + private static final long serialVersionUID = Version.LOCAL_DB_VERSION; + + private static final Logger log = LoggerFactory.getLogger(OKPayAccount.class); + + public OKPayAccount() { + super(PaymentMethod.OK_PAY); + tradeCurrencies.addAll(CurrencyUtil.getAllOKPayCurrencies()); + contractData = new OKPayAccountContractData(paymentMethod.getId(), id, paymentMethod.getMaxTradePeriod()); + } + + public void setAccountNr(String accountNr) { + ((OKPayAccountContractData) contractData).setAccountNr(accountNr); + } + + public String getAccountNr() { + return ((OKPayAccountContractData) contractData).getAccountNr(); + } +} diff --git a/core/src/main/java/io/bitsquare/payment/OKPayAccountContractData.java b/core/src/main/java/io/bitsquare/payment/OKPayAccountContractData.java new file mode 100644 index 0000000000..8f111c97e9 --- /dev/null +++ b/core/src/main/java/io/bitsquare/payment/OKPayAccountContractData.java @@ -0,0 +1,48 @@ +/* + * 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.payment; + +import io.bitsquare.app.Version; + +import java.io.Serializable; + +public class OKPayAccountContractData extends PaymentAccountContractData implements Serializable { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + + private String accountNr; + + public OKPayAccountContractData(String paymentMethod, String id, int maxTradePeriod) { + super(paymentMethod, id, maxTradePeriod); + } + + public void setAccountNr(String accountNr) { + this.accountNr = accountNr; + } + + public String getAccountNr() { + return accountNr; + } + + @Override + public String getPaymentDetails() { + return "OKPay - Account nr.: " + accountNr; + } + + +} diff --git a/core/src/main/java/io/bitsquare/payment/PaymentAccount.java b/core/src/main/java/io/bitsquare/payment/PaymentAccount.java new file mode 100644 index 0000000000..ee9f68efcd --- /dev/null +++ b/core/src/main/java/io/bitsquare/payment/PaymentAccount.java @@ -0,0 +1,168 @@ +/* + * 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.payment; + +import io.bitsquare.app.Version; +import io.bitsquare.locale.Country; +import io.bitsquare.locale.TradeCurrency; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class PaymentAccount implements Serializable { + // That object is saved to disc. We need to take care of changes to not break deserialization. + private static final long serialVersionUID = Version.LOCAL_DB_VERSION; + + private static final Logger log = LoggerFactory.getLogger(PaymentAccount.class); + + protected final String id; + protected final PaymentMethod paymentMethod; + protected String accountName; + protected final List tradeCurrencies = new ArrayList<>(); + protected TradeCurrency selectedTradeCurrency; + @Nullable + protected Country country = null; + protected PaymentAccountContractData contractData; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + + public PaymentAccount(PaymentMethod paymentMethod) { + this.paymentMethod = paymentMethod; + id = UUID.randomUUID().toString(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addCurrency(TradeCurrency tradeCurrency) { + if (!tradeCurrencies.contains(tradeCurrency)) + tradeCurrencies.add(tradeCurrency); + } + + public void removeCurrency(TradeCurrency tradeCurrency) { + if (tradeCurrencies.contains(tradeCurrency)) + tradeCurrencies.remove(tradeCurrency); + } + + public boolean hasMultipleCurrencies() { + return tradeCurrencies.size() > 1; + } + + public void setSingleTradeCurrency(TradeCurrency tradeCurrency) { + tradeCurrencies.clear(); + tradeCurrencies.add(tradeCurrency); + setSelectedTradeCurrency(tradeCurrency); + } + + public TradeCurrency getSingleTradeCurrency() { + if (!tradeCurrencies.isEmpty()) + return tradeCurrencies.get(0); + else + return null; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getter, Setter + /////////////////////////////////////////////////////////////////////////////////////////// + + public String getAccountName() { + return accountName; + } + + public void setAccountName(String accountName) { + this.accountName = accountName; + } + + @Nullable + public Country getCountry() { + return country; + } + + public void setCountry(@Nullable Country country) { + this.country = country; + contractData.setCountryCode(country.code); + } + + public void setSelectedTradeCurrency(TradeCurrency tradeCurrency) { + selectedTradeCurrency = tradeCurrency; + } + + public TradeCurrency getSelectedTradeCurrency() { + return selectedTradeCurrency; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getter + /////////////////////////////////////////////////////////////////////////////////////////// + + public String getId() { + return id; + } + + public PaymentMethod getPaymentMethod() { + return paymentMethod; + } + + public List getTradeCurrencies() { + return tradeCurrencies; + } + + public PaymentAccountContractData getContractData() { + return contractData; + } + + public String getPaymentDetails() { + return contractData.getPaymentDetails(); + } + + public int getMaxTradePeriod() { + return contractData.getMaxTradePeriod(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Util + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public String toString() { + return contractData.toString() + '\'' + + "PaymentAccount{" + + "id='" + id + '\'' + + ", paymentMethod=" + paymentMethod + + ", accountName='" + accountName + '\'' + + ", tradeCurrencies=" + tradeCurrencies + + ", selectedTradeCurrency=" + selectedTradeCurrency + + ", country=" + country + + ", contractData=" + contractData + + '}'; + } +} diff --git a/core/src/main/java/io/bitsquare/payment/PaymentAccountContractData.java b/core/src/main/java/io/bitsquare/payment/PaymentAccountContractData.java new file mode 100644 index 0000000000..ddf4a93e7c --- /dev/null +++ b/core/src/main/java/io/bitsquare/payment/PaymentAccountContractData.java @@ -0,0 +1,102 @@ +/* + * 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.payment; + +import io.bitsquare.app.Version; + +import javax.annotation.Nullable; +import java.io.Serializable; + +public abstract class PaymentAccountContractData implements Serializable { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + + private final String paymentMethodName; + private final String id; + private final int maxTradePeriod; + + @Nullable + private String countryCode; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public PaymentAccountContractData(String paymentMethodName, String id, int maxTradePeriod) { + this.paymentMethodName = paymentMethodName; + this.id = id; + this.maxTradePeriod = maxTradePeriod; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getter, Setter + /////////////////////////////////////////////////////////////////////////////////////////// + + public void setCountryCode(String countryCode) { + this.countryCode = countryCode; + } + + @Nullable + public String getCountryCode() { + return countryCode; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getter + /////////////////////////////////////////////////////////////////////////////////////////// + + public String getId() { + return id; + } + + public String getPaymentMethodName() { + return paymentMethodName; + } + + abstract public String getPaymentDetails(); + + public int getMaxTradePeriod() { + return maxTradePeriod; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PaymentAccountContractData)) return false; + + PaymentAccountContractData that = (PaymentAccountContractData) o; + + if (maxTradePeriod != that.maxTradePeriod) return false; + if (paymentMethodName != null ? !paymentMethodName.equals(that.paymentMethodName) : that.paymentMethodName != null) + return false; + if (id != null ? !id.equals(that.id) : that.id != null) return false; + return !(countryCode != null ? !countryCode.equals(that.countryCode) : that.countryCode != null); + + } + + @Override + public int hashCode() { + int result = paymentMethodName != null ? paymentMethodName.hashCode() : 0; + result = 31 * result + (id != null ? id.hashCode() : 0); + result = 31 * result + maxTradePeriod; + result = 31 * result + (countryCode != null ? countryCode.hashCode() : 0); + return result; + } +} diff --git a/core/src/main/java/io/bitsquare/payment/PaymentMethod.java b/core/src/main/java/io/bitsquare/payment/PaymentMethod.java new file mode 100644 index 0000000000..8376715b18 --- /dev/null +++ b/core/src/main/java/io/bitsquare/payment/PaymentMethod.java @@ -0,0 +1,144 @@ +/* + * 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.payment; + +import io.bitsquare.app.Version; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +// Don't use Enum as it breaks serialisation when changing entries and we want to stay flexible here +public class PaymentMethod implements Serializable, Comparable { + // That object is saved to disc. We need to take care of changes to not break deserialization. + private static final long serialVersionUID = Version.LOCAL_DB_VERSION; + + protected final Logger log = LoggerFactory.getLogger(this.getClass()); + + // time in blocks (average 10 min for one block confirmation + private static final int HOUR = 6; + private static final int DAY = HOUR * 24; + + public static final String OK_PAY_ID = "OK_PAY"; + public static final String PERFECT_MONEY_ID = "PERFECT_MONEY"; + public static final String SEPA_ID = "SEPA"; + public static final String SWISH_ID = "SWISH"; + public static final String ALI_PAY_ID = "ALI_PAY"; + /* public static final String FED_WIRE="FED_WIRE";*/ + /* public static final String TRANSFER_WISE="TRANSFER_WISE";*/ + /* public static final String US_POSTAL_MONEY_ORDER="US_POSTAL_MONEY_ORDER";*/ + public static final String BLOCK_CHAINS_ID = "BLOCK_CHAINS"; + + public static PaymentMethod OK_PAY; + public static PaymentMethod PERFECT_MONEY; + public static PaymentMethod SEPA; + public static PaymentMethod SWISH; + public static PaymentMethod ALI_PAY; + /* public static PaymentMethod FED_WIRE;*/ + /* public static PaymentMethod TRANSFER_WISE;*/ + /* public static PaymentMethod US_POSTAL_MONEY_ORDER;*/ + public static PaymentMethod BLOCK_CHAINS; + + public static final List ALL_VALUES = new ArrayList<>(Arrays.asList( + OK_PAY = new PaymentMethod(OK_PAY_ID, 0, HOUR), // tx instant so min. wait time + PERFECT_MONEY = new PaymentMethod(PERFECT_MONEY_ID, 0, DAY), + SEPA = new PaymentMethod(SEPA_ID, 0, 8 * DAY), // sepa takes 1-3 business days. We use 8 days to include safety for holidays + SWISH = new PaymentMethod(SWISH_ID, 0, DAY), + ALI_PAY = new PaymentMethod(ALI_PAY_ID, 0, DAY), + /* FED_WIRE = new PaymentMethod(FED_WIRE_ID, 0, DAY),*/ + /* TRANSFER_WISE = new PaymentMethod(TRANSFER_WISE_ID, 0, DAY),*/ + /* US_POSTAL_MONEY_ORDER = new PaymentMethod(US_POSTAL_MONEY_ORDER_ID, 0, DAY),*/ + BLOCK_CHAINS = new PaymentMethod(BLOCK_CHAINS_ID, 0, DAY) + )); + + + private final String id; + + private final long lockTime; + + private final int maxTradePeriod; + + /** + * @param id + * @param lockTime lock time when seller release BTC until the payout tx gets valid (bitcoin tx lockTime). Serves as protection + * against charge back risk. If Bank do the charge back quickly the Arbitrator and the seller can push another + * double spend tx to invalidate the time locked payout tx. For the moment we set all to 0 but will have it in + * place when needed. + * @param maxTradePeriod The min. period a trader need to wait until he gets displayed the contact form for opening a dispute. + */ + public PaymentMethod(String id, long lockTime, int maxTradePeriod) { + this.id = id; + this.lockTime = lockTime; + this.maxTradePeriod = maxTradePeriod; + } + + public static PaymentMethod getPaymentMethodByName(String name) { + return ALL_VALUES.stream().filter(e -> e.getId().equals(name)).findFirst().get(); + } + + public String getId() { + return id; + } + + public int getMaxTradePeriod() { + return maxTradePeriod; + } + + public long getLockTime() { + return lockTime; + } + + @Override + public int compareTo(@NotNull Object other) { + return this.id.compareTo(((PaymentMethod) other).id); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PaymentMethod)) return false; + + PaymentMethod that = (PaymentMethod) o; + + if (getLockTime() != that.getLockTime()) return false; + if (getMaxTradePeriod() != that.getMaxTradePeriod()) return false; + return !(getId() != null ? !getId().equals(that.getId()) : that.getId() != null); + + } + + @Override + public int hashCode() { + int result = getId() != null ? getId().hashCode() : 0; + result = 31 * result + (int) (getLockTime() ^ (getLockTime() >>> 32)); + result = 31 * result + getMaxTradePeriod(); + return result; + } + + @Override + public String toString() { + return "PaymentMethod{" + + "name='" + id + '\'' + + ", lockTime=" + lockTime + + ", waitPeriodForOpenDispute=" + maxTradePeriod + + '}'; + } +} diff --git a/core/src/main/java/io/bitsquare/payment/PerfectMoneyAccount.java b/core/src/main/java/io/bitsquare/payment/PerfectMoneyAccount.java new file mode 100644 index 0000000000..46b2521cd8 --- /dev/null +++ b/core/src/main/java/io/bitsquare/payment/PerfectMoneyAccount.java @@ -0,0 +1,51 @@ +/* + * This file is part of Bitsquare. + * + * Bitsquare is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bitsquare is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bitsquare. If not, see . + */ + +package io.bitsquare.payment; + +import io.bitsquare.app.Version; +import io.bitsquare.locale.FiatCurrency; + +import java.io.Serializable; + +public class PerfectMoneyAccount extends PaymentAccount implements Serializable { + // That object is saved to disc. We need to take care of changes to not break deserialization. + private static final long serialVersionUID = Version.LOCAL_DB_VERSION; + + public PerfectMoneyAccount() { + super(PaymentMethod.PERFECT_MONEY); + setSingleTradeCurrency(new FiatCurrency("USD")); + + contractData = new PerfectMoneyAccountContractData(paymentMethod.getId(), id, paymentMethod.getMaxTradePeriod()); + } + + public String getHolderName() { + return ((PerfectMoneyAccountContractData) contractData).getHolderName(); + } + + public void setHolderName(String holderName) { + ((PerfectMoneyAccountContractData) contractData).setHolderName(holderName); + } + + public void setAccountNr(String accountNr) { + ((PerfectMoneyAccountContractData) contractData).setAccountNr(accountNr); + } + + public String getAccountNr() { + return ((PerfectMoneyAccountContractData) contractData).getAccountNr(); + } +} diff --git a/core/src/main/java/io/bitsquare/payment/PerfectMoneyAccountContractData.java b/core/src/main/java/io/bitsquare/payment/PerfectMoneyAccountContractData.java new file mode 100644 index 0000000000..4161e79e62 --- /dev/null +++ b/core/src/main/java/io/bitsquare/payment/PerfectMoneyAccountContractData.java @@ -0,0 +1,56 @@ +/* + * 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.payment; + +import io.bitsquare.app.Version; + +import java.io.Serializable; + +public class PerfectMoneyAccountContractData extends PaymentAccountContractData implements Serializable { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + + private String holderName; + private String accountNr; + + public PerfectMoneyAccountContractData(String paymentMethod, String id, int maxTradePeriod) { + super(paymentMethod, id, maxTradePeriod); + } + + public String getHolderName() { + return holderName; + } + + public void setHolderName(String holderName) { + this.holderName = holderName; + } + + public void setAccountNr(String accountNr) { + this.accountNr = accountNr; + } + + public String getAccountNr() { + return accountNr; + } + + @Override + public String getPaymentDetails() { + return "PerfectMoney - Holder name: " + holderName + ", account nr.: " + accountNr; + } + +} diff --git a/core/src/main/java/io/bitsquare/payment/SepaAccount.java b/core/src/main/java/io/bitsquare/payment/SepaAccount.java new file mode 100644 index 0000000000..835e5dec2c --- /dev/null +++ b/core/src/main/java/io/bitsquare/payment/SepaAccount.java @@ -0,0 +1,71 @@ +/* + * 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.payment; + +import io.bitsquare.app.Version; + +import java.io.Serializable; +import java.util.List; + +public class SepaAccount extends PaymentAccount implements Serializable { + // That object is saved to disc. We need to take care of changes to not break deserialization. + private static final long serialVersionUID = Version.LOCAL_DB_VERSION; + + public SepaAccount() { + super(PaymentMethod.SEPA); + + contractData = new SepaAccountContractData(paymentMethod.getId(), id, paymentMethod.getMaxTradePeriod()); + } + + public void setHolderName(String holderName) { + ((SepaAccountContractData) contractData).setHolderName(holderName); + } + + public String getHolderName() { + return ((SepaAccountContractData) contractData).getHolderName(); + } + + public void setIban(String iban) { + ((SepaAccountContractData) contractData).setIban(iban); + } + + public String getIban() { + return ((SepaAccountContractData) contractData).getIban(); + } + + public void setBic(String bic) { + ((SepaAccountContractData) contractData).setBic(bic); + } + + public String getBic() { + return ((SepaAccountContractData) contractData).getBic(); + } + + public List getAcceptedCountryCodes() { + return ((SepaAccountContractData) contractData).getAcceptedCountryCodes(); + } + + public void addAcceptedCountry(String countryCode) { + ((SepaAccountContractData) contractData).addAcceptedCountry(countryCode); + } + + public void removeAcceptedCountry(String countryCode) { + ((SepaAccountContractData) contractData).removeAcceptedCountry(countryCode); + } + +} diff --git a/core/src/main/java/io/bitsquare/payment/SepaAccountContractData.java b/core/src/main/java/io/bitsquare/payment/SepaAccountContractData.java new file mode 100644 index 0000000000..a50aeb2eea --- /dev/null +++ b/core/src/main/java/io/bitsquare/payment/SepaAccountContractData.java @@ -0,0 +1,89 @@ +/* + * 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.payment; + +import io.bitsquare.app.Version; +import io.bitsquare.locale.CountryUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class SepaAccountContractData extends PaymentAccountContractData implements Serializable { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + + transient private static final Logger log = LoggerFactory.getLogger(SepaAccountContractData.class); + + private String holderName; + private String iban; + private String bic; + private Set acceptedCountryCodes; + + public SepaAccountContractData(String paymentMethod, String id, int maxTradePeriod) { + super(paymentMethod, id, maxTradePeriod); + acceptedCountryCodes = CountryUtil.getAllSepaCountries().stream().map(e -> e.code).collect(Collectors.toSet()); + } + + public void setHolderName(String holderName) { + this.holderName = holderName; + } + + public String getHolderName() { + return holderName; + } + + public void setIban(String iban) { + this.iban = iban; + } + + public String getIban() { + return iban; + } + + public void setBic(String bic) { + this.bic = bic; + } + + public String getBic() { + return bic; + } + + public void addAcceptedCountry(String countryCode) { + acceptedCountryCodes.add(countryCode); + } + + public void removeAcceptedCountry(String countryCode) { + acceptedCountryCodes.remove(countryCode); + } + + public List getAcceptedCountryCodes() { + List sortedList = new ArrayList<>(acceptedCountryCodes); + sortedList.sort((a, b) -> a.compareTo(b)); + return sortedList; + } + + @Override + public String getPaymentDetails() { + return "SEPA - Holder name: " + holderName + ", IBAN: " + iban + ", BIC: " + bic + ", country code: " + getCountryCode(); + } +} diff --git a/core/src/main/java/io/bitsquare/payment/SwishAccount.java b/core/src/main/java/io/bitsquare/payment/SwishAccount.java new file mode 100644 index 0000000000..6f81a9fbe6 --- /dev/null +++ b/core/src/main/java/io/bitsquare/payment/SwishAccount.java @@ -0,0 +1,51 @@ +/* + * This file is part of Bitsquare. + * + * Bitsquare is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bitsquare is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bitsquare. If not, see . + */ + +package io.bitsquare.payment; + +import io.bitsquare.app.Version; +import io.bitsquare.locale.FiatCurrency; + +import java.io.Serializable; + +public class SwishAccount extends PaymentAccount implements Serializable { + // That object is saved to disc. We need to take care of changes to not break deserialization. + private static final long serialVersionUID = Version.LOCAL_DB_VERSION; + + public SwishAccount() { + super(PaymentMethod.SWISH); + setSingleTradeCurrency(new FiatCurrency("SEK")); + + contractData = new SwishAccountContractData(paymentMethod.getId(), id, paymentMethod.getMaxTradePeriod()); + } + + public void setMobileNr(String mobileNr) { + ((SwishAccountContractData) contractData).setMobileNr(mobileNr); + } + + public String getMobileNr() { + return ((SwishAccountContractData) contractData).getMobileNr(); + } + + public void setHolderName(String holderName) { + ((SwishAccountContractData) contractData).setHolderName(holderName); + } + + public String getHolderName() { + return ((SwishAccountContractData) contractData).getHolderName(); + } +} diff --git a/core/src/main/java/io/bitsquare/payment/SwishAccountContractData.java b/core/src/main/java/io/bitsquare/payment/SwishAccountContractData.java new file mode 100644 index 0000000000..1dacb37de0 --- /dev/null +++ b/core/src/main/java/io/bitsquare/payment/SwishAccountContractData.java @@ -0,0 +1,56 @@ +/* + * 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.payment; + +import io.bitsquare.app.Version; + +import java.io.Serializable; + +public class SwishAccountContractData extends PaymentAccountContractData implements Serializable { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + + private String mobileNr; + private String holderName; + + public SwishAccountContractData(String paymentMethod, String id, int maxTradePeriod) { + super(paymentMethod, id, maxTradePeriod); + } + + public void setMobileNr(String mobileNr) { + this.mobileNr = mobileNr; + } + + public String getMobileNr() { + return mobileNr; + } + + public String getHolderName() { + return holderName; + } + + public void setHolderName(String holderName) { + this.holderName = holderName; + } + + @Override + public String getPaymentDetails() { + return "Swish - Holder name: " + holderName + ", mobile nr.: " + mobileNr; + } + +} diff --git a/core/src/main/java/io/bitsquare/payment/unused/FedWireAccount.java b/core/src/main/java/io/bitsquare/payment/unused/FedWireAccount.java new file mode 100644 index 0000000000..b0fe5ff9a1 --- /dev/null +++ b/core/src/main/java/io/bitsquare/payment/unused/FedWireAccount.java @@ -0,0 +1,97 @@ +/* + * 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.payment.unused; + +import io.bitsquare.app.Version; +import io.bitsquare.payment.PaymentAccount; +import io.bitsquare.payment.PaymentMethod; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Serializable; + +// US only +public class FedWireAccount extends PaymentAccount implements Serializable { + // That object is saved to disc. We need to take care of changes to not break deserialization. + private static final long serialVersionUID = Version.LOCAL_DB_VERSION; + + private static final Logger log = LoggerFactory.getLogger(FedWireAccount.class); + + private String holderName; + private String holderState; + private String holderZIP; + private String holderStreet; + private String holderCity; + private String holderSSN; // Optional? social security Nr only Arizona and Oklahoma? + private String accountNr; + private String bankCode;// SWIFT Code/BIC/RoutingNr/ABA (ABA for UD domestic) + private String bankName; + private String bankState; + private String bankZIP; + private String bankStreet; + private String bankCity; + + private FedWireAccount() { + super(PaymentMethod.SEPA); + } + + public String getHolderName() { + return holderName; + } + + public void setHolderName(String holderName) { + this.holderName = holderName; + } + + public String getAccountNr() { + return accountNr; + } + + public void setAccountNr(String accountNr) { + this.accountNr = accountNr; + } + + public String getBankCode() { + return bankCode; + } + + public void setBankCode(String bankCode) { + this.bankCode = bankCode; + } + + @Override + public String getPaymentDetails() { + return "{accountName='" + accountName + '\'' + + '}'; + } + + @Override + public String toString() { + return "SepaAccount{" + + "accountName='" + accountName + '\'' + + ", id='" + id + '\'' + + ", paymentMethod=" + paymentMethod + + ", holderName='" + holderName + '\'' + + ", accountNr='" + accountNr + '\'' + + ", bankCode='" + bankCode + '\'' + + ", country=" + country + + ", tradeCurrencies='" + getTradeCurrencies() + '\'' + + ", selectedTradeCurrency=" + selectedTradeCurrency + + '}'; + } +} diff --git a/core/src/main/java/io/bitsquare/payment/unused/TransferWiseAccount.java b/core/src/main/java/io/bitsquare/payment/unused/TransferWiseAccount.java new file mode 100644 index 0000000000..63cd839a78 --- /dev/null +++ b/core/src/main/java/io/bitsquare/payment/unused/TransferWiseAccount.java @@ -0,0 +1,87 @@ +/* + * 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.payment.unused; + +import io.bitsquare.app.Version; +import io.bitsquare.payment.PaymentAccount; +import io.bitsquare.payment.PaymentMethod; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Serializable; + +public class TransferWiseAccount extends PaymentAccount implements Serializable { + // That object is saved to disc. We need to take care of changes to not break deserialization. + private static final long serialVersionUID = Version.LOCAL_DB_VERSION; + + private static final Logger log = LoggerFactory.getLogger(TransferWiseAccount.class); + + private String holderName; + private String iban; + private String bic; + + + private TransferWiseAccount() { + super(PaymentMethod.SEPA); + } + + public String getHolderName() { + return holderName; + } + + public void setHolderName(String holderName) { + this.holderName = holderName; + } + + public String getIban() { + return iban; + } + + public void setIban(String iban) { + this.iban = iban; + } + + public String getBic() { + return bic; + } + + public void setBic(String bic) { + this.bic = bic; + } + + @Override + public String getPaymentDetails() { + return "TransferWise{accountName='" + accountName + '\'' + + '}'; + } + + @Override + public String toString() { + return "TransferWiseAccount{" + + "accountName='" + accountName + '\'' + + ", id='" + id + '\'' + + ", paymentMethod=" + paymentMethod + + ", holderName='" + holderName + '\'' + + ", iban='" + iban + '\'' + + ", bic='" + bic + '\'' + + ", country=" + country + + ", tradeCurrencies='" + getTradeCurrencies() + '\'' + + ", selectedTradeCurrency=" + selectedTradeCurrency + + '}'; + } +} diff --git a/core/src/main/java/io/bitsquare/payment/unused/USPostalMoneyOrderAccount.java b/core/src/main/java/io/bitsquare/payment/unused/USPostalMoneyOrderAccount.java new file mode 100644 index 0000000000..83845d26f8 --- /dev/null +++ b/core/src/main/java/io/bitsquare/payment/unused/USPostalMoneyOrderAccount.java @@ -0,0 +1,86 @@ +/* + * 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.payment.unused; + +import io.bitsquare.app.Version; +import io.bitsquare.payment.PaymentAccount; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Serializable; + +public class USPostalMoneyOrderAccount extends PaymentAccount implements Serializable { + // That object is saved to disc. We need to take care of changes to not break deserialization. + private static final long serialVersionUID = Version.LOCAL_DB_VERSION; + + private static final Logger log = LoggerFactory.getLogger(USPostalMoneyOrderAccount.class); + + private String holderName; + private String iban; + private String bic; + + + private USPostalMoneyOrderAccount() { + super(null /*PaymentMethod.US_POSTAL_MONEY_ORDER*/); + } + + public String getHolderName() { + return holderName; + } + + public void setHolderName(String holderName) { + this.holderName = holderName; + } + + public String getIban() { + return iban; + } + + public void setIban(String iban) { + this.iban = iban; + } + + public String getBic() { + return bic; + } + + public void setBic(String bic) { + this.bic = bic; + } + + @Override + public String getPaymentDetails() { + return "{accountName='" + accountName + '\'' + + '}'; + } + + @Override + public String toString() { + return "USPostalMoneyOrderAccount{" + + "accountName='" + accountName + '\'' + + ", id='" + id + '\'' + + ", paymentMethod=" + paymentMethod + + ", holderName='" + holderName + '\'' + + ", iban='" + iban + '\'' + + ", bic='" + bic + '\'' + + ", country=" + country + + ", tradeCurrencies='" + getTradeCurrencies() + '\'' + + ", selectedTradeCurrency=" + selectedTradeCurrency + + '}'; + } +} diff --git a/core/src/main/java/io/bitsquare/trade/handlers/TradeResultHandler.java b/core/src/main/java/io/bitsquare/trade/handlers/TradeResultHandler.java new file mode 100644 index 0000000000..96348fabda --- /dev/null +++ b/core/src/main/java/io/bitsquare/trade/handlers/TradeResultHandler.java @@ -0,0 +1,25 @@ +/* + * 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.handlers; + +import io.bitsquare.trade.Trade; + + +public interface TradeResultHandler { + void handleResult(Trade trade); +} diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/ArbitrationSelectionRule.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/ArbitrationSelectionRule.java new file mode 100644 index 0000000000..78829bdf20 --- /dev/null +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/ArbitrationSelectionRule.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; + +import io.bitsquare.p2p.Address; +import io.bitsquare.trade.offer.Offer; +import org.bitcoinj.core.Sha256Hash; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import static com.google.common.base.Preconditions.checkArgument; + +public class ArbitrationSelectionRule { + private static final Logger log = LoggerFactory.getLogger(ArbitrationSelectionRule.class); + + public static Address select(List
acceptedArbitratorAddresses, Offer offer) { + List
candidates = new ArrayList<>(); + for (Address offerArbitratorAddress : offer.getArbitratorAddresses()) { + candidates.addAll(acceptedArbitratorAddresses.stream().filter(offerArbitratorAddress::equals).collect(Collectors.toList())); + } + checkArgument(candidates.size() > 0, "candidates.size() <= 0"); + + int index = Math.abs(Sha256Hash.hash(offer.getId().getBytes()).hashCode()) % candidates.size(); + Address selectedArbitrator = candidates.get(index); + log.debug("selectedArbitrator " + selectedArbitrator); + return selectedArbitrator; + } +} diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/buyer/CreateAndSignDepositTxAsBuyer.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/buyer/CreateAndSignDepositTxAsBuyer.java new file mode 100644 index 0000000000..c42e31d117 --- /dev/null +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/buyer/CreateAndSignDepositTxAsBuyer.java @@ -0,0 +1,75 @@ +/* + * 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.buyer; + +import io.bitsquare.btc.FeePolicy; +import io.bitsquare.btc.data.PreparedDepositTxAndOffererInputs; +import io.bitsquare.common.crypto.CryptoUtil; +import io.bitsquare.common.taskrunner.TaskRunner; +import io.bitsquare.trade.Trade; +import io.bitsquare.trade.protocol.trade.tasks.TradeTask; +import org.bitcoinj.core.Coin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class CreateAndSignDepositTxAsBuyer extends TradeTask { + private static final Logger log = LoggerFactory.getLogger(CreateAndSignDepositTxAsBuyer.class); + + public CreateAndSignDepositTxAsBuyer(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null"); + Coin buyerInputAmount = FeePolicy.SECURITY_DEPOSIT.add(FeePolicy.TX_FEE); + Coin msOutputAmount = buyerInputAmount.add(FeePolicy.SECURITY_DEPOSIT).add(trade.getTradeAmount()); + + log.debug("getContractAsJson"); + log.debug("----------"); + log.debug(trade.getContractAsJson()); + log.debug("----------"); + + byte[] contractHash = CryptoUtil.getHash(trade.getContractAsJson()); + trade.setContractHash(contractHash); + PreparedDepositTxAndOffererInputs result = processModel.getTradeWalletService().offererCreatesAndSignsDepositTx( + true, + contractHash, + buyerInputAmount, + msOutputAmount, + processModel.tradingPeer.getRawInputs(), + processModel.tradingPeer.getChangeOutputValue(), + processModel.tradingPeer.getChangeOutputAddress(), + processModel.getAddressEntry(), + processModel.getTradeWalletPubKey(), + processModel.tradingPeer.getTradeWalletPubKey(), + processModel.getArbitratorPubKey(trade.getArbitratorAddress())); + + processModel.setPreparedDepositTx(result.depositTransaction); + processModel.setRawInputs(result.rawOffererInputs); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/buyer/CreateDepositTxInputsAsBuyer.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/buyer/CreateDepositTxInputsAsBuyer.java new file mode 100644 index 0000000000..23b9821085 --- /dev/null +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/buyer/CreateDepositTxInputsAsBuyer.java @@ -0,0 +1,51 @@ +/* + * This file is part of Bitsquare. + * + * Bitsquare is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bitsquare is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bitsquare. If not, see . + */ + +package io.bitsquare.trade.protocol.trade.tasks.buyer; + +import io.bitsquare.btc.FeePolicy; +import io.bitsquare.btc.data.InputsAndChangeOutput; +import io.bitsquare.common.taskrunner.TaskRunner; +import io.bitsquare.trade.Trade; +import io.bitsquare.trade.protocol.trade.tasks.TradeTask; +import org.bitcoinj.core.Coin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CreateDepositTxInputsAsBuyer extends TradeTask { + private static final Logger log = LoggerFactory.getLogger(CreateDepositTxInputsAsBuyer.class); + + public CreateDepositTxInputsAsBuyer(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + Coin takerInputAmount = FeePolicy.SECURITY_DEPOSIT.add(FeePolicy.TX_FEE); + InputsAndChangeOutput result = processModel.getTradeWalletService().takerCreatesDepositsTxInputs(takerInputAmount, processModel.getAddressEntry()); + processModel.setRawInputs(result.rawInputs); + processModel.setChangeOutputValue(result.changeOutputValue); + processModel.setChangeOutputAddress(result.changeOutputAddress); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/buyer/SignAndPublishDepositTxAsBuyer.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/buyer/SignAndPublishDepositTxAsBuyer.java new file mode 100644 index 0000000000..12329e3647 --- /dev/null +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/buyer/SignAndPublishDepositTxAsBuyer.java @@ -0,0 +1,76 @@ +/* + * 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.buyer; + +import com.google.common.util.concurrent.FutureCallback; +import io.bitsquare.common.crypto.CryptoUtil; +import io.bitsquare.common.taskrunner.TaskRunner; +import io.bitsquare.trade.Trade; +import io.bitsquare.trade.protocol.trade.tasks.TradeTask; +import org.bitcoinj.core.Transaction; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Date; + +public class SignAndPublishDepositTxAsBuyer extends TradeTask { + private static final Logger log = LoggerFactory.getLogger(SignAndPublishDepositTxAsBuyer.class); + + public SignAndPublishDepositTxAsBuyer(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + byte[] contractHash = CryptoUtil.getHash(trade.getContractAsJson()); + trade.setContractHash(contractHash); + processModel.getTradeWalletService().takerSignsAndPublishesDepositTx( + false, + contractHash, + processModel.getPreparedDepositTx(), + processModel.getRawInputs(), + processModel.tradingPeer.getRawInputs(), + processModel.getTradeWalletPubKey(), + processModel.tradingPeer.getTradeWalletPubKey(), + processModel.getArbitratorPubKey(trade.getArbitratorAddress()), + new FutureCallback() { + @Override + public void onSuccess(Transaction transaction) { + log.trace("takerSignAndPublishTx succeeded " + transaction); + + trade.setDepositTx(transaction); + trade.setTakeOfferDate(new Date()); + trade.setState(Trade.State.DEPOSIT_PUBLISHED); + + complete(); + } + + @Override + public void onFailure(@NotNull Throwable t) { + failed(t); + } + }); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/offerer/AddDepositTxToWallet.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/offerer/AddDepositTxToWallet.java new file mode 100644 index 0000000000..f370021985 --- /dev/null +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/offerer/AddDepositTxToWallet.java @@ -0,0 +1,48 @@ +/* + * 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 org.bitcoinj.core.Transaction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AddDepositTxToWallet extends TradeTask { + private static final Logger log = LoggerFactory.getLogger(AddDepositTxToWallet.class); + + public AddDepositTxToWallet(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + // To access tx confidence we need to add that tx into our wallet. + Transaction depositTx = processModel.getTradeWalletService().addTransactionToWallet(trade.getDepositTx()); + // update with full tx + trade.setDepositTx(depositTx); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/offerer/CreateAndSignContract.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/offerer/CreateAndSignContract.java new file mode 100644 index 0000000000..a26fa33757 --- /dev/null +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/offerer/CreateAndSignContract.java @@ -0,0 +1,89 @@ +/* + * 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.crypto.CryptoUtil; +import io.bitsquare.common.taskrunner.TaskRunner; +import io.bitsquare.common.util.Utilities; +import io.bitsquare.p2p.Address; +import io.bitsquare.payment.PaymentAccountContractData; +import io.bitsquare.trade.BuyerAsOffererTrade; +import io.bitsquare.trade.Contract; +import io.bitsquare.trade.Trade; +import io.bitsquare.trade.protocol.trade.TradingPeer; +import io.bitsquare.trade.protocol.trade.tasks.TradeTask; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class CreateAndSignContract extends TradeTask { + private static final Logger log = LoggerFactory.getLogger(CreateAndSignContract.class); + + public CreateAndSignContract(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + checkNotNull(processModel.getTakeOfferFeeTxId(), "processModel.getTakeOfferFeeTxId() must not be null"); + + TradingPeer taker = processModel.tradingPeer; + PaymentAccountContractData offererPaymentAccountContractData = processModel.getPaymentAccountContractData(trade); + PaymentAccountContractData takerPaymentAccountContractData = taker.getPaymentAccountContractData(); + boolean isBuyerOffererOrSellerTaker = trade instanceof BuyerAsOffererTrade; + + Address buyerAddress = isBuyerOffererOrSellerTaker ? processModel.getMyAddress() : processModel.getTempTradingPeerAddress(); + Address sellerAddress = isBuyerOffererOrSellerTaker ? processModel.getTempTradingPeerAddress() : processModel.getMyAddress(); + log.debug("isBuyerOffererOrSellerTaker " + isBuyerOffererOrSellerTaker); + log.debug("buyerAddress " + buyerAddress); + log.debug("sellerAddress " + sellerAddress); + Contract contract = new Contract( + processModel.getOffer(), + trade.getTradeAmount(), + processModel.getTakeOfferFeeTxId(), + buyerAddress, + sellerAddress, + trade.getArbitratorAddress(), + isBuyerOffererOrSellerTaker, + processModel.getAccountId(), + taker.getAccountId(), + offererPaymentAccountContractData, + takerPaymentAccountContractData, + processModel.getPubKeyRing(), + taker.getPubKeyRing(), + processModel.getAddressEntry().getAddressString(), + taker.getPayoutAddressString(), + processModel.getTradeWalletPubKey(), + taker.getTradeWalletPubKey() + ); + String contractAsJson = Utilities.objectToJson(contract); + String signature = CryptoUtil.signMessage(processModel.getKeyRing().getMsgSignatureKeyPair().getPrivate(), contractAsJson); + + trade.setContract(contract); + trade.setContractAsJson(contractAsJson); + trade.setOffererContractSignature(signature); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/offerer/LoadTakeOfferFeeTx.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/offerer/LoadTakeOfferFeeTx.java new file mode 100644 index 0000000000..8bb84229de --- /dev/null +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/offerer/LoadTakeOfferFeeTx.java @@ -0,0 +1,46 @@ +/* + * 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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LoadTakeOfferFeeTx extends TradeTask { + private static final Logger log = LoggerFactory.getLogger(LoadTakeOfferFeeTx.class); + + public LoadTakeOfferFeeTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + // TODO impl. not completed + //processModel.getWalletService().findTxInBlockChain(processModel.getTakeOfferFeeTxId()); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/offerer/ProcessDepositTxPublishedMessage.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/offerer/ProcessDepositTxPublishedMessage.java new file mode 100644 index 0000000000..2624041334 --- /dev/null +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/offerer/ProcessDepositTxPublishedMessage.java @@ -0,0 +1,65 @@ +/* + * 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.OffererTrade; +import io.bitsquare.trade.Trade; +import io.bitsquare.trade.protocol.trade.messages.DepositTxPublishedMessage; +import io.bitsquare.trade.protocol.trade.tasks.TradeTask; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Date; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.bitsquare.util.Validator.checkTradeId; + +public class ProcessDepositTxPublishedMessage extends TradeTask { + private static final Logger log = LoggerFactory.getLogger(ProcessDepositTxPublishedMessage.class); + + public ProcessDepositTxPublishedMessage(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + log.debug("current trade state " + trade.getState()); + DepositTxPublishedMessage message = (DepositTxPublishedMessage) processModel.getTradeMessage(); + checkTradeId(processModel.getId(), message); + checkNotNull(message); + checkArgument(message.depositTx != null); + trade.setDepositTx(processModel.getWalletService().getTransactionFromSerializedTx(message.depositTx)); + trade.setState(Trade.State.DEPOSIT_PUBLISHED_MSG_RECEIVED); + trade.setTakeOfferDate(new Date()); + + if (trade instanceof OffererTrade) + processModel.getOpenOfferManager().closeOpenOffer(trade.getOffer()); + + // update to the latest peer address of our peer if the message is correct + trade.setTradingPeerAddress(processModel.getTempTradingPeerAddress()); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/offerer/ProcessPayDepositRequest.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/offerer/ProcessPayDepositRequest.java new file mode 100644 index 0000000000..41e2389fd9 --- /dev/null +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/offerer/ProcessPayDepositRequest.java @@ -0,0 +1,90 @@ +/* + * 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.crypto.CryptoUtil; +import io.bitsquare.common.taskrunner.TaskRunner; +import io.bitsquare.locale.CurrencyUtil; +import io.bitsquare.payment.BlockChainAccountContractData; +import io.bitsquare.payment.PaymentAccountContractData; +import io.bitsquare.trade.Trade; +import io.bitsquare.trade.protocol.trade.messages.PayDepositRequest; +import io.bitsquare.trade.protocol.trade.tasks.TradeTask; +import org.bitcoinj.core.Coin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.bitsquare.util.Validator.checkTradeId; +import static io.bitsquare.util.Validator.nonEmptyStringOf; + +public class ProcessPayDepositRequest extends TradeTask { + private static final Logger log = LoggerFactory.getLogger(ProcessPayDepositRequest.class); + + public ProcessPayDepositRequest(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + log.debug("current trade state " + trade.getState()); + PayDepositRequest payDepositRequest = (PayDepositRequest) processModel.getTradeMessage(); + checkTradeId(processModel.getId(), payDepositRequest); + checkNotNull(payDepositRequest); + + processModel.tradingPeer.setRawInputs(checkNotNull(payDepositRequest.rawInputs)); + checkArgument(payDepositRequest.rawInputs.size() > 0); + + processModel.tradingPeer.setChangeOutputValue(payDepositRequest.changeOutputValue); + if (payDepositRequest.changeOutputAddress != null) + processModel.tradingPeer.setChangeOutputAddress(payDepositRequest.changeOutputAddress); + + processModel.tradingPeer.setTradeWalletPubKey(checkNotNull(payDepositRequest.takerTradeWalletPubKey)); + processModel.tradingPeer.setPayoutAddressString(nonEmptyStringOf(payDepositRequest.takerPayoutAddressString)); + processModel.tradingPeer.setPubKeyRing(checkNotNull(payDepositRequest.takerPubKeyRing)); + + PaymentAccountContractData paymentAccountContractData = checkNotNull(payDepositRequest.takerPaymentAccountContractData); + processModel.tradingPeer.setPaymentAccountContractData(paymentAccountContractData); + // We apply the payment ID in case its a cryptoNote coin. It is created form the hash of the trade ID + if (paymentAccountContractData instanceof BlockChainAccountContractData && + CurrencyUtil.isCryptoNoteCoin(processModel.getOffer().getCurrencyCode())) { + String paymentId = CryptoUtil.getHashAsHex(trade.getId()).substring(0, 32); + ((BlockChainAccountContractData) paymentAccountContractData).setPaymentId(paymentId); + } + + processModel.tradingPeer.setAccountId(nonEmptyStringOf(payDepositRequest.takerAccountId)); + processModel.setTakeOfferFeeTxId(nonEmptyStringOf(payDepositRequest.takeOfferFeeTxId)); + processModel.setTakerAcceptedArbitratorAddresses(checkNotNull(payDepositRequest.acceptedArbitratorAddresses)); + if (payDepositRequest.acceptedArbitratorAddresses.size() < 1) + failed("acceptedArbitratorNames size must be at least 1"); + trade.setArbitratorAddress(checkNotNull(payDepositRequest.arbitratorAddress)); + checkArgument(payDepositRequest.tradeAmount > 0); + trade.setTradeAmount(Coin.valueOf(payDepositRequest.tradeAmount)); + + // update to the latest peer address of our peer if the payDepositRequest is correct + trade.setTradingPeerAddress(processModel.getTempTradingPeerAddress()); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/offerer/SendPublishDepositTxRequest.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/offerer/SendPublishDepositTxRequest.java new file mode 100644 index 0000000000..db06161dce --- /dev/null +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/offerer/SendPublishDepositTxRequest.java @@ -0,0 +1,76 @@ +/* + * 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.p2p.messaging.SendMailMessageListener; +import io.bitsquare.trade.Trade; +import io.bitsquare.trade.protocol.trade.messages.PublishDepositTxRequest; +import io.bitsquare.trade.protocol.trade.tasks.TradeTask; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SendPublishDepositTxRequest extends TradeTask { + private static final Logger log = LoggerFactory.getLogger(SendPublishDepositTxRequest.class); + + public SendPublishDepositTxRequest(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + trade.setState(Trade.State.DEPOSIT_PUBLISH_REQUESTED); + PublishDepositTxRequest tradeMessage = new PublishDepositTxRequest( + processModel.getId(), + processModel.getPaymentAccountContractData(trade), + processModel.getAccountId(), + processModel.getTradeWalletPubKey(), + trade.getContractAsJson(), + trade.getOffererContractSignature(), + processModel.getAddressEntry().getAddressString(), + processModel.getPreparedDepositTx(), + processModel.getRawInputs(), + trade.getOpenDisputeTimeAsBlockHeight(), + trade.getCheckPaymentTimeAsBlockHeight() + ); + + processModel.getP2PService().sendEncryptedMailMessage( + trade.getTradingPeerAddress(), + processModel.tradingPeer.getPubKeyRing(), + tradeMessage, + new SendMailMessageListener() { + @Override + public void onArrived() { + log.trace("Message arrived at peer."); + complete(); + } + + @Override + public void onFault() { + appendToErrorMessage("PublishDepositTxRequest sending failed"); + failed(); + } + } + ); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/offerer/SetupDepositBalanceListener.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/offerer/SetupDepositBalanceListener.java new file mode 100644 index 0000000000..61085f92be --- /dev/null +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/offerer/SetupDepositBalanceListener.java @@ -0,0 +1,108 @@ +/* + * 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.btc.WalletService; +import io.bitsquare.btc.listeners.BalanceListener; +import io.bitsquare.common.UserThread; +import io.bitsquare.common.taskrunner.TaskRunner; +import io.bitsquare.trade.OffererTrade; +import io.bitsquare.trade.Trade; +import io.bitsquare.trade.protocol.trade.tasks.TradeTask; +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.fxmisc.easybind.EasyBind; +import org.fxmisc.easybind.Subscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +// The buyer waits for the msg from the seller that he has published the deposit tx. +// In error case he might not get that msg so we check additionally the balance of our inputs, if it is zero, it means the deposit +// is already published. We set then the DEPOSIT_LOCKED state, so the user get informed that he is already in the critical state and need +// to request support. +public class SetupDepositBalanceListener extends TradeTask { + private static final Logger log = LoggerFactory.getLogger(SetupDepositBalanceListener.class); + private Subscription tradeStateSubscription; + private BalanceListener balanceListener; + + public SetupDepositBalanceListener(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + WalletService walletService = processModel.getWalletService(); + Address address = walletService.getAddressEntryByOfferId(trade.getId()).getAddress(); + balanceListener = walletService.addBalanceListener(new BalanceListener(address) { + @Override + public void onBalanceChanged(Coin balance) { + updateBalance(balance); + } + }); + walletService.addBalanceListener(balanceListener); + + tradeStateSubscription = EasyBind.subscribe(trade.stateProperty(), newValue -> { + log.debug("tradeStateSubscription newValue " + newValue); + if (newValue == Trade.State.DEPOSIT_PUBLISHED_MSG_RECEIVED + || newValue == Trade.State.DEPOSIT_SEEN_IN_NETWORK) { + + walletService.removeBalanceListener(balanceListener); + log.debug(" UserThread.execute(this::unSubscribe);"); + // TODO is that allowed? + UserThread.execute(this::unSubscribe); + } + }); + updateBalance(walletService.getBalanceForAddress(address)); + + // we complete immediately, our object stays alive because the balanceListener is stored in the WalletService + complete(); + } catch (Throwable t) { + failed(t); + } + } + + private void unSubscribe() { + //TODO investigate, seems to not get called sometimes + log.debug("unSubscribe tradeStateSubscription"); + tradeStateSubscription.unsubscribe(); + } + + private void updateBalance(Coin balance) { + log.debug("updateBalance " + balance.toFriendlyString()); + log.debug("pre tradeState " + trade.getState().toString()); + Trade.State tradeState = trade.getState(); + if (balance.compareTo(Coin.ZERO) == 0) { + if (trade instanceof OffererTrade) { + processModel.getOpenOfferManager().closeOpenOffer(trade.getOffer()); + + if (tradeState == Trade.State.DEPOSIT_PUBLISH_REQUESTED) { + trade.setState(Trade.State.DEPOSIT_SEEN_IN_NETWORK); + } else if (tradeState.getPhase() == Trade.Phase.PREPARATION) { + processModel.getTradeManager().removePreparedTrade(trade); + } else if (tradeState.getPhase().ordinal() < Trade.Phase.DEPOSIT_PAID.ordinal()) { + processModel.getTradeManager().addTradeToFailedTrades(trade); + } + } + } + + log.debug("tradeState " + trade.getState().toString()); + } +} diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/offerer/VerifyArbitrationSelection.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/offerer/VerifyArbitrationSelection.java new file mode 100644 index 0000000000..bd147ba405 --- /dev/null +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/offerer/VerifyArbitrationSelection.java @@ -0,0 +1,48 @@ +/* + * 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.ArbitrationSelectionRule; +import io.bitsquare.trade.protocol.trade.tasks.TradeTask; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class VerifyArbitrationSelection extends TradeTask { + private static final Logger log = LoggerFactory.getLogger(VerifyArbitrationSelection.class); + + public VerifyArbitrationSelection(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + if (trade.getArbitratorAddress().equals(ArbitrationSelectionRule.select(processModel.getTakerAcceptedArbitratorAddresses(), + processModel.getOffer()))) + complete(); + else + failed("Arbitrator selection verification failed"); + } catch (Throwable t) { + failed(t); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/seller/CreateAndSignDepositTxAsSeller.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/seller/CreateAndSignDepositTxAsSeller.java new file mode 100644 index 0000000000..957de8ea7e --- /dev/null +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/seller/CreateAndSignDepositTxAsSeller.java @@ -0,0 +1,70 @@ +/* + * 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.seller; + +import io.bitsquare.btc.FeePolicy; +import io.bitsquare.btc.data.PreparedDepositTxAndOffererInputs; +import io.bitsquare.common.crypto.CryptoUtil; +import io.bitsquare.common.taskrunner.TaskRunner; +import io.bitsquare.trade.Trade; +import io.bitsquare.trade.protocol.trade.tasks.TradeTask; +import org.bitcoinj.core.Coin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class CreateAndSignDepositTxAsSeller extends TradeTask { + private static final Logger log = LoggerFactory.getLogger(CreateAndSignDepositTxAsSeller.class); + + public CreateAndSignDepositTxAsSeller(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null"); + Coin sellerInputAmount = FeePolicy.SECURITY_DEPOSIT.add(FeePolicy.TX_FEE).add(trade.getTradeAmount()); + Coin msOutputAmount = sellerInputAmount.add(FeePolicy.SECURITY_DEPOSIT); + + byte[] contractHash = CryptoUtil.getHash(trade.getContractAsJson()); + trade.setContractHash(contractHash); + PreparedDepositTxAndOffererInputs result = processModel.getTradeWalletService().offererCreatesAndSignsDepositTx( + false, + contractHash, + sellerInputAmount, + msOutputAmount, + processModel.tradingPeer.getRawInputs(), + processModel.tradingPeer.getChangeOutputValue(), + processModel.tradingPeer.getChangeOutputAddress(), + processModel.getAddressEntry(), + processModel.tradingPeer.getTradeWalletPubKey(), + processModel.getTradeWalletPubKey(), + processModel.getArbitratorPubKey(trade.getArbitratorAddress())); + + processModel.setPreparedDepositTx(result.depositTransaction); + processModel.setRawInputs(result.rawOffererInputs); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/seller/CreateDepositTxInputsAsSeller.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/seller/CreateDepositTxInputsAsSeller.java new file mode 100644 index 0000000000..f523595c97 --- /dev/null +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/seller/CreateDepositTxInputsAsSeller.java @@ -0,0 +1,57 @@ +/* + * 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.seller; + +import io.bitsquare.btc.FeePolicy; +import io.bitsquare.btc.data.InputsAndChangeOutput; +import io.bitsquare.common.taskrunner.TaskRunner; +import io.bitsquare.trade.Trade; +import io.bitsquare.trade.protocol.trade.tasks.TradeTask; +import org.bitcoinj.core.Coin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CreateDepositTxInputsAsSeller extends TradeTask { + private static final Logger log = LoggerFactory.getLogger(CreateDepositTxInputsAsSeller.class); + + public CreateDepositTxInputsAsSeller(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + if (trade.getTradeAmount() != null) { + Coin takerInputAmount = FeePolicy.SECURITY_DEPOSIT.add(FeePolicy.TX_FEE).add(trade.getTradeAmount()); + + InputsAndChangeOutput result = processModel.getTradeWalletService().takerCreatesDepositsTxInputs(takerInputAmount, processModel + .getAddressEntry()); + processModel.setRawInputs(result.rawInputs); + processModel.setChangeOutputValue(result.changeOutputValue); + processModel.setChangeOutputAddress(result.changeOutputAddress); + + complete(); + } else { + failed("trade.getTradeAmount() = null"); + } + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/seller/SignAndPublishDepositTxAsSeller.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/seller/SignAndPublishDepositTxAsSeller.java new file mode 100644 index 0000000000..062279d1c9 --- /dev/null +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/seller/SignAndPublishDepositTxAsSeller.java @@ -0,0 +1,79 @@ +/* + * 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.seller; + +import com.google.common.util.concurrent.FutureCallback; +import io.bitsquare.common.crypto.CryptoUtil; +import io.bitsquare.common.taskrunner.TaskRunner; +import io.bitsquare.trade.Trade; +import io.bitsquare.trade.protocol.trade.tasks.TradeTask; +import org.bitcoinj.core.Transaction; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Date; + +public class SignAndPublishDepositTxAsSeller extends TradeTask { + private static final Logger log = LoggerFactory.getLogger(SignAndPublishDepositTxAsSeller.class); + + public SignAndPublishDepositTxAsSeller(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + log.debug("getContractAsJson"); + log.debug("----------"); + log.debug(trade.getContractAsJson()); + log.debug("----------"); + byte[] contractHash = CryptoUtil.getHash(trade.getContractAsJson()); + trade.setContractHash(contractHash); + processModel.getTradeWalletService().takerSignsAndPublishesDepositTx( + true, + contractHash, + processModel.getPreparedDepositTx(), + processModel.tradingPeer.getRawInputs(), + processModel.getRawInputs(), + processModel.tradingPeer.getTradeWalletPubKey(), + processModel.getTradeWalletPubKey(), + processModel.getArbitratorPubKey(trade.getArbitratorAddress()), + new FutureCallback() { + @Override + public void onSuccess(Transaction transaction) { + log.trace("takerSignAndPublishTx succeeded " + transaction); + + trade.setDepositTx(transaction); + trade.setTakeOfferDate(new Date()); + trade.setState(Trade.State.DEPOSIT_PUBLISHED); + + complete(); + } + + @Override + public void onFailure(@NotNull Throwable t) { + failed(t); + } + }); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/shared/InitWaitPeriodForOpenDispute.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/shared/InitWaitPeriodForOpenDispute.java new file mode 100644 index 0000000000..15b130118c --- /dev/null +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/shared/InitWaitPeriodForOpenDispute.java @@ -0,0 +1,50 @@ +/* + * 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.shared; + +import io.bitsquare.common.taskrunner.TaskRunner; +import io.bitsquare.trade.Trade; +import io.bitsquare.trade.protocol.trade.tasks.TradeTask; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class InitWaitPeriodForOpenDispute extends TradeTask { + private static final Logger log = LoggerFactory.getLogger(InitWaitPeriodForOpenDispute.class); + + public InitWaitPeriodForOpenDispute(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + int openDisputeTimeAsBlockHeight = processModel.getTradeWalletService().getLastBlockSeenHeight() + + trade.getOffer().getPaymentMethod().getMaxTradePeriod(); + trade.setOpenDisputeTimeAsBlockHeight(openDisputeTimeAsBlockHeight); + + int checkPaymentTimeAsBlockHeight = processModel.getTradeWalletService().getLastBlockSeenHeight() + + trade.getOffer().getPaymentMethod().getMaxTradePeriod() / 2; + trade.setCheckPaymentTimeAsBlockHeight(checkPaymentTimeAsBlockHeight); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/shared/SignPayoutTx.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/shared/SignPayoutTx.java new file mode 100644 index 0000000000..bf283907f4 --- /dev/null +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/shared/SignPayoutTx.java @@ -0,0 +1,67 @@ +/* + * 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.shared; + +import io.bitsquare.btc.FeePolicy; +import io.bitsquare.common.taskrunner.TaskRunner; +import io.bitsquare.trade.Trade; +import io.bitsquare.trade.protocol.trade.tasks.TradeTask; +import org.bitcoinj.core.Coin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class SignPayoutTx extends TradeTask { + private static final Logger log = LoggerFactory.getLogger(SignPayoutTx.class); + + public SignPayoutTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null"); + Coin sellerPayoutAmount = FeePolicy.SECURITY_DEPOSIT; + Coin buyerPayoutAmount = sellerPayoutAmount.add(trade.getTradeAmount()); + + long lockTimeAsBlockHeight = processModel.getTradeWalletService().getLastBlockSeenHeight() + trade.getOffer().getPaymentMethod().getLockTime(); + trade.setLockTimeAsBlockHeight(lockTimeAsBlockHeight); + + byte[] payoutTxSignature = processModel.getTradeWalletService().sellerSignsPayoutTx( + trade.getDepositTx(), + buyerPayoutAmount, + sellerPayoutAmount, + processModel.tradingPeer.getPayoutAddressString(), + processModel.getAddressEntry(), + lockTimeAsBlockHeight, + processModel.tradingPeer.getTradeWalletPubKey(), + processModel.getTradeWalletPubKey(), + processModel.getArbitratorPubKey(trade.getArbitratorAddress())); + + processModel.setPayoutTxSignature(payoutTxSignature); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} + diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/LoadCreateOfferFeeTx.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/LoadCreateOfferFeeTx.java new file mode 100644 index 0000000000..e028b8022c --- /dev/null +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/LoadCreateOfferFeeTx.java @@ -0,0 +1,46 @@ +/* + * 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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LoadCreateOfferFeeTx extends TradeTask { + private static final Logger log = LoggerFactory.getLogger(LoadCreateOfferFeeTx.class); + + public LoadCreateOfferFeeTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + // TODO impl. not completed + ///processModel.getWalletService().findTxInBlockChain(trade.getOffer().getOfferFeePaymentTxID()); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/ProcessPublishDepositTxRequest.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/ProcessPublishDepositTxRequest.java new file mode 100644 index 0000000000..214ee743f9 --- /dev/null +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/ProcessPublishDepositTxRequest.java @@ -0,0 +1,89 @@ +/* + * 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.crypto.CryptoUtil; +import io.bitsquare.common.taskrunner.TaskRunner; +import io.bitsquare.locale.CurrencyUtil; +import io.bitsquare.payment.BlockChainAccountContractData; +import io.bitsquare.payment.PaymentAccountContractData; +import io.bitsquare.trade.Trade; +import io.bitsquare.trade.protocol.trade.messages.PublishDepositTxRequest; +import io.bitsquare.trade.protocol.trade.tasks.TradeTask; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static io.bitsquare.util.Validator.checkTradeId; +import static io.bitsquare.util.Validator.nonEmptyStringOf; + +public class ProcessPublishDepositTxRequest extends TradeTask { + private static final Logger log = LoggerFactory.getLogger(ProcessPublishDepositTxRequest.class); + + public ProcessPublishDepositTxRequest(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + log.debug("current trade state " + trade.getState()); + PublishDepositTxRequest publishDepositTxRequest = (PublishDepositTxRequest) processModel.getTradeMessage(); + checkTradeId(processModel.getId(), publishDepositTxRequest); + checkNotNull(publishDepositTxRequest); + + PaymentAccountContractData paymentAccountContractData = checkNotNull(publishDepositTxRequest.offererPaymentAccountContractData); + processModel.tradingPeer.setPaymentAccountContractData(paymentAccountContractData); + // We apply the payment ID in case its a cryptoNote coin. It is created form the hash of the trade ID + if (paymentAccountContractData instanceof BlockChainAccountContractData && + CurrencyUtil.isCryptoNoteCoin(processModel.getOffer().getCurrencyCode())) { + String paymentId = CryptoUtil.getHashAsHex(trade.getId()).substring(0, 32); + ((BlockChainAccountContractData) paymentAccountContractData).setPaymentId(paymentId); + } + + processModel.tradingPeer.setAccountId(nonEmptyStringOf(publishDepositTxRequest.offererAccountId)); + processModel.tradingPeer.setTradeWalletPubKey(checkNotNull(publishDepositTxRequest.offererTradeWalletPubKey)); + processModel.tradingPeer.setContractAsJson(nonEmptyStringOf(publishDepositTxRequest.offererContractAsJson)); + processModel.tradingPeer.setContractSignature(nonEmptyStringOf(publishDepositTxRequest.offererContractSignature)); + processModel.tradingPeer.setPayoutAddressString(nonEmptyStringOf(publishDepositTxRequest.offererPayoutAddressString)); + processModel.tradingPeer.setRawInputs(checkNotNull(publishDepositTxRequest.offererInputs)); + processModel.setPreparedDepositTx(checkNotNull(publishDepositTxRequest.preparedDepositTx)); + checkArgument(publishDepositTxRequest.offererInputs.size() > 0); + if (publishDepositTxRequest.openDisputeTimeAsBlockHeight != 0) { + trade.setOpenDisputeTimeAsBlockHeight(publishDepositTxRequest.openDisputeTimeAsBlockHeight); + } else { + failed("waitPeriodForOpenDisputeAsBlockHeight = 0"); + } + + if (publishDepositTxRequest.checkPaymentTimeAsBlockHeight != 0) { + trade.setCheckPaymentTimeAsBlockHeight(publishDepositTxRequest.checkPaymentTimeAsBlockHeight); + } else { + failed("notificationTimeAsBlockHeight = 0"); + } + + // update to the latest peer address of our peer if the message is correct + trade.setTradingPeerAddress(processModel.getTempTradingPeerAddress()); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/SelectArbitrator.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/SelectArbitrator.java new file mode 100644 index 0000000000..4b7aee180e --- /dev/null +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/SelectArbitrator.java @@ -0,0 +1,46 @@ +/* + * 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.ArbitrationSelectionRule; +import io.bitsquare.trade.protocol.trade.tasks.TradeTask; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SelectArbitrator extends TradeTask { + private static final Logger log = LoggerFactory.getLogger(SelectArbitrator.class); + + public SelectArbitrator(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + trade.setArbitratorAddress(ArbitrationSelectionRule.select(processModel.getUser().getAcceptedArbitratorAddresses(), processModel.getOffer())); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/SendDepositTxPublishedMessage.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/SendDepositTxPublishedMessage.java new file mode 100644 index 0000000000..76da9d0106 --- /dev/null +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/SendDepositTxPublishedMessage.java @@ -0,0 +1,80 @@ +/* + * 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.p2p.messaging.SendMailboxMessageListener; +import io.bitsquare.trade.Trade; +import io.bitsquare.trade.protocol.trade.messages.DepositTxPublishedMessage; +import io.bitsquare.trade.protocol.trade.tasks.TradeTask; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SendDepositTxPublishedMessage extends TradeTask { + private static final Logger log = LoggerFactory.getLogger(SendDepositTxPublishedMessage.class); + + public SendDepositTxPublishedMessage(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + if (trade.getDepositTx() != null) { + DepositTxPublishedMessage tradeMessage = new DepositTxPublishedMessage(processModel.getId(), + trade.getDepositTx().bitcoinSerialize(), + processModel.getMyAddress()); + + processModel.getP2PService().sendEncryptedMailboxMessage( + trade.getTradingPeerAddress(), + processModel.tradingPeer.getPubKeyRing(), + tradeMessage, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.trace("Message arrived at peer."); + trade.setState(Trade.State.DEPOSIT_PUBLISHED_MSG_SENT); + complete(); + } + + @Override + public void onStoredInMailbox() { + log.trace("Message stored in mailbox."); + trade.setState(Trade.State.DEPOSIT_PUBLISHED_MSG_SENT); + complete(); + } + + @Override + public void onFault() { + appendToErrorMessage("DepositTxPublishedMessage sending failed"); + failed(); + } + } + ); + } else { + log.error("trade.getDepositTx() = " + trade.getDepositTx()); + failed("DepositTx is null"); + } + } catch (Throwable t) { + failed(t); + } + } + + +} diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/SendPayDepositRequest.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/SendPayDepositRequest.java new file mode 100644 index 0000000000..c05d9fc066 --- /dev/null +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/SendPayDepositRequest.java @@ -0,0 +1,89 @@ +/* + * 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.p2p.messaging.SendMailboxMessageListener; +import io.bitsquare.trade.Trade; +import io.bitsquare.trade.protocol.trade.messages.PayDepositRequest; +import io.bitsquare.trade.protocol.trade.tasks.TradeTask; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SendPayDepositRequest extends TradeTask { + private static final Logger log = LoggerFactory.getLogger(SendPayDepositRequest.class); + + public SendPayDepositRequest(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + if (processModel.getTakeOfferFeeTx() != null) { + PayDepositRequest payDepositRequest = new PayDepositRequest( + processModel.getMyAddress(), + processModel.getId(), + trade.getTradeAmount().value, + processModel.getRawInputs(), + processModel.getChangeOutputValue(), + processModel.getChangeOutputAddress(), + processModel.getTradeWalletPubKey(), + processModel.getAddressEntry().getAddressString(), + processModel.getPubKeyRing(), + processModel.getPaymentAccountContractData(trade), + processModel.getAccountId(), + processModel.getTakeOfferFeeTx().getHashAsString(), + processModel.getUser().getAcceptedArbitratorAddresses(), + trade.getArbitratorAddress() + ); + + processModel.getP2PService().sendEncryptedMailboxMessage( + trade.getTradingPeerAddress(), + processModel.tradingPeer.getPubKeyRing(), + payDepositRequest, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.trace("Message arrived at peer."); + complete(); + } + + @Override + public void onStoredInMailbox() { + log.trace("Message stored in mailbox."); + complete(); + } + + @Override + public void onFault() { + appendToErrorMessage("PayDepositRequest sending failed"); + failed(); + } + } + ); + } else { + log.error("processModel.getTakeOfferFeeTx() = " + processModel.getTakeOfferFeeTx()); + failed("TakeOfferFeeTx is null"); + } + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/VerifyAndSignContract.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/VerifyAndSignContract.java new file mode 100644 index 0000000000..32396a0c06 --- /dev/null +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/VerifyAndSignContract.java @@ -0,0 +1,98 @@ +/* + * 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.crypto.CryptoUtil; +import io.bitsquare.common.taskrunner.TaskRunner; +import io.bitsquare.common.util.Utilities; +import io.bitsquare.p2p.Address; +import io.bitsquare.payment.PaymentAccountContractData; +import io.bitsquare.trade.Contract; +import io.bitsquare.trade.SellerAsTakerTrade; +import io.bitsquare.trade.Trade; +import io.bitsquare.trade.protocol.trade.TradingPeer; +import io.bitsquare.trade.protocol.trade.tasks.TradeTask; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class VerifyAndSignContract extends TradeTask { + private static final Logger log = LoggerFactory.getLogger(VerifyAndSignContract.class); + + public VerifyAndSignContract(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + if (processModel.getTakeOfferFeeTx() != null) { + TradingPeer offerer = processModel.tradingPeer; + PaymentAccountContractData offererPaymentAccountContractData = offerer.getPaymentAccountContractData(); + PaymentAccountContractData takerPaymentAccountContractData = processModel.getPaymentAccountContractData(trade); + + boolean isBuyerOffererOrSellerTaker = trade instanceof SellerAsTakerTrade; + Address buyerAddress = isBuyerOffererOrSellerTaker ? processModel.getTempTradingPeerAddress() : processModel.getMyAddress(); + Address sellerAddress = isBuyerOffererOrSellerTaker ? processModel.getMyAddress() : processModel.getTempTradingPeerAddress(); + log.debug("isBuyerOffererOrSellerTaker " + isBuyerOffererOrSellerTaker); + log.debug("buyerAddress " + buyerAddress); + log.debug("sellerAddress " + sellerAddress); + + Contract contract = new Contract( + processModel.getOffer(), + trade.getTradeAmount(), + processModel.getTakeOfferFeeTx().getHashAsString(), + buyerAddress, + sellerAddress, + trade.getArbitratorAddress(), + isBuyerOffererOrSellerTaker, + offerer.getAccountId(), + processModel.getAccountId(), + offererPaymentAccountContractData, + takerPaymentAccountContractData, + offerer.getPubKeyRing(), + processModel.getPubKeyRing(), + offerer.getPayoutAddressString(), + processModel.getAddressEntry().getAddressString(), + offerer.getTradeWalletPubKey(), + processModel.getTradeWalletPubKey() + ); + String contractAsJson = Utilities.objectToJson(contract); + String signature = CryptoUtil.signMessage(processModel.getKeyRing().getMsgSignatureKeyPair().getPrivate(), contractAsJson); + trade.setContract(contract); + trade.setContractAsJson(contractAsJson); + trade.setTakerContractSignature(signature); + + try { + CryptoUtil.verifyMessage(offerer.getPubKeyRing().getMsgSignaturePubKey(), + contractAsJson, + offerer.getContractSignature()); + } catch (Throwable t) { + failed("Signature verification failed. " + t.getMessage()); + } + + complete(); + } else { + failed("processModel.getTakeOfferFeeTx() = null"); + } + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/io/bitsquare/user/BlockChainExplorer.java b/core/src/main/java/io/bitsquare/user/BlockChainExplorer.java new file mode 100644 index 0000000000..bfe8ae5909 --- /dev/null +++ b/core/src/main/java/io/bitsquare/user/BlockChainExplorer.java @@ -0,0 +1,41 @@ +/* + * 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.user; + +import io.bitsquare.app.Version; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Serializable; + +public class BlockChainExplorer implements Serializable { + // That object is saved to disc. We need to take care of changes to not break deserialization. + private static final long serialVersionUID = Version.LOCAL_DB_VERSION; + + private static final Logger log = LoggerFactory.getLogger(BlockChainExplorer.class); + + public final String name; + public final String txUrl; + public final String addressUrl; + + public BlockChainExplorer(String name, String txUrl, String addressUrl) { + this.name = name; + this.txUrl = txUrl; + this.addressUrl = addressUrl; + } +} diff --git a/core/src/main/java/io/bitsquare/user/PopupId.java b/core/src/main/java/io/bitsquare/user/PopupId.java new file mode 100644 index 0000000000..47cce412b4 --- /dev/null +++ b/core/src/main/java/io/bitsquare/user/PopupId.java @@ -0,0 +1,27 @@ +/* + * 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.user; + +public class PopupId { + + // We don't use an enum because it would break updates if we add a new item in a new version + + public static String SEC_DEPOSIT = "SEC_DEPOSIT"; + public static String TRADE_WALLET = "TRADE_WALLET"; + +} diff --git a/core/src/main/resources/logback.xml b/core/src/main/resources/logback.xml new file mode 100644 index 0000000000..528b3cfd2b --- /dev/null +++ b/core/src/main/resources/logback.xml @@ -0,0 +1,63 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{15} - %msg %xEx%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/gui/src/main/java/io/bitsquare/gui/common/model/ActivatableDataModel.java b/gui/src/main/java/io/bitsquare/gui/common/model/ActivatableDataModel.java new file mode 100644 index 0000000000..edc1f7c2f9 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/common/model/ActivatableDataModel.java @@ -0,0 +1,41 @@ +/* + * 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.common.model; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class ActivatableDataModel implements Activatable, DataModel { + protected final Logger log = LoggerFactory.getLogger(this.getClass()); + + @Override + public final void _activate() { + this.activate(); + } + + protected void activate() { + } + + @Override + public final void _deactivate() { + this.deactivate(); + } + + protected void deactivate() { + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/common/model/ActivatableViewModel.java b/gui/src/main/java/io/bitsquare/gui/common/model/ActivatableViewModel.java new file mode 100644 index 0000000000..dc12f2cf5e --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/common/model/ActivatableViewModel.java @@ -0,0 +1,41 @@ +/* + * 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.common.model; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class ActivatableViewModel implements Activatable, ViewModel { + protected final Logger log = LoggerFactory.getLogger(this.getClass()); + + @Override + public final void _activate() { + this.activate(); + } + + protected void activate() { + } + + @Override + public final void _deactivate() { + this.deactivate(); + } + + protected void deactivate() { + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/components/PasswordTextField.java b/gui/src/main/java/io/bitsquare/gui/components/PasswordTextField.java new file mode 100644 index 0000000000..b9ae94265b --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/components/PasswordTextField.java @@ -0,0 +1,172 @@ +/* + * 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.components; + +import io.bitsquare.gui.util.validation.InputValidator; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.geometry.Insets; +import javafx.geometry.Point2D; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.effect.BlurType; +import javafx.scene.effect.DropShadow; +import javafx.scene.effect.Effect; +import javafx.scene.layout.Region; +import javafx.scene.paint.Color; +import javafx.stage.Window; +import org.controlsfx.control.PopOver; + +public class PasswordTextField extends PasswordField { + + private final Effect invalidEffect = new DropShadow(BlurType.THREE_PASS_BOX, Color.RED, 4, 0.0, 0, 0); + + private final ObjectProperty validationResult = new SimpleObjectProperty<> + (new InputValidator.ValidationResult(true)); + + private static PopOver errorMessageDisplay; + private Region layoutReference = this; + + public InputValidator getValidator() { + return validator; + } + + public void setValidator(InputValidator validator) { + this.validator = validator; + } + + private InputValidator validator; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Static + /////////////////////////////////////////////////////////////////////////////////////////// + + private static void hideErrorMessageDisplay() { + if (errorMessageDisplay != null) + errorMessageDisplay.hide(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public PasswordTextField() { + super(); + + validationResult.addListener((ov, oldValue, newValue) -> { + if (newValue != null) { + setEffect(newValue.isValid ? null : invalidEffect); + + if (newValue.isValid) + hideErrorMessageDisplay(); + else + applyErrorMessage(newValue); + } + }); + + sceneProperty().addListener((ov, oldValue, newValue) -> { + // we got removed from the scene so hide the popup (if open) + if (newValue == null) + hideErrorMessageDisplay(); + }); + + focusedProperty().addListener((o, oldValue, newValue) -> { + if (oldValue && !newValue && validator != null) + validationResult.set(validator.validate(getText())); + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public methods + /////////////////////////////////////////////////////////////////////////////////////////// + + public void resetValidation() { + setEffect(null); + hideErrorMessageDisplay(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Setters + /////////////////////////////////////////////////////////////////////////////////////////// + + /** + * @param layoutReference The node used as reference for positioning. If not set explicitly the + * ValidatingTextField instance is used. + */ + public void setLayoutReference(Region layoutReference) { + this.layoutReference = layoutReference; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + public ObjectProperty validationResultProperty() { + return validationResult; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private methods + /////////////////////////////////////////////////////////////////////////////////////////// + + private void applyErrorMessage(InputValidator.ValidationResult validationResult) { + if (errorMessageDisplay != null) + errorMessageDisplay.hide(); + + if (!validationResult.isValid) { + createErrorPopOver(validationResult.errorMessage); + if (getScene() != null) + errorMessageDisplay.show(getScene().getWindow(), getErrorPopupPosition().getX(), + getErrorPopupPosition().getY()); + + if (errorMessageDisplay != null) + errorMessageDisplay.setDetached(false); + } + } + + private Point2D getErrorPopupPosition() { + Window window = getScene().getWindow(); + Point2D point; + point = layoutReference.localToScene(0, 0); + double x = Math.floor(point.getX() + window.getX() + layoutReference.getWidth() + 20 - getPadding().getLeft() - + getPadding().getRight()); + double y = Math.floor(point.getY() + window.getY() + getHeight() / 2 - getPadding().getTop() - getPadding() + .getBottom()); + return new Point2D(x, y); + } + + + private static void createErrorPopOver(String errorMessage) { + Label errorLabel = new Label(errorMessage); + errorLabel.setId("validation-error"); + errorLabel.setPadding(new Insets(0, 10, 0, 10)); + errorLabel.setOnMouseClicked(e -> hideErrorMessageDisplay()); + + errorMessageDisplay = new PopOver(errorLabel); + errorMessageDisplay.setDetachable(true); + errorMessageDisplay.setDetachedTitle("Close"); + errorMessageDisplay.setArrowIndent(5); + } + +} \ No newline at end of file diff --git a/gui/src/main/java/io/bitsquare/gui/components/TableGroupHeadline.java b/gui/src/main/java/io/bitsquare/gui/components/TableGroupHeadline.java new file mode 100644 index 0000000000..965a9e0354 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/components/TableGroupHeadline.java @@ -0,0 +1,84 @@ +/* + * 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.components; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.geometry.Insets; +import javafx.scene.control.Label; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Pane; +import javafx.scene.layout.StackPane; + +public class TableGroupHeadline extends Pane { + + private final Label label; + private final StringProperty text = new SimpleStringProperty(); + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public TableGroupHeadline() { + this(""); + } + + public TableGroupHeadline(String title) { + text.set(title); + + GridPane.setMargin(this, new Insets(-10, -10, -10, -10)); + GridPane.setColumnSpan(this, 2); + + Pane bg = new StackPane(); + bg.setId("table-group-headline"); + bg.prefWidthProperty().bind(widthProperty()); + bg.prefHeightProperty().bind(heightProperty()); + + label = new Label(); + label.textProperty().bind(text); + label.setLayoutX(8); + label.setLayoutY(-8); + label.setPadding(new Insets(0, 7, 0, 5)); + setActive(); + getChildren().addAll(bg, label); + } + + public void setInactive() { + setId("titled-group-bg"); + label.setId("titled-group-bg-label"); + } + + private void setActive() { + setId("titled-group-bg-active"); + label.setId("titled-group-bg-label-active"); + } + + public String getText() { + return text.get(); + } + + public StringProperty textProperty() { + return text; + } + + public void setText(String text) { + this.text.set(text); + } + + +} diff --git a/gui/src/main/java/io/bitsquare/gui/components/paymentmethods/AliPayForm.java b/gui/src/main/java/io/bitsquare/gui/components/paymentmethods/AliPayForm.java new file mode 100644 index 0000000000..f1f4267a41 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/components/paymentmethods/AliPayForm.java @@ -0,0 +1,98 @@ +/* + * 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.components.paymentmethods; + +import io.bitsquare.gui.components.InputTextField; +import io.bitsquare.gui.util.Layout; +import io.bitsquare.gui.util.validation.AliPayValidator; +import io.bitsquare.gui.util.validation.InputValidator; +import io.bitsquare.locale.BSResources; +import io.bitsquare.payment.AliPayAccount; +import io.bitsquare.payment.AliPayAccountContractData; +import io.bitsquare.payment.PaymentAccount; +import io.bitsquare.payment.PaymentAccountContractData; +import javafx.scene.layout.GridPane; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static io.bitsquare.gui.util.FormBuilder.*; + +public class AliPayForm extends PaymentMethodForm { + private static final Logger log = LoggerFactory.getLogger(AliPayForm.class); + + private final AliPayAccount aliPayAccount; + private final AliPayValidator aliPayValidator; + private InputTextField accountNrInputTextField; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountContractData paymentAccountContractData) { + addLabelTextField(gridPane, ++gridRow, "Payment method:", BSResources.get(paymentAccountContractData.getPaymentMethodName())); + addLabelTextFieldWithCopyIcon(gridPane, ++gridRow, "Account nr.:", ((AliPayAccountContractData) paymentAccountContractData).getAccountNr()); + addAllowedPeriod(gridPane, ++gridRow, paymentAccountContractData); + return gridRow; + } + + public AliPayForm(PaymentAccount paymentAccount, AliPayValidator aliPayValidator, InputValidator inputValidator, GridPane gridPane, int gridRow) { + super(paymentAccount, inputValidator, gridPane, gridRow); + this.aliPayAccount = (AliPayAccount) paymentAccount; + this.aliPayValidator = aliPayValidator; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + accountNrInputTextField = addLabelInputTextField(gridPane, ++gridRow, "Account nr.:").second; + accountNrInputTextField.setValidator(aliPayValidator); + accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + aliPayAccount.setAccountNr(newValue); + updateFromInputs(); + }); + + addLabelTextField(gridPane, ++gridRow, "Currency:", aliPayAccount.getSingleTradeCurrency().getCodeAndName()); + addAllowedPeriod(); + addAccountNameTextFieldWithAutoFillCheckBox(); + } + + @Override + protected void autoFillNameTextField() { + if (autoFillCheckBox != null && autoFillCheckBox.isSelected()) { + String accountNr = accountNrInputTextField.getText(); + accountNr = accountNr.substring(0, Math.min(5, accountNr.length())) + "..."; + String method = BSResources.get(paymentAccount.getPaymentMethod().getId()); + accountNameTextField.setText(method.concat(", ").concat(accountNr)); + } + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + addLabelTextField(gridPane, gridRow, "Account name:", aliPayAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addLabelTextField(gridPane, ++gridRow, "Payment method:", BSResources.get(aliPayAccount.getPaymentMethod().getId())); + addLabelTextField(gridPane, ++gridRow, "Account nr.:", aliPayAccount.getAccountNr()); + addLabelTextField(gridPane, ++gridRow, "Currency:", aliPayAccount.getSingleTradeCurrency().getCodeAndName()); + addAllowedPeriod(); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && aliPayValidator.validate(aliPayAccount.getAccountNr()).isValid + && aliPayAccount.getTradeCurrencies().size() > 0); + } + +} diff --git a/gui/src/main/java/io/bitsquare/gui/components/paymentmethods/BlockChainForm.java b/gui/src/main/java/io/bitsquare/gui/components/paymentmethods/BlockChainForm.java new file mode 100644 index 0000000000..ef91de5276 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/components/paymentmethods/BlockChainForm.java @@ -0,0 +1,135 @@ +/* + * 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.components.paymentmethods; + +import io.bitsquare.gui.components.InputTextField; +import io.bitsquare.gui.util.Layout; +import io.bitsquare.gui.util.validation.AltCoinAddressValidator; +import io.bitsquare.gui.util.validation.InputValidator; +import io.bitsquare.locale.BSResources; +import io.bitsquare.locale.CurrencyUtil; +import io.bitsquare.locale.TradeCurrency; +import io.bitsquare.payment.BlockChainAccount; +import io.bitsquare.payment.BlockChainAccountContractData; +import io.bitsquare.payment.PaymentAccount; +import io.bitsquare.payment.PaymentAccountContractData; +import javafx.collections.FXCollections; +import javafx.scene.control.ComboBox; +import javafx.scene.layout.GridPane; +import javafx.util.StringConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static io.bitsquare.gui.util.FormBuilder.*; + +public class BlockChainForm extends PaymentMethodForm { + private static final Logger log = LoggerFactory.getLogger(BlockChainForm.class); + + private final BlockChainAccount blockChainAccount; + private final AltCoinAddressValidator altCoinAddressValidator; + private InputTextField addressInputTextField; + + private ComboBox currencyComboBox; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountContractData paymentAccountContractData) { + addLabelTextField(gridPane, ++gridRow, "Payment method:", BSResources.get(paymentAccountContractData.getPaymentMethodName())); + addLabelTextFieldWithCopyIcon(gridPane, ++gridRow, "Address:", ((BlockChainAccountContractData) paymentAccountContractData).getAddress()); + if (paymentAccountContractData instanceof BlockChainAccountContractData && + ((BlockChainAccountContractData) paymentAccountContractData).getPaymentId() != null) + addLabelTextFieldWithCopyIcon(gridPane, ++gridRow, "Payment ID:", ((BlockChainAccountContractData) paymentAccountContractData).getPaymentId()); + + addAllowedPeriod(gridPane, ++gridRow, paymentAccountContractData); + return gridRow; + } + + public BlockChainForm(PaymentAccount paymentAccount, AltCoinAddressValidator altCoinAddressValidator, InputValidator inputValidator, GridPane gridPane, + int gridRow) { + super(paymentAccount, inputValidator, gridPane, gridRow); + this.blockChainAccount = (BlockChainAccount) paymentAccount; + this.altCoinAddressValidator = altCoinAddressValidator; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + addTradeCurrencyComboBox(); + currencyComboBox.setPrefWidth(250); + addressInputTextField = addLabelInputTextField(gridPane, ++gridRow, "Address:").second; + addressInputTextField.setValidator(altCoinAddressValidator); + + addressInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + blockChainAccount.setAddress(newValue); + updateFromInputs(); + }); + + addAllowedPeriod(); + addAccountNameTextFieldWithAutoFillCheckBox(); + } + + @Override + protected void autoFillNameTextField() { + if (autoFillCheckBox != null && autoFillCheckBox.isSelected()) { + String method = BSResources.get(paymentAccount.getPaymentMethod().getId()); + String address = addressInputTextField.getText(); + address = address.substring(0, Math.min(9, address.length())) + "..."; + String currency = paymentAccount.getSingleTradeCurrency() != null ? paymentAccount.getSingleTradeCurrency().getCode() : "?"; + accountNameTextField.setText(method.concat(", ").concat(currency).concat(", ").concat(address)); + } + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + addLabelTextField(gridPane, gridRow, "Account name:", blockChainAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addLabelTextField(gridPane, ++gridRow, "Payment method:", BSResources.get(blockChainAccount.getPaymentMethod().getId())); + addLabelTextField(gridPane, ++gridRow, "Address:", blockChainAccount.getAddress()); + addLabelTextField(gridPane, ++gridRow, "Crypto currency:", blockChainAccount.getSingleTradeCurrency().getCodeAndName()); + addAllowedPeriod(); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && altCoinAddressValidator.validate(blockChainAccount.getAddress()).isValid + && blockChainAccount.getSingleTradeCurrency() != null); + } + + @Override + protected void addTradeCurrencyComboBox() { + currencyComboBox = addLabelComboBox(gridPane, ++gridRow, "Crypto currency:").second; + currencyComboBox.setPromptText("Select crypto currency"); + currencyComboBox.setItems(FXCollections.observableArrayList(CurrencyUtil.getSortedCryptoCurrencies())); + currencyComboBox.setVisibleRowCount(Math.min(currencyComboBox.getItems().size(), 20)); + currencyComboBox.setConverter(new StringConverter() { + @Override + public String toString(TradeCurrency tradeCurrency) { + return tradeCurrency.getCodeAndName(); + } + + @Override + public TradeCurrency fromString(String s) { + return null; + } + }); + currencyComboBox.setOnAction(e -> { + paymentAccount.setSingleTradeCurrency(currencyComboBox.getSelectionModel().getSelectedItem()); + updateFromInputs(); + }); + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/components/paymentmethods/OKPayForm.java b/gui/src/main/java/io/bitsquare/gui/components/paymentmethods/OKPayForm.java new file mode 100644 index 0000000000..d235a41953 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/components/paymentmethods/OKPayForm.java @@ -0,0 +1,140 @@ +/* + * 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.components.paymentmethods; + +import io.bitsquare.gui.components.InputTextField; +import io.bitsquare.gui.util.Layout; +import io.bitsquare.gui.util.validation.InputValidator; +import io.bitsquare.gui.util.validation.OKPayValidator; +import io.bitsquare.locale.BSResources; +import io.bitsquare.locale.CurrencyUtil; +import io.bitsquare.payment.OKPayAccount; +import io.bitsquare.payment.OKPayAccountContractData; +import io.bitsquare.payment.PaymentAccount; +import io.bitsquare.payment.PaymentAccountContractData; +import javafx.geometry.Insets; +import javafx.geometry.VPos; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Label; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.GridPane; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static io.bitsquare.gui.util.FormBuilder.*; + +public class OKPayForm extends PaymentMethodForm { + private static final Logger log = LoggerFactory.getLogger(OKPayForm.class); + + private final OKPayAccount okPayAccount; + private final OKPayValidator okPayValidator; + private InputTextField accountNrInputTextField; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountContractData paymentAccountContractData) { + addLabelTextField(gridPane, ++gridRow, "Payment method:", BSResources.get(paymentAccountContractData.getPaymentMethodName())); + addLabelTextFieldWithCopyIcon(gridPane, ++gridRow, "Account nr.:", ((OKPayAccountContractData) paymentAccountContractData).getAccountNr()); + addAllowedPeriod(gridPane, ++gridRow, paymentAccountContractData); + return gridRow; + } + + public OKPayForm(PaymentAccount paymentAccount, OKPayValidator okPayValidator, InputValidator inputValidator, GridPane gridPane, int gridRow) { + super(paymentAccount, inputValidator, gridPane, gridRow); + this.okPayAccount = (OKPayAccount) paymentAccount; + this.okPayValidator = okPayValidator; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + accountNrInputTextField = addLabelInputTextField(gridPane, ++gridRow, "Account nr.:").second; + accountNrInputTextField.setValidator(okPayValidator); + accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + okPayAccount.setAccountNr(newValue); + updateFromInputs(); + }); + + addCurrenciesGrid(true); + addAllowedPeriod(); + addAccountNameTextFieldWithAutoFillCheckBox(); + } + + private void addCurrenciesGrid(boolean isEditable) { + Label label = addLabel(gridPane, ++gridRow, "Supported OKPay currencies:", 0); + GridPane.setValignment(label, VPos.TOP); + FlowPane flowPane = new FlowPane(); + flowPane.setPadding(new Insets(10, 10, 10, 10)); + flowPane.setVgap(10); + flowPane.setHgap(10); + + if (isEditable) + flowPane.setId("flowpane-checkboxes-bg"); + else + flowPane.setId("flowpane-checkboxes-non-editable-bg"); + + CurrencyUtil.getAllOKPayCurrencies().stream().forEach(e -> + { + CheckBox checkBox = new CheckBox(e.getCode()); + checkBox.setMouseTransparent(!isEditable); + checkBox.setSelected(okPayAccount.getTradeCurrencies().contains(e)); + checkBox.setMinWidth(60); + checkBox.setMaxWidth(checkBox.getMinWidth()); + checkBox.setOnAction(event -> { + if (checkBox.isSelected()) + okPayAccount.addCurrency(e); + else + okPayAccount.removeCurrency(e); + + updateAllInputsValid(); + }); + flowPane.getChildren().add(checkBox); + }); + + GridPane.setRowIndex(flowPane, gridRow); + GridPane.setColumnIndex(flowPane, 1); + gridPane.getChildren().add(flowPane); + } + + @Override + protected void autoFillNameTextField() { + if (autoFillCheckBox != null && autoFillCheckBox.isSelected()) { + String accountNr = accountNrInputTextField.getText(); + accountNr = accountNr.substring(0, Math.min(5, accountNr.length())) + "..."; + String method = BSResources.get(paymentAccount.getPaymentMethod().getId()); + accountNameTextField.setText(method.concat(", ").concat(accountNr)); + } + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + addLabelTextField(gridPane, gridRow, "Account name:", okPayAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addLabelTextField(gridPane, ++gridRow, "Payment method:", BSResources.get(okPayAccount.getPaymentMethod().getId())); + addLabelTextField(gridPane, ++gridRow, "Account nr.:", okPayAccount.getAccountNr()); + addAllowedPeriod(); + addCurrenciesGrid(false); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && okPayValidator.validate(okPayAccount.getAccountNr()).isValid + && okPayAccount.getTradeCurrencies().size() > 0); + } + +} diff --git a/gui/src/main/java/io/bitsquare/gui/components/paymentmethods/PaymentMethodForm.java b/gui/src/main/java/io/bitsquare/gui/components/paymentmethods/PaymentMethodForm.java new file mode 100644 index 0000000000..4ea4ef6c8c --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/components/paymentmethods/PaymentMethodForm.java @@ -0,0 +1,154 @@ +/* + * 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.components.paymentmethods; + +import io.bitsquare.common.util.Tuple3; +import io.bitsquare.gui.components.InputTextField; +import io.bitsquare.gui.util.validation.InputValidator; +import io.bitsquare.locale.CurrencyUtil; +import io.bitsquare.locale.TradeCurrency; +import io.bitsquare.payment.PaymentAccount; +import io.bitsquare.payment.PaymentAccountContractData; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.collections.FXCollections; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.layout.GridPane; +import javafx.util.StringConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static io.bitsquare.gui.util.FormBuilder.*; + +public abstract class PaymentMethodForm { + private static final Logger log = LoggerFactory.getLogger(PaymentMethodForm.class); + + protected final PaymentAccount paymentAccount; + protected final InputValidator inputValidator; + protected final GridPane gridPane; + protected int gridRow; + protected final BooleanProperty allInputsValid = new SimpleBooleanProperty(); + + protected int gridRowFrom; + protected InputTextField accountNameTextField; + protected CheckBox autoFillCheckBox; + private ComboBox currencyComboBox; + + public PaymentMethodForm(PaymentAccount paymentAccount, InputValidator inputValidator, GridPane gridPane, int gridRow) { + this.paymentAccount = paymentAccount; + this.inputValidator = inputValidator; + this.gridPane = gridPane; + this.gridRow = gridRow; + } + + protected void addTradeCurrencyComboBox() { + currencyComboBox = addLabelComboBox(gridPane, ++gridRow, "Currency:").second; + currencyComboBox.setPromptText("Select currency"); + currencyComboBox.setItems(FXCollections.observableArrayList(CurrencyUtil.getAllSortedCurrencies())); + currencyComboBox.setConverter(new StringConverter() { + @Override + public String toString(TradeCurrency tradeCurrency) { + return tradeCurrency.getCodeAndName(); + } + + @Override + public TradeCurrency fromString(String s) { + return null; + } + }); + currencyComboBox.setOnAction(e -> { + paymentAccount.setSingleTradeCurrency(currencyComboBox.getSelectionModel().getSelectedItem()); + updateFromInputs(); + }); + } + + protected void addAccountNameTextFieldWithAutoFillCheckBox() { + Tuple3 tuple = addLabelInputTextFieldCheckBox(gridPane, ++gridRow, "Account name:", "Auto-fill"); + accountNameTextField = tuple.second; + accountNameTextField.setPrefWidth(250); + accountNameTextField.setEditable(false); + accountNameTextField.setValidator(inputValidator); + accountNameTextField.textProperty().addListener((ov, oldValue, newValue) -> { + paymentAccount.setAccountName(newValue); + updateAllInputsValid(); + }); + autoFillCheckBox = tuple.third; + autoFillCheckBox.setSelected(true); + autoFillCheckBox.setOnAction(e -> { + accountNameTextField.setEditable(!autoFillCheckBox.isSelected()); + autoFillNameTextField(); + }); + } + + static void addAllowedPeriod(GridPane gridPane, int gridRow, PaymentAccountContractData paymentAccountContractData) { + long hours = paymentAccountContractData.getMaxTradePeriod() / 6; + String displayText = hours + " hours"; + if (hours == 24) + displayText = "1 day"; + if (hours > 24) + displayText = hours / 24 + " days"; + + addLabelTextField(gridPane, gridRow, "Max. allowed trade period:", displayText); + } + + protected void addAllowedPeriod() { + long hours = paymentAccount.getPaymentMethod().getMaxTradePeriod() / 6; + String displayText = hours + " hours"; + if (hours == 24) + displayText = "1 day"; + if (hours > 24) + displayText = hours / 24 + " days"; + + addLabelTextField(gridPane, ++gridRow, "Max. allowed trade period:", displayText); + } + + abstract protected void autoFillNameTextField(); + + abstract public void addFormForAddAccount(); + + abstract public void addFormForDisplayAccount(); + + protected abstract void updateAllInputsValid(); + + public void updateFromInputs() { + autoFillNameTextField(); + updateAllInputsValid(); + } + + public boolean isAccountNameValid() { + return inputValidator.validate(paymentAccount.getAccountName()).isValid; + } + + public int getGridRow() { + return gridRow; + } + + public int getRowSpan() { + return gridRow - gridRowFrom + 1; + } + + public PaymentAccount getPaymentAccount() { + return paymentAccount; + } + + public BooleanProperty allInputsValidProperty() { + return allInputsValid; + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/components/paymentmethods/PerfectMoneyForm.java b/gui/src/main/java/io/bitsquare/gui/components/paymentmethods/PerfectMoneyForm.java new file mode 100644 index 0000000000..309adb5ec0 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/components/paymentmethods/PerfectMoneyForm.java @@ -0,0 +1,109 @@ +/* + * 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.components.paymentmethods; + +import io.bitsquare.gui.components.InputTextField; +import io.bitsquare.gui.util.Layout; +import io.bitsquare.gui.util.validation.InputValidator; +import io.bitsquare.gui.util.validation.PerfectMoneyValidator; +import io.bitsquare.locale.BSResources; +import io.bitsquare.payment.PaymentAccount; +import io.bitsquare.payment.PaymentAccountContractData; +import io.bitsquare.payment.PerfectMoneyAccount; +import io.bitsquare.payment.PerfectMoneyAccountContractData; +import javafx.scene.layout.GridPane; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static io.bitsquare.gui.util.FormBuilder.*; + +public class PerfectMoneyForm extends PaymentMethodForm { + private static final Logger log = LoggerFactory.getLogger(PerfectMoneyForm.class); + + private final PerfectMoneyAccount perfectMoneyAccount; + private final PerfectMoneyValidator perfectMoneyValidator; + private InputTextField accountNrInputTextField; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountContractData paymentAccountContractData) { + addLabelTextField(gridPane, ++gridRow, "Payment method:", BSResources.get(paymentAccountContractData.getPaymentMethodName())); + addLabelTextFieldWithCopyIcon(gridPane, ++gridRow, "Account holder name:", ((PerfectMoneyAccountContractData) paymentAccountContractData) + .getHolderName()); + addLabelTextFieldWithCopyIcon(gridPane, ++gridRow, "Account nr.:", ((PerfectMoneyAccountContractData) paymentAccountContractData).getAccountNr()); + addAllowedPeriod(gridPane, ++gridRow, paymentAccountContractData); + return gridRow; + } + + public PerfectMoneyForm(PaymentAccount paymentAccount, PerfectMoneyValidator perfectMoneyValidator, InputValidator inputValidator, GridPane gridPane, int + gridRow) { + super(paymentAccount, inputValidator, gridPane, gridRow); + this.perfectMoneyAccount = (PerfectMoneyAccount) paymentAccount; + this.perfectMoneyValidator = perfectMoneyValidator; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + InputTextField holderNameInputTextField = addLabelInputTextField(gridPane, ++gridRow, "Account holder name:").second; + holderNameInputTextField.setValidator(inputValidator); + holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + perfectMoneyAccount.setHolderName(newValue); + updateFromInputs(); + }); + + accountNrInputTextField = addLabelInputTextField(gridPane, ++gridRow, "Account nr.:").second; + accountNrInputTextField.setValidator(perfectMoneyValidator); + accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + perfectMoneyAccount.setAccountNr(newValue); + updateFromInputs(); + }); + + addLabelTextField(gridPane, ++gridRow, "Currency:", perfectMoneyAccount.getSingleTradeCurrency().getCodeAndName()); + addAllowedPeriod(); + addAccountNameTextFieldWithAutoFillCheckBox(); + } + + + @Override + protected void autoFillNameTextField() { + if (autoFillCheckBox != null && autoFillCheckBox.isSelected()) { + String accountNr = accountNrInputTextField.getText(); + accountNr = accountNr.substring(0, Math.min(5, accountNr.length())) + "..."; + String method = BSResources.get(paymentAccount.getPaymentMethod().getId()); + accountNameTextField.setText(method.concat(", ").concat(accountNr)); + } + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + addLabelTextField(gridPane, gridRow, "Account name:", perfectMoneyAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addLabelTextField(gridPane, ++gridRow, "Payment method:", BSResources.get(perfectMoneyAccount.getPaymentMethod().getId())); + addLabelTextField(gridPane, ++gridRow, "Account holder name:", perfectMoneyAccount.getHolderName()); + addLabelTextField(gridPane, ++gridRow, "Account nr.:", perfectMoneyAccount.getAccountNr()); + addLabelTextField(gridPane, ++gridRow, "Currency:", perfectMoneyAccount.getSingleTradeCurrency().getCodeAndName()); + addAllowedPeriod(); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && perfectMoneyValidator.validate(perfectMoneyAccount.getAccountNr()).isValid + && perfectMoneyAccount.getTradeCurrencies().size() > 0); + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/components/paymentmethods/SepaForm.java b/gui/src/main/java/io/bitsquare/gui/components/paymentmethods/SepaForm.java new file mode 100644 index 0000000000..55f4939e81 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/components/paymentmethods/SepaForm.java @@ -0,0 +1,252 @@ +/* + * 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.components.paymentmethods; + +import io.bitsquare.common.util.Tuple2; +import io.bitsquare.gui.components.InputTextField; +import io.bitsquare.gui.util.Layout; +import io.bitsquare.gui.util.validation.BICValidator; +import io.bitsquare.gui.util.validation.IBANValidator; +import io.bitsquare.gui.util.validation.InputValidator; +import io.bitsquare.locale.*; +import io.bitsquare.payment.PaymentAccount; +import io.bitsquare.payment.PaymentAccountContractData; +import io.bitsquare.payment.SepaAccount; +import io.bitsquare.payment.SepaAccountContractData; +import javafx.collections.FXCollections; +import javafx.geometry.Insets; +import javafx.geometry.VPos; +import javafx.scene.control.*; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.GridPane; +import javafx.util.StringConverter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +import static io.bitsquare.gui.util.FormBuilder.*; + +public class SepaForm extends PaymentMethodForm { + private static final Logger log = LoggerFactory.getLogger(SepaForm.class); + + private final SepaAccount sepaAccount; + private final IBANValidator ibanValidator; + private final BICValidator bicValidator; + private InputTextField ibanInputTextField; + private InputTextField bicInputTextField; + private TextField currencyTextField; + private List euroCountryCheckBoxes = new ArrayList<>(); + private List nonEuroCountryCheckBoxes = new ArrayList<>(); + + public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountContractData paymentAccountContractData) { + addLabelTextField(gridPane, ++gridRow, "Payment method:", BSResources.get(paymentAccountContractData.getPaymentMethodName())); + addLabelTextFieldWithCopyIcon(gridPane, ++gridRow, "Account holder name:", ((SepaAccountContractData) paymentAccountContractData).getHolderName()); + addLabelTextFieldWithCopyIcon(gridPane, ++gridRow, "IBAN:", ((SepaAccountContractData) paymentAccountContractData).getIban()); + addLabelTextFieldWithCopyIcon(gridPane, ++gridRow, "BIC/SWIFT:", ((SepaAccountContractData) paymentAccountContractData).getBic()); + addAllowedPeriod(gridPane, ++gridRow, paymentAccountContractData); + return gridRow; + } + + public SepaForm(PaymentAccount paymentAccount, IBANValidator ibanValidator, BICValidator bicValidator, InputValidator inputValidator, + GridPane gridPane, int gridRow) { + super(paymentAccount, inputValidator, gridPane, gridRow); + this.sepaAccount = (SepaAccount) paymentAccount; + this.ibanValidator = ibanValidator; + this.bicValidator = bicValidator; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + InputTextField holderNameInputTextField = addLabelInputTextField(gridPane, ++gridRow, "Account holder name:").second; + holderNameInputTextField.setValidator(inputValidator); + holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + sepaAccount.setHolderName(newValue); + updateFromInputs(); + }); + + ibanInputTextField = addLabelInputTextField(gridPane, ++gridRow, "IBAN:").second; + ibanInputTextField.setValidator(ibanValidator); + ibanInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + sepaAccount.setIban(newValue); + updateFromInputs(); + + }); + bicInputTextField = addLabelInputTextField(gridPane, ++gridRow, "BIC/SWIFT:").second; + bicInputTextField.setValidator(bicValidator); + bicInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + sepaAccount.setBic(newValue); + updateFromInputs(); + + }); + + Tuple2 tuple2 = addLabelComboBox(gridPane, ++gridRow, "Country of Bank:"); + ComboBox countryComboBox = tuple2.second; + countryComboBox.setPromptText("Select country of Bank"); + countryComboBox.setConverter(new StringConverter() { + @Override + public String toString(Country country) { + return country.code + " (" + country.name + ")"; + } + + @Override + public Country fromString(String s) { + return null; + } + }); + countryComboBox.setOnAction(e -> { + Country selectedItem = countryComboBox.getSelectionModel().getSelectedItem(); + sepaAccount.setCountry(selectedItem); + TradeCurrency currency = CurrencyUtil.getCurrencyByCountryCode(selectedItem.code); + sepaAccount.setSingleTradeCurrency(currency); + currencyTextField.setText(currency.getCodeAndName()); + updateCountriesSelection(true, euroCountryCheckBoxes); + updateCountriesSelection(true, nonEuroCountryCheckBoxes); + updateFromInputs(); + }); + + currencyTextField = addLabelTextField(gridPane, ++gridRow, "Currency:").second; + + addEuroCountriesGrid(true); + addNonEuroCountriesGrid(true); + addAllowedPeriod(); + addAccountNameTextFieldWithAutoFillCheckBox(); + + countryComboBox.setItems(FXCollections.observableArrayList(CountryUtil.getAllSepaCountries())); + Country country = CountryUtil.getDefaultCountry(); + countryComboBox.getSelectionModel().select(country); + sepaAccount.setCountry(country); + TradeCurrency currency = CurrencyUtil.getCurrencyByCountryCode(country.code); + sepaAccount.setSingleTradeCurrency(currency); + currencyTextField.setText(currency.getCodeAndName()); + updateFromInputs(); + } + + private void addEuroCountriesGrid(boolean isEditable) { + addCountriesGrid(isEditable, "Accept taker countries (Euro):", euroCountryCheckBoxes, CountryUtil.getAllSepaEuroCountries()); + } + + private void addNonEuroCountriesGrid(boolean isEditable) { + addCountriesGrid(isEditable, "Accepted taker countries (non-Euro):", nonEuroCountryCheckBoxes, CountryUtil.getAllSepaNonEuroCountries()); + } + + private void addCountriesGrid(boolean isEditable, String title, List checkBoxList, List dataProvider) { + Label label = addLabel(gridPane, ++gridRow, title, 0); + GridPane.setValignment(label, VPos.TOP); + FlowPane flowPane = new FlowPane(); + flowPane.setPadding(new Insets(10, 10, 10, 10)); + flowPane.setVgap(10); + flowPane.setHgap(10); + + if (isEditable) + flowPane.setId("flowpane-checkboxes-bg"); + else + flowPane.setId("flowpane-checkboxes-non-editable-bg"); + + dataProvider.stream().forEach(country -> + { + final String countryCode = country.code; + CheckBox checkBox = new CheckBox(countryCode); + checkBox.setUserData(countryCode); + checkBoxList.add(checkBox); + checkBox.setMouseTransparent(!isEditable); + checkBox.setMinWidth(45); + checkBox.setMaxWidth(checkBox.getMinWidth()); + checkBox.setTooltip(new Tooltip(country.name)); + checkBox.setOnAction(event -> { + if (checkBox.isSelected()) + sepaAccount.addAcceptedCountry(countryCode); + else + sepaAccount.removeAcceptedCountry(countryCode); + + updateAllInputsValid(); + }); + flowPane.getChildren().add(checkBox); + }); + updateCountriesSelection(isEditable, checkBoxList); + + GridPane.setRowIndex(flowPane, gridRow); + GridPane.setColumnIndex(flowPane, 1); + gridPane.getChildren().add(flowPane); + } + + private void updateCountriesSelection(boolean isEditable, List checkBoxList) { + checkBoxList.stream().forEach(checkBox -> { + String countryCode = (String) checkBox.getUserData(); + TradeCurrency selectedCurrency = sepaAccount.getSelectedTradeCurrency(); + if (selectedCurrency == null) + selectedCurrency = CurrencyUtil.getCurrencyByCountryCode(CountryUtil.getDefaultCountry().code); + + boolean selected; + + if (isEditable) { + selected = CurrencyUtil.getCurrencyByCountryCode(countryCode).getCode().equals(selectedCurrency.getCode()); + + if (selected) + sepaAccount.addAcceptedCountry(countryCode); + else + sepaAccount.removeAcceptedCountry(countryCode); + } else { + selected = sepaAccount.getAcceptedCountryCodes().contains(countryCode); + } + checkBox.setSelected(selected); + }); + } + + @Override + protected void autoFillNameTextField() { + if (autoFillCheckBox != null && autoFillCheckBox.isSelected()) { + String iban = ibanInputTextField.getText(); + if (iban.length() > 5) + iban = "..." + iban.substring(iban.length() - 5, iban.length()); + String method = BSResources.get(paymentAccount.getPaymentMethod().getId()); + String country = paymentAccount.getCountry() != null ? paymentAccount.getCountry().code : "?"; + String currency = paymentAccount.getSingleTradeCurrency() != null ? paymentAccount.getSingleTradeCurrency().getCode() : "?"; + accountNameTextField.setText(method.concat(", ").concat(currency).concat(", ").concat(country).concat(", ").concat(iban)); + } + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && bicValidator.validate(sepaAccount.getBic()).isValid + && ibanValidator.validate(sepaAccount.getIban()).isValid + && inputValidator.validate(sepaAccount.getHolderName()).isValid + && sepaAccount.getAcceptedCountryCodes().size() > 0 + && sepaAccount.getSingleTradeCurrency() != null + && sepaAccount.getCountry() != null); + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + addLabelTextField(gridPane, gridRow, "Account name:", sepaAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addLabelTextField(gridPane, ++gridRow, "Payment method:", BSResources.get(sepaAccount.getPaymentMethod().getId())); + addLabelTextField(gridPane, ++gridRow, "Account holder name:", sepaAccount.getHolderName()); + addLabelTextField(gridPane, ++gridRow, "IBAN:", sepaAccount.getIban()); + addLabelTextField(gridPane, ++gridRow, "BIC/SWIFT:", sepaAccount.getBic()); + addLabelTextField(gridPane, ++gridRow, "Location of Bank:", sepaAccount.getCountry().name); + addLabelTextField(gridPane, ++gridRow, "Currency:", sepaAccount.getSingleTradeCurrency().getCodeAndName()); + addAllowedPeriod(); + addEuroCountriesGrid(false); + addNonEuroCountriesGrid(false); + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/components/paymentmethods/SwishForm.java b/gui/src/main/java/io/bitsquare/gui/components/paymentmethods/SwishForm.java new file mode 100644 index 0000000000..a6353f89c3 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/components/paymentmethods/SwishForm.java @@ -0,0 +1,108 @@ +/* + * 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.components.paymentmethods; + +import io.bitsquare.gui.components.InputTextField; +import io.bitsquare.gui.util.Layout; +import io.bitsquare.gui.util.validation.InputValidator; +import io.bitsquare.gui.util.validation.SwishValidator; +import io.bitsquare.locale.BSResources; +import io.bitsquare.payment.PaymentAccount; +import io.bitsquare.payment.PaymentAccountContractData; +import io.bitsquare.payment.SwishAccount; +import io.bitsquare.payment.SwishAccountContractData; +import javafx.scene.layout.GridPane; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static io.bitsquare.gui.util.FormBuilder.addLabelInputTextField; +import static io.bitsquare.gui.util.FormBuilder.addLabelTextField; + +public class SwishForm extends PaymentMethodForm { + private static final Logger log = LoggerFactory.getLogger(SwishForm.class); + + private final SwishAccount swishAccount; + private final SwishValidator swishValidator; + private InputTextField mobileNrInputTextField; + + public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountContractData paymentAccountContractData) { + addLabelTextField(gridPane, ++gridRow, "Payment method:", BSResources.get(paymentAccountContractData.getPaymentMethodName())); + addLabelTextField(gridPane, ++gridRow, "Account holder name:", ((SwishAccountContractData) paymentAccountContractData).getHolderName()); + addLabelTextField(gridPane, ++gridRow, "Mobile nr.:", ((SwishAccountContractData) paymentAccountContractData).getMobileNr()); + addAllowedPeriod(gridPane, ++gridRow, paymentAccountContractData); + return gridRow; + } + + public SwishForm(PaymentAccount paymentAccount, SwishValidator swishValidator, InputValidator inputValidator, GridPane gridPane, int gridRow) { + super(paymentAccount, inputValidator, gridPane, gridRow); + this.swishAccount = (SwishAccount) paymentAccount; + this.swishValidator = swishValidator; + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + InputTextField holderNameInputTextField = addLabelInputTextField(gridPane, ++gridRow, "Account holder name:").second; + holderNameInputTextField.setValidator(inputValidator); + holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + swishAccount.setHolderName(newValue); + updateFromInputs(); + }); + + mobileNrInputTextField = addLabelInputTextField(gridPane, ++gridRow, "Mobile nr.:").second; + mobileNrInputTextField.setValidator(swishValidator); + mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + swishAccount.setMobileNr(newValue); + updateFromInputs(); + }); + + addLabelTextField(gridPane, ++gridRow, "Currency:", swishAccount.getSingleTradeCurrency().getCodeAndName()); + addAllowedPeriod(); + addAccountNameTextFieldWithAutoFillCheckBox(); + } + + @Override + protected void autoFillNameTextField() { + if (autoFillCheckBox != null && autoFillCheckBox.isSelected()) { + String mobileNr = mobileNrInputTextField.getText(); + mobileNr = mobileNr.substring(0, Math.min(5, mobileNr.length())) + "..."; + String method = BSResources.get(paymentAccount.getPaymentMethod().getId()); + accountNameTextField.setText(method.concat(", ").concat(mobileNr)); + } + } + + @Override + public void addFormForDisplayAccount() { + gridRowFrom = gridRow; + addLabelTextField(gridPane, gridRow, "Account name:", swishAccount.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + addLabelTextField(gridPane, ++gridRow, "Payment method:", BSResources.get(swishAccount.getPaymentMethod().getId())); + addLabelTextField(gridPane, ++gridRow, "Account holder name:", swishAccount.getHolderName()); + addLabelTextField(gridPane, ++gridRow, "Mobile nr.:", swishAccount.getMobileNr()); + addLabelTextField(gridPane, ++gridRow, "Currency:", swishAccount.getSingleTradeCurrency().getCodeAndName()); + addAllowedPeriod(); + } + + @Override + public void updateAllInputsValid() { + allInputsValid.set(isAccountNameValid() + && swishValidator.validate(swishAccount.getMobileNr()).isValid + && inputValidator.validate(swishAccount.getHolderName()).isValid + && swishAccount.getTradeCurrencies().size() > 0); + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/main/account/arbitratorregistration/ArbitratorRegistrationView.fxml b/gui/src/main/java/io/bitsquare/gui/main/account/arbitratorregistration/ArbitratorRegistrationView.fxml new file mode 100644 index 0000000000..e38c9db6fd --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/account/arbitratorregistration/ArbitratorRegistrationView.fxml @@ -0,0 +1,27 @@ + + + + + + + + + + \ No newline at end of file diff --git a/gui/src/main/java/io/bitsquare/gui/main/account/arbitratorregistration/ArbitratorRegistrationView.java b/gui/src/main/java/io/bitsquare/gui/main/account/arbitratorregistration/ArbitratorRegistrationView.java new file mode 100644 index 0000000000..c94df4349e --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/account/arbitratorregistration/ArbitratorRegistrationView.java @@ -0,0 +1,232 @@ +/* + * 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.account.arbitratorregistration; + + +import io.bitsquare.app.BitsquareApp; +import io.bitsquare.arbitration.Arbitrator; +import io.bitsquare.common.UserThread; +import io.bitsquare.common.util.Tuple2; +import io.bitsquare.gui.common.view.ActivatableViewAndModel; +import io.bitsquare.gui.common.view.FxmlView; +import io.bitsquare.gui.popups.Popup; +import io.bitsquare.gui.util.FormBuilder; +import io.bitsquare.gui.util.ImageUtil; +import io.bitsquare.gui.util.Layout; +import io.bitsquare.locale.LanguageUtil; +import javafx.beans.value.ChangeListener; +import javafx.collections.ListChangeListener; +import javafx.geometry.HPos; +import javafx.geometry.Insets; +import javafx.geometry.VPos; +import javafx.scene.control.*; +import javafx.scene.image.ImageView; +import javafx.scene.layout.*; +import javafx.util.Callback; +import javafx.util.StringConverter; + +import javax.inject.Inject; + +import static io.bitsquare.gui.util.FormBuilder.*; + +@FxmlView +public class ArbitratorRegistrationView extends ActivatableViewAndModel { + + private TextField pubKeyTextField; + private ListView languagesListView; + private ComboBox languageComboBox; + + private int gridRow = 0; + private Button registerButton; + private Button revokeButton; + + private ChangeListener arbitratorChangeListener; + private EnterPrivKeyPopup enterPrivKeyPopup; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private ArbitratorRegistrationView(ArbitratorRegistrationViewModel model) { + super(model); + } + + @Override + public void initialize() { + buildUI(); + + languageComboBox.setItems(model.allLanguageCodes); + + arbitratorChangeListener = (observable, oldValue, arbitrator) -> updateLanguageList(); + } + + @Override + protected void activate() { + } + + @Override + protected void deactivate() { + } + + public void onTabSelection(boolean isSelectedTab) { + if (isSelectedTab) { + model.myArbitratorProperty.addListener(arbitratorChangeListener); + updateLanguageList(); + + if (model.registrationPubKeyAsHex.get() == null && enterPrivKeyPopup == null) { + enterPrivKeyPopup = new EnterPrivKeyPopup(); + enterPrivKeyPopup.onClose(() -> enterPrivKeyPopup = null) + .onKey(privKey -> model.setPrivKeyAndCheckPubKey(privKey)) + .width(700) + .show(); + } + } else { + model.myArbitratorProperty.removeListener(arbitratorChangeListener); + } + } + + private void updateLanguageList() { + languagesListView.setItems(model.languageCodes); + languagesListView.setPrefHeight(languagesListView.getItems().size() * Layout.LIST_ROW_HEIGHT + 2); + languagesListView.getItems().addListener((ListChangeListener) + c -> languagesListView.setPrefHeight(languagesListView.getItems().size() * Layout.LIST_ROW_HEIGHT + 2)); + } + + private void buildUI() { + GridPane gridPane = new GridPane(); + gridPane.setPadding(new Insets(30, 25, -1, 25)); + gridPane.setHgap(5); + gridPane.setVgap(5); + ColumnConstraints columnConstraints1 = new ColumnConstraints(); + columnConstraints1.setHalignment(HPos.RIGHT); + columnConstraints1.setHgrow(Priority.SOMETIMES); + columnConstraints1.setMinWidth(200); + ColumnConstraints columnConstraints2 = new ColumnConstraints(); + columnConstraints2.setHgrow(Priority.ALWAYS); + gridPane.getColumnConstraints().addAll(columnConstraints1, columnConstraints2); + root.getChildren().add(gridPane); + + addTitledGroupBg(gridPane, gridRow, 3, "Arbitrator registration"); + pubKeyTextField = FormBuilder.addLabelTextField(gridPane, gridRow, "Public key:", + model.registrationPubKeyAsHex.get(), Layout.FIRST_ROW_DISTANCE).second; + + if (BitsquareApp.DEV_MODE) + pubKeyTextField.setText("6ac43ea1df2a290c1c8391736aa42e4339c5cb4f110ff0257a13b63211977b7a"); + + pubKeyTextField.textProperty().bind(model.registrationPubKeyAsHex); + + Tuple2 tuple = addLabelListView(gridPane, ++gridRow, "Your languages:"); + GridPane.setValignment(tuple.first, VPos.TOP); + languagesListView = tuple.second; + languagesListView.disableProperty().bind(model.registrationEditDisabled); + languagesListView.setMinHeight(3 * Layout.LIST_ROW_HEIGHT + 2); + languagesListView.setMaxHeight(6 * Layout.LIST_ROW_HEIGHT + 2); + languagesListView.setCellFactory(new Callback, ListCell>() { + @Override + public ListCell call(ListView list) { + return new ListCell() { + final Label label = new Label(); + final ImageView icon = ImageUtil.getImageViewById(ImageUtil.REMOVE_ICON); + final Button removeButton = new Button("", icon); + final AnchorPane pane = new AnchorPane(label, removeButton); + + { + label.setLayoutY(5); + removeButton.setId("icon-button"); + AnchorPane.setRightAnchor(removeButton, 0d); + } + + @Override + public void updateItem(final String item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + label.setText(LanguageUtil.getDisplayName(item)); + removeButton.setOnAction(e -> onRemoveLanguage(item)); + setGraphic(pane); + } else { + setGraphic(null); + } + } + }; + } + }); + + languageComboBox = addLabelComboBox(gridPane, ++gridRow).second; + languageComboBox.disableProperty().bind(model.registrationEditDisabled); + languageComboBox.setPromptText("Add language"); + languageComboBox.setConverter(new StringConverter() { + @Override + public String toString(String code) { + return LanguageUtil.getDisplayName(code); + } + + @Override + public String fromString(String s) { + return null; + } + }); + languageComboBox.setOnAction(e -> onAddLanguage()); + + registerButton = addButtonAfterGroup(gridPane, ++gridRow, "Register arbitrator"); + registerButton.disableProperty().bind(model.registrationEditDisabled); + registerButton.setOnAction(e -> onRegister()); + + revokeButton = addButton(gridPane, ++gridRow, "Revoke registration"); + revokeButton.setDefaultButton(false); + revokeButton.disableProperty().bind(model.revokeButtonDisabled); + revokeButton.setOnAction(e -> onRevoke()); + + addTitledGroupBg(gridPane, ++gridRow, 2, "Information", Layout.GROUP_DISTANCE); + Label infoLabel = addMultilineLabel(gridPane, gridRow); + GridPane.setMargin(infoLabel, new Insets(Layout.FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, 0)); + infoLabel.setText("Please not that you need to stay available for 15 days after revoking as there might be trades which are using you as " + + "arbitrator. The max. allowed trader period is 8 days and the dispute process might take up to 7 days."); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // UI actions + /////////////////////////////////////////////////////////////////////////////////////////// + + private void onAddLanguage() { + model.onAddLanguage(languageComboBox.getSelectionModel().getSelectedItem()); + UserThread.execute(() -> languageComboBox.getSelectionModel().clearSelection()); + } + + private void onRemoveLanguage(String locale) { + model.onRemoveLanguage(locale); + + if (languagesListView.getItems().size() == 0) { + new Popup().warning("You need to set at least 1 language.\nWe added the default language for you.").show(); + model.onAddLanguage(LanguageUtil.getDefaultLanguageLocaleAsCode()); + } + } + + private void onRevoke() { + model.onRevoke( + () -> new Popup().information("You have successfully removed your arbitrator from the P2P network.").show(), + (errorMessage) -> new Popup().error("Could not remove arbitrator.\nError message: " + errorMessage).show()); + } + + private void onRegister() { + model.onRegister( + () -> new Popup().information("You have successfully registered your arbitrator to the P2P network.").show(), + (errorMessage) -> new Popup().error("Could not register arbitrator.\nError message: " + errorMessage).show()); + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/main/account/arbitratorregistration/ArbitratorRegistrationViewModel.java b/gui/src/main/java/io/bitsquare/gui/main/account/arbitratorregistration/ArbitratorRegistrationViewModel.java new file mode 100644 index 0000000000..8fd16d07b9 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/account/arbitratorregistration/ArbitratorRegistrationViewModel.java @@ -0,0 +1,186 @@ +/* + * 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.account.arbitratorregistration; + +import com.google.inject.Inject; +import io.bitsquare.arbitration.Arbitrator; +import io.bitsquare.arbitration.ArbitratorManager; +import io.bitsquare.btc.AddressEntry; +import io.bitsquare.btc.WalletService; +import io.bitsquare.common.crypto.KeyRing; +import io.bitsquare.common.handlers.ErrorMessageHandler; +import io.bitsquare.common.handlers.ResultHandler; +import io.bitsquare.gui.common.model.ActivatableViewModel; +import io.bitsquare.locale.LanguageUtil; +import io.bitsquare.p2p.Address; +import io.bitsquare.p2p.P2PService; +import io.bitsquare.user.User; +import javafx.beans.property.*; +import javafx.collections.FXCollections; +import javafx.collections.MapChangeListener; +import javafx.collections.ObservableList; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.Utils; + +import java.util.ArrayList; +import java.util.Date; + +class ArbitratorRegistrationViewModel extends ActivatableViewModel { + private final ArbitratorManager arbitratorManager; + private P2PService p2PService; + private final WalletService walletService; + private final KeyRing keyRing; + + final BooleanProperty registrationEditDisabled = new SimpleBooleanProperty(true); + final BooleanProperty revokeButtonDisabled = new SimpleBooleanProperty(true); + final ObjectProperty myArbitratorProperty = new SimpleObjectProperty<>(); + + final ObservableList languageCodes = FXCollections.observableArrayList(LanguageUtil.getDefaultLanguageLocaleAsCode()); + final ObservableList allLanguageCodes = FXCollections.observableArrayList(LanguageUtil.getAllLanguageCodes()); + private boolean allDataValid; + private final MapChangeListener arbitratorMapChangeListener; + private ECKey registrationKey; + StringProperty registrationPubKeyAsHex = new SimpleStringProperty(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public ArbitratorRegistrationViewModel(ArbitratorManager arbitratorManager, + User user, + P2PService p2PService, + WalletService walletService, + KeyRing keyRing) { + this.arbitratorManager = arbitratorManager; + this.p2PService = p2PService; + this.walletService = walletService; + this.keyRing = keyRing; + + arbitratorMapChangeListener = new MapChangeListener() { + @Override + public void onChanged(Change change) { + Arbitrator myRegisteredArbitrator = user.getRegisteredArbitrator(); + myArbitratorProperty.set(myRegisteredArbitrator); + + // We don't reset the languages in case of revokation, as its likely that the arbitrator will use the same again when he re-activate + // registration later + if (myRegisteredArbitrator != null) + languageCodes.setAll(myRegisteredArbitrator.getLanguageCodes()); + + updateDisableStates(); + } + }; + } + + @Override + protected void activate() { + arbitratorManager.getArbitratorsObservableMap().addListener(arbitratorMapChangeListener); + updateDisableStates(); + } + + @Override + protected void deactivate() { + arbitratorManager.getArbitratorsObservableMap().removeListener(arbitratorMapChangeListener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // UI actions + /////////////////////////////////////////////////////////////////////////////////////////// + + void onAddLanguage(String code) { + if (code != null && !languageCodes.contains(code)) + languageCodes.add(code); + + updateDisableStates(); + } + + void onRemoveLanguage(String code) { + if (code != null && languageCodes.contains(code)) + languageCodes.remove(code); + + updateDisableStates(); + } + + boolean setPrivKeyAndCheckPubKey(String privKeyString) { + ECKey _registrationKey = arbitratorManager.getRegistrationKey(privKeyString); + if (_registrationKey != null) { + String _registrationPubKeyAsHex = Utils.HEX.encode(_registrationKey.getPubKey()); + boolean isKeyValid = arbitratorManager.isPublicKeyInList(_registrationPubKeyAsHex); + if (isKeyValid) { + registrationKey = _registrationKey; + registrationPubKeyAsHex.set(_registrationPubKeyAsHex); + } + updateDisableStates(); + return isKeyValid; + } else { + updateDisableStates(); + return false; + } + } + + void onRegister(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + updateDisableStates(); + if (allDataValid) { + AddressEntry arbitratorDepositAddressEntry = walletService.getArbitratorAddressEntry(); + String registrationSignature = arbitratorManager.signStorageSignaturePubKey(registrationKey); + Arbitrator arbitrator = new Arbitrator( + p2PService.getAddress(), + arbitratorDepositAddressEntry.getPubKey(), + arbitratorDepositAddressEntry.getAddressString(), + keyRing.getPubKeyRing(), + new ArrayList<>(languageCodes), + new Date(), + registrationKey.getPubKey(), + registrationSignature + ); + if (arbitrator != null) { + arbitratorManager.addArbitrator(arbitrator, + () -> { + updateDisableStates(); + resultHandler.handleResult(); + }, + (errorMessage) -> { + updateDisableStates(); + errorMessageHandler.handleErrorMessage(errorMessage); + }); + } + } + } + + + void onRevoke(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + arbitratorManager.removeArbitrator( + () -> { + updateDisableStates(); + resultHandler.handleResult(); + }, + (errorMessage) -> { + updateDisableStates(); + errorMessageHandler.handleErrorMessage(errorMessage); + }); + } + + private void updateDisableStates() { + allDataValid = languageCodes != null && languageCodes.size() > 0 && registrationKey != null && registrationPubKeyAsHex.get() != null; + registrationEditDisabled.set(!allDataValid || myArbitratorProperty.get() != null); + revokeButtonDisabled.set(!allDataValid || myArbitratorProperty.get() == null); + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/main/account/arbitratorregistration/EnterPrivKeyPopup.java b/gui/src/main/java/io/bitsquare/gui/main/account/arbitratorregistration/EnterPrivKeyPopup.java new file mode 100644 index 0000000000..35d6c24319 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/account/arbitratorregistration/EnterPrivKeyPopup.java @@ -0,0 +1,130 @@ +/* + * 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.account.arbitratorregistration; + +import io.bitsquare.app.BitsquareApp; +import io.bitsquare.gui.components.InputTextField; +import io.bitsquare.gui.popups.Popup; +import javafx.geometry.Insets; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; + +import java.util.Optional; + +public class EnterPrivKeyPopup extends Popup { + private Button unlockButton; + private InputTextField keyInputTextField; + private PrivKeyHandler privKeyHandler; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Interface + /////////////////////////////////////////////////////////////////////////////////////////// + + public interface PrivKeyHandler { + boolean checkKey(String privKey); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public API + /////////////////////////////////////////////////////////////////////////////////////////// + + public EnterPrivKeyPopup() { + } + + public EnterPrivKeyPopup show() { + if (gridPane != null) { + rowIndex = -1; + gridPane.getChildren().clear(); + } + + if (headLine == null) + headLine = "Registration open for invited arbitrators only"; + + createGridPane(); + addHeadLine(); + addInputFields(); + addButtons(); + createPopup(); + + return this; + } + + public EnterPrivKeyPopup onClose(Runnable closeHandler) { + this.closeHandlerOptional = Optional.of(closeHandler); + return this; + } + + public EnterPrivKeyPopup onKey(PrivKeyHandler privKeyHandler) { + this.privKeyHandler = privKeyHandler; + return this; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + private void addInputFields() { + Label label = new Label("Enter private key:"); + label.setWrapText(true); + GridPane.setMargin(label, new Insets(3, 0, 0, 0)); + GridPane.setRowIndex(label, ++rowIndex); + + keyInputTextField = new InputTextField(); + //TODO change when testing is done + if (BitsquareApp.DEV_MODE) + keyInputTextField.setText("6ac43ea1df2a290c1c8391736aa42e4339c5cb4f110ff0257a13b63211977b7a"); + GridPane.setMargin(keyInputTextField, new Insets(3, 0, 0, 0)); + GridPane.setRowIndex(keyInputTextField, rowIndex); + GridPane.setColumnIndex(keyInputTextField, 1); + keyInputTextField.textProperty().addListener((observable, oldValue, newValue) -> { + unlockButton.setDisable(newValue.length() == 0); + }); + gridPane.getChildren().addAll(label, keyInputTextField); + } + + private void addButtons() { + unlockButton = new Button("Unlock"); + unlockButton.setDefaultButton(true); + unlockButton.setDisable(keyInputTextField.getText().length() == 0); + unlockButton.setOnAction(e -> { + if (privKeyHandler.checkKey(keyInputTextField.getText())) + hide(); + else + new Popup().warning("The key you entered was not correct.").width(300).onClose(() -> blurAgain()).show(); + }); + + Button cancelButton = new Button("Close"); + cancelButton.setOnAction(event -> { + hide(); + closeHandlerOptional.ifPresent(closeHandler -> closeHandler.run()); + }); + + HBox hBox = new HBox(); + hBox.setSpacing(10); + GridPane.setRowIndex(hBox, ++rowIndex); + GridPane.setColumnIndex(hBox, 1); + hBox.getChildren().addAll(unlockButton, cancelButton); + gridPane.getChildren().add(hBox); + } + +} diff --git a/gui/src/main/java/io/bitsquare/gui/main/account/content/arbitratorselection/ArbitratorListItem.java b/gui/src/main/java/io/bitsquare/gui/main/account/content/arbitratorselection/ArbitratorListItem.java new file mode 100644 index 0000000000..13e48f55e0 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/account/content/arbitratorselection/ArbitratorListItem.java @@ -0,0 +1,58 @@ +/* + * 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.account.content.arbitratorselection; + +import io.bitsquare.arbitration.Arbitrator; +import io.bitsquare.gui.util.BSFormatter; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; + +public class ArbitratorListItem { + public final Arbitrator arbitrator; + private final BSFormatter formatter; + private final BooleanProperty isSelected = new SimpleBooleanProperty(); + + public ArbitratorListItem(Arbitrator arbitrator, BSFormatter formatter) { + this.arbitrator = arbitrator; + this.formatter = formatter; + } + + public String getAddressString() { + return arbitrator != null ? arbitrator.getArbitratorAddress().getFullAddress() : ""; + } + + public String getLanguageCodes() { + return arbitrator != null && arbitrator.getLanguageCodes() != null ? formatter.languageCodesToString(arbitrator.getLanguageCodes()) : ""; + } + + public String getRegistrationDate() { + return arbitrator != null ? formatter.formatDate(arbitrator.getRegistrationDate()) : ""; + } + + public boolean getIsSelected() { + return isSelected.get(); + } + + public BooleanProperty isSelectedProperty() { + return isSelected; + } + + public void setIsSelected(boolean isSelected) { + this.isSelected.set(isSelected); + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/main/account/content/arbitratorselection/ArbitratorSelectionView.fxml b/gui/src/main/java/io/bitsquare/gui/main/account/content/arbitratorselection/ArbitratorSelectionView.fxml new file mode 100644 index 0000000000..15888594ca --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/account/content/arbitratorselection/ArbitratorSelectionView.fxml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + diff --git a/gui/src/main/java/io/bitsquare/gui/main/account/content/arbitratorselection/ArbitratorSelectionView.java b/gui/src/main/java/io/bitsquare/gui/main/account/content/arbitratorselection/ArbitratorSelectionView.java new file mode 100644 index 0000000000..7ec9b3146c --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/account/content/arbitratorselection/ArbitratorSelectionView.java @@ -0,0 +1,304 @@ +/* + * 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.account.content.arbitratorselection; + +import io.bitsquare.common.UserThread; +import io.bitsquare.common.util.Tuple2; +import io.bitsquare.gui.common.view.ActivatableViewAndModel; +import io.bitsquare.gui.common.view.FxmlView; +import io.bitsquare.gui.components.TableGroupHeadline; +import io.bitsquare.gui.popups.Popup; +import io.bitsquare.gui.util.ImageUtil; +import io.bitsquare.gui.util.Layout; +import io.bitsquare.locale.LanguageUtil; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.collections.ListChangeListener; +import javafx.geometry.HPos; +import javafx.geometry.Insets; +import javafx.geometry.VPos; +import javafx.scene.control.*; +import javafx.scene.image.ImageView; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.GridPane; +import javafx.util.Callback; +import javafx.util.StringConverter; + +import javax.inject.Inject; + +import static io.bitsquare.gui.util.FormBuilder.*; + +@FxmlView +public class ArbitratorSelectionView extends ActivatableViewAndModel { + + private final ArbitratorSelectionViewModel model; + + private ListView languagesListView; + private ComboBox languageComboBox; + private TableView table; + private int gridRow = 0; + private CheckBox autoSelectAllMatchingCheckBox; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private ArbitratorSelectionView(ArbitratorSelectionViewModel model) { + super(model); + this.model = model; + } + + @Override + public void initialize() { + addLanguageGroup(); + addArbitratorsGroup(); + } + + @Override + protected void activate() { + languagesListView.getItems().addListener((ListChangeListener) c -> { + languagesListView.setPrefHeight(languagesListView.getItems().size() * Layout.LIST_ROW_HEIGHT + 2); + }); + languageComboBox.setItems(model.allLanguageCodes); + languagesListView.setItems(model.languageCodes); + languagesListView.setPrefHeight(languagesListView.getItems().size() * Layout.LIST_ROW_HEIGHT + 2); + + table.setItems(model.arbitratorListItems); + autoSelectAllMatchingCheckBox.setSelected(model.getAutoSelectArbitrators()); + } + + @Override + protected void deactivate() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // UI actions + /////////////////////////////////////////////////////////////////////////////////////////// + + private void onAddLanguage() { + model.onAddLanguage(languageComboBox.getSelectionModel().getSelectedItem()); + UserThread.execute(() -> languageComboBox.getSelectionModel().clearSelection()); + } + + private void onRemoveLanguage(String locale) { + model.onRemoveLanguage(locale); + + if (languagesListView.getItems().size() == 0) { + new Popup().warning("You need to set at least 1 language.\n" + + "We added the default language for you.").show(); + model.onAddLanguage(LanguageUtil.getDefaultLanguageLocaleAsCode()); + } + } + + private void onAddArbitrator(ArbitratorListItem arbitratorListItem) { + model.onAddArbitrator(arbitratorListItem.arbitrator); + } + + private void onRemoveArbitrator(ArbitratorListItem arbitratorListItem) { + model.onRemoveArbitrator(arbitratorListItem.arbitrator); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // UI builder + /////////////////////////////////////////////////////////////////////////////////////////// + + private void addLanguageGroup() { + addTitledGroupBg(root, gridRow, 2, "Which languages do you speak?"); + + Tuple2 tuple = addLabelListView(root, gridRow, "Your languages:", Layout.FIRST_ROW_DISTANCE); + GridPane.setValignment(tuple.first, VPos.TOP); + languagesListView = tuple.second; + languagesListView.setMinHeight(3 * Layout.LIST_ROW_HEIGHT + 2); + languagesListView.setMaxHeight(6 * Layout.LIST_ROW_HEIGHT + 2); + languagesListView.setCellFactory(new Callback, ListCell>() { + @Override + public ListCell call(ListView list) { + return new ListCell() { + final Label label = new Label(); + final ImageView icon = ImageUtil.getImageViewById(ImageUtil.REMOVE_ICON); + final Button removeButton = new Button("", icon); + final AnchorPane pane = new AnchorPane(label, removeButton); + + { + label.setLayoutY(5); + removeButton.setId("icon-button"); + AnchorPane.setRightAnchor(removeButton, 0d); + } + + @Override + public void updateItem(final String item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + label.setText(LanguageUtil.getDisplayName(item)); + removeButton.setOnAction(e -> onRemoveLanguage(item)); + setGraphic(pane); + } else { + setGraphic(null); + } + } + }; + } + }); + + languageComboBox = addLabelComboBox(root, ++gridRow).second; + languageComboBox.setPromptText("Add language"); + languageComboBox.setConverter(new StringConverter() { + @Override + public String toString(String code) { + return LanguageUtil.getDisplayName(code); + } + + @Override + public String fromString(String s) { + return null; + } + }); + languageComboBox.setOnAction(e -> onAddLanguage()); + } + + private void addArbitratorsGroup() { + TableGroupHeadline tableGroupHeadline = new TableGroupHeadline("Which arbitrators do you accept"); + GridPane.setRowIndex(tableGroupHeadline, ++gridRow); + GridPane.setColumnSpan(tableGroupHeadline, 2); + GridPane.setMargin(tableGroupHeadline, new Insets(40, -10, -10, -10)); + root.getChildren().add(tableGroupHeadline); + + table = new TableView<>(); + GridPane.setRowIndex(table, gridRow); + GridPane.setColumnSpan(table, 2); + GridPane.setMargin(table, new Insets(60, -10, 5, -10)); + root.getChildren().add(table); + + autoSelectAllMatchingCheckBox = addCheckBox(root, ++gridRow, "Auto select all with matching language"); + GridPane.setColumnIndex(autoSelectAllMatchingCheckBox, 0); + GridPane.setMargin(autoSelectAllMatchingCheckBox, new Insets(0, -10, 0, -10)); + autoSelectAllMatchingCheckBox.setOnAction(event -> model.setAutoSelectArbitrators(autoSelectAllMatchingCheckBox.isSelected())); + + Button reloadButton = addButton(root, gridRow, "Reload"); + GridPane.setColumnIndex(reloadButton, 1); + GridPane.setHalignment(reloadButton, HPos.RIGHT); + GridPane.setMargin(reloadButton, new Insets(0, -10, 0, -10)); + reloadButton.setOnAction(event -> model.reload()); + + TableColumn dateColumn = new TableColumn("Registration date"); + dateColumn.setCellValueFactory(param -> new ReadOnlyObjectWrapper(param.getValue().getRegistrationDate())); + dateColumn.setMinWidth(130); + dateColumn.setMaxWidth(130); + + TableColumn nameColumn = new TableColumn("Public key"); + nameColumn.setCellValueFactory(param -> new ReadOnlyObjectWrapper(param.getValue().getAddressString())); + nameColumn.setMinWidth(90); + + TableColumn languagesColumn = new TableColumn("Languages"); + languagesColumn.setCellValueFactory(param -> new ReadOnlyObjectWrapper(param.getValue().getLanguageCodes())); + languagesColumn.setMinWidth(130); + + TableColumn selectionColumn = new TableColumn("Accept") { + { + setMinWidth(60); + setMaxWidth(60); + setSortable(false); + } + }; + selectionColumn.setCellValueFactory((arbitrator) -> new ReadOnlyObjectWrapper<>(arbitrator.getValue())); + selectionColumn.setCellFactory( + new Callback, TableCell>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell() { + private final CheckBox checkBox = new CheckBox(); + private TableRow tableRow; + + private void updateDisableState(final ArbitratorListItem item) { + boolean selected = model.isAcceptedArbitrator(item.arbitrator); + item.setIsSelected(selected); + + boolean hasMatchingLanguage = model.hasMatchingLanguage(item.arbitrator); + if (!hasMatchingLanguage) { + model.onRemoveArbitrator(item.arbitrator); + if (selected) + item.setIsSelected(false); + } + + boolean isMyOwnRegisteredArbitrator = model.isMyOwnRegisteredArbitrator(item.arbitrator); + checkBox.setDisable(!hasMatchingLanguage || isMyOwnRegisteredArbitrator); + + tableRow = getTableRow(); + if (tableRow != null) { + tableRow.setOpacity(hasMatchingLanguage && !isMyOwnRegisteredArbitrator ? 1 : 0.4); + + if (isMyOwnRegisteredArbitrator) { + tableRow.setTooltip(new Tooltip("An arbitrator cannot select himself for trading.")); + tableRow.setOnMouseClicked(e -> new Popup().warning( + "An arbitrator cannot select himself for trading.").show()); + } else if (!hasMatchingLanguage) { + tableRow.setTooltip(new Tooltip("No matching language.")); + tableRow.setOnMouseClicked(e -> new Popup().warning( + "You can only select arbitrators who are speaking at least 1 common language.").show()); + } else { + tableRow.setOnMouseClicked(null); + tableRow.setTooltip(null); + } + } + } + + @Override + public void updateItem(final ArbitratorListItem item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + model.languageCodes.addListener((ListChangeListener) c -> updateDisableState(item)); + item.isSelectedProperty().addListener((observable, oldValue, newValue) -> checkBox.setSelected(newValue)); + + checkBox.setSelected(model.isAcceptedArbitrator(item.arbitrator)); + checkBox.setOnAction(e -> { + if (checkBox.isSelected()) { + onAddArbitrator(item); + } else if (model.isDeselectAllowed(item)) { + onRemoveArbitrator(item); + } else { + new Popup().warning("You need to have at least one arbitrator selected.").show(); + checkBox.setSelected(true); + } + item.setIsSelected(checkBox.isSelected()); + } + ); + + updateDisableState(item); + setGraphic(checkBox); + } else { + setGraphic(null); + + if (checkBox != null) + checkBox.setOnAction(null); + if (tableRow != null) + tableRow.setOnMouseClicked(null); + } + } + }; + } + }); + + table.getColumns().addAll(dateColumn, nameColumn, languagesColumn, selectionColumn); + table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + } +} + diff --git a/gui/src/main/java/io/bitsquare/gui/main/account/content/arbitratorselection/ArbitratorSelectionViewModel.java b/gui/src/main/java/io/bitsquare/gui/main/account/content/arbitratorselection/ArbitratorSelectionViewModel.java new file mode 100644 index 0000000000..1a8ee09f96 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/account/content/arbitratorselection/ArbitratorSelectionViewModel.java @@ -0,0 +1,164 @@ +/* + * 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.account.content.arbitratorselection; + +import com.google.inject.Inject; +import io.bitsquare.arbitration.Arbitrator; +import io.bitsquare.arbitration.ArbitratorManager; +import io.bitsquare.common.crypto.KeyRing; +import io.bitsquare.gui.common.model.ActivatableDataModel; +import io.bitsquare.gui.util.BSFormatter; +import io.bitsquare.locale.LanguageUtil; +import io.bitsquare.p2p.Address; +import io.bitsquare.user.Preferences; +import io.bitsquare.user.User; +import javafx.collections.FXCollections; +import javafx.collections.MapChangeListener; +import javafx.collections.ObservableList; + +import java.util.stream.Collectors; + +class ArbitratorSelectionViewModel extends ActivatableDataModel { + private User user; + private final ArbitratorManager arbitratorManager; + private final Preferences preferences; + private final KeyRing keyRing; + final ObservableList languageCodes = FXCollections.observableArrayList(); + final ObservableList arbitratorListItems = FXCollections.observableArrayList(); + final ObservableList allLanguageCodes = FXCollections.observableArrayList(LanguageUtil.getAllLanguageCodes()); + private final MapChangeListener arbitratorMapChangeListener; + + @Inject + public ArbitratorSelectionViewModel(User user, ArbitratorManager arbitratorManager, Preferences preferences, + KeyRing keyRing, BSFormatter formatter) { + this.user = user; + this.arbitratorManager = arbitratorManager; + this.preferences = preferences; + this.keyRing = keyRing; + + arbitratorMapChangeListener = change -> { + log.debug("getValueAdded " + change.getValueAdded()); + log.debug("getValueRemoved " + change.getValueRemoved()); + log.debug("values() " + arbitratorManager.getArbitratorsObservableMap().values()); + arbitratorListItems.clear(); + arbitratorListItems.addAll(arbitratorManager.getArbitratorsObservableMap().values().stream() + .map(e -> new ArbitratorListItem(e, formatter)).collect(Collectors.toList())); + }; + } + + @Override + protected void activate() { + languageCodes.setAll(user.getAcceptedLanguageLocaleCodes()); + arbitratorManager.getArbitratorsObservableMap().addListener(arbitratorMapChangeListener); + arbitratorManager.applyArbitrators(); + + updateAutoSelectArbitrators(); + } + + @Override + protected void deactivate() { + arbitratorManager.getArbitratorsObservableMap().removeListener(arbitratorMapChangeListener); + } + + void onAddLanguage(String code) { + if (code != null) { + boolean changed = user.addAcceptedLanguageLocale(code); + if (changed) + languageCodes.add(code); + } + + updateAutoSelectArbitrators(); + } + + void onRemoveLanguage(String code) { + if (code != null) { + boolean changed = user.removeAcceptedLanguageLocale(code); + if (changed) + languageCodes.remove(code); + } + + updateAutoSelectArbitrators(); + } + + void onAddArbitrator(Arbitrator arbitrator) { + if (!arbitratorIsTrader(arbitrator)) + user.addAcceptedArbitrator(arbitrator); + } + + void onRemoveArbitrator(Arbitrator arbitrator) { + if (arbitrator != null) + user.removeAcceptedArbitrator(arbitrator); + } + + public boolean isDeselectAllowed(ArbitratorListItem arbitratorListItem) { + return arbitratorListItem != null + && user.getAcceptedArbitrators() != null + && user.getAcceptedArbitrators().size() > 1; + } + + public boolean isAcceptedArbitrator(Arbitrator arbitrator) { + if (arbitrator != null && user.getAcceptedArbitrators() != null) + return user.getAcceptedArbitrators().contains(arbitrator) && !isMyOwnRegisteredArbitrator(arbitrator); + else + return false; + } + + public boolean arbitratorIsTrader(Arbitrator arbitrator) { + return keyRing.getPubKeyRing().equals(arbitrator.getPubKeyRing()); + } + + public boolean hasMatchingLanguage(Arbitrator arbitrator) { + return user.hasMatchingLanguage(arbitrator); + } + + public boolean isMyOwnRegisteredArbitrator(Arbitrator arbitrator) { + return user.isMyOwnRegisteredArbitrator(arbitrator); + } + + public void reload() { + arbitratorManager.applyArbitrators(); + } + + private void updateAutoSelectArbitrators() { + if (preferences.getAutoSelectArbitrators()) { + arbitratorListItems.stream().forEach(item -> { + Arbitrator arbitrator = item.arbitrator; + if (!isMyOwnRegisteredArbitrator(arbitrator)) { + if (hasMatchingLanguage(arbitrator)) { + onAddArbitrator(arbitrator); + item.setIsSelected(true); + } else { + onRemoveArbitrator(arbitrator); + item.setIsSelected(false); + } + } else { + item.setIsSelected(false); + } + }); + } + } + + public void setAutoSelectArbitrators(boolean doAutoSelect) { + preferences.setAutoSelectArbitrators(doAutoSelect); + updateAutoSelectArbitrators(); + } + + public boolean getAutoSelectArbitrators() { + return preferences.getAutoSelectArbitrators(); + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/main/account/content/backup/BackupView.fxml b/gui/src/main/java/io/bitsquare/gui/main/account/content/backup/BackupView.fxml new file mode 100644 index 0000000000..1c88c39a29 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/account/content/backup/BackupView.fxml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + 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 new file mode 100644 index 0000000000..f5db10258d --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/account/content/backup/BackupView.java @@ -0,0 +1,124 @@ +/* + * 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.account.content.backup; + +import io.bitsquare.app.BitsquareEnvironment; +import io.bitsquare.gui.common.view.ActivatableView; +import io.bitsquare.gui.common.view.FxmlView; +import io.bitsquare.gui.popups.Popup; +import io.bitsquare.gui.util.BSFormatter; +import io.bitsquare.gui.util.Layout; +import io.bitsquare.user.Preferences; +import javafx.scene.control.Button; +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; +import javafx.stage.DirectoryChooser; +import javafx.stage.Stage; +import org.apache.commons.io.FileUtils; + +import javax.inject.Inject; +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.text.SimpleDateFormat; +import java.util.Date; + +import static io.bitsquare.gui.util.FormBuilder.*; + +@FxmlView +public class BackupView extends ActivatableView { + + + private int gridRow = 0; + private final Stage stage; + private final Preferences preferences; + private final BitsquareEnvironment environment; + private final BSFormatter formatter; + private Button selectBackupDir, backupNow; + private TextField backUpLocationTextField; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private BackupView(Stage stage, Preferences preferences, BitsquareEnvironment environment, BSFormatter formatter) { + super(); + this.stage = stage; + this.preferences = preferences; + this.environment = environment; + this.formatter = formatter; + } + + @Override + public void initialize() { + addTitledGroupBg(root, gridRow, 3, "Backup wallet and data"); + backUpLocationTextField = addLabelTextField(root, gridRow, "Backup location:", "", Layout.FIRST_ROW_DISTANCE).second; + if (preferences.getBackupDirectory() != null) + backUpLocationTextField.setText(preferences.getBackupDirectory()); + + selectBackupDir = addButton(root, ++gridRow, "Select backup location"); + selectBackupDir.setDefaultButton(preferences.getBackupDirectory() == null); + backupNow = addButton(root, ++gridRow, "Backup now (backup is not encrypted!)"); + backupNow.setDisable(preferences.getBackupDirectory() == null || preferences.getBackupDirectory().length() == 0); + backupNow.setDefaultButton(preferences.getBackupDirectory() != null); + } + + @Override + protected void activate() { + selectBackupDir.setOnAction(e -> { + DirectoryChooser directoryChooser = new DirectoryChooser(); + directoryChooser.setTitle("Select backup location"); + File dir = directoryChooser.showDialog(stage); + if (dir != null) { + String backupDirectory = dir.getAbsolutePath(); + backUpLocationTextField.setText(backupDirectory); + preferences.setBackupDirectory(backupDirectory); + backupNow.setDisable(false); + backupNow.setDefaultButton(true); + selectBackupDir.setDefaultButton(false); + } + }); + + backupNow.setOnAction(e -> { + String backupDirectory = preferences.getBackupDirectory(); + if (backupDirectory.length() > 0) { + try { + String dateString = new SimpleDateFormat("YYYY-MM-dd-HHmmss").format(new Date()); + String destination = Paths.get(backupDirectory, "bitsquare_backup_" + dateString).toString(); + FileUtils.copyDirectory(new File(environment.getProperty(BitsquareEnvironment.APP_DATA_DIR_KEY)), + new File(destination)); + new Popup().information("Backup successfully saved at:\n" + destination).show(); + } catch (IOException e1) { + e1.printStackTrace(); + log.error(e1.getMessage()); + new Popup().error("Backup could not be saved.\nError message: " + e1.getMessage()).show(); + } + } + }); + } + + @Override + protected void deactivate() { + selectBackupDir.setOnAction(null); + } + + +} + diff --git a/gui/src/main/java/io/bitsquare/gui/main/account/content/paymentsaccount/PaymentAccountDataModel.java b/gui/src/main/java/io/bitsquare/gui/main/account/content/paymentsaccount/PaymentAccountDataModel.java new file mode 100644 index 0000000000..50db596a0e --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/account/content/paymentsaccount/PaymentAccountDataModel.java @@ -0,0 +1,69 @@ +/* + * 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.account.content.paymentsaccount; + +import com.google.inject.Inject; +import io.bitsquare.gui.common.model.ActivatableDataModel; +import io.bitsquare.payment.PaymentAccount; +import io.bitsquare.user.User; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.SetChangeListener; + +class PaymentAccountDataModel extends ActivatableDataModel { + + private final User user; + final ObservableList paymentAccounts = FXCollections.observableArrayList(); + private final SetChangeListener setChangeListener; + + @Inject + public PaymentAccountDataModel(User user) { + this.user = user; + setChangeListener = change -> paymentAccounts.setAll(user.getPaymentAccounts()); + } + + @Override + protected void activate() { + user.getPaymentAccountsAsObservable().addListener(setChangeListener); + paymentAccounts.setAll(user.getPaymentAccounts()); + } + + @Override + protected void deactivate() { + user.getPaymentAccountsAsObservable().removeListener(setChangeListener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // UI actions + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onSaveNewAccount(PaymentAccount paymentAccount) { + user.addPaymentAccount(paymentAccount); + } + + public void onDeleteAccount(PaymentAccount paymentAccount) { + user.removePaymentAccount(paymentAccount); + } + + public void onSelectAccount(PaymentAccount paymentAccount) { + user.setCurrentPaymentAccount(paymentAccount); + } + + +} diff --git a/gui/src/main/java/io/bitsquare/gui/main/account/content/paymentsaccount/PaymentAccountView.fxml b/gui/src/main/java/io/bitsquare/gui/main/account/content/paymentsaccount/PaymentAccountView.fxml new file mode 100644 index 0000000000..63f11d6f05 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/account/content/paymentsaccount/PaymentAccountView.fxml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/gui/src/main/java/io/bitsquare/gui/main/account/content/paymentsaccount/PaymentAccountView.java b/gui/src/main/java/io/bitsquare/gui/main/account/content/paymentsaccount/PaymentAccountView.java new file mode 100644 index 0000000000..07bc2b14c2 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/account/content/paymentsaccount/PaymentAccountView.java @@ -0,0 +1,319 @@ +/* + * 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.account.content.paymentsaccount; + +import io.bitsquare.common.util.Tuple2; +import io.bitsquare.gui.common.view.ActivatableViewAndModel; +import io.bitsquare.gui.common.view.FxmlView; +import io.bitsquare.gui.common.view.Wizard; +import io.bitsquare.gui.components.TitledGroupBg; +import io.bitsquare.gui.components.paymentmethods.*; +import io.bitsquare.gui.popups.Popup; +import io.bitsquare.gui.util.FormBuilder; +import io.bitsquare.gui.util.Layout; +import io.bitsquare.gui.util.validation.*; +import io.bitsquare.locale.BSResources; +import io.bitsquare.payment.*; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.layout.GridPane; +import javafx.util.StringConverter; + +import javax.inject.Inject; + +import static io.bitsquare.gui.util.FormBuilder.*; + +@FxmlView +public class PaymentAccountView extends ActivatableViewAndModel implements Wizard.Step { + + private ComboBox paymentAccountsComboBox; + private ComboBox paymentMethodsComboBox; + + private Wizard wizard; + + private final IBANValidator ibanValidator; + private final BICValidator bicValidator; + private final InputValidator inputValidator; + private final OKPayValidator okPayValidator; + private final AliPayValidator aliPayValidator; + private final PerfectMoneyValidator perfectMoneyValidator; + private final SwishValidator swishValidator; + private final AltCoinAddressValidator altCoinAddressValidator; + + private PaymentMethodForm paymentMethodForm; + private TitledGroupBg accountTitledGroupBg; + private Button addAccountButton; + private Button saveNewAccountButton; + private int gridRow = 0; + + @Inject + public PaymentAccountView(PaymentAccountViewModel model, + IBANValidator ibanValidator, + BICValidator bicValidator, + InputValidator inputValidator, + OKPayValidator okPayValidator, + AliPayValidator aliPayValidator, + PerfectMoneyValidator perfectMoneyValidator, + SwishValidator swishValidator, + AltCoinAddressValidator altCoinAddressValidator) { + super(model); + + this.ibanValidator = ibanValidator; + this.bicValidator = bicValidator; + this.inputValidator = inputValidator; + this.okPayValidator = okPayValidator; + this.aliPayValidator = aliPayValidator; + this.perfectMoneyValidator = perfectMoneyValidator; + this.swishValidator = swishValidator; + this.altCoinAddressValidator = altCoinAddressValidator; + } + + @Override + public void initialize() { + buildForm(); + } + + @Override + protected void activate() { + paymentAccountsComboBox.setItems(model.getPaymentAccounts()); + EventHandler paymentAccountsComboBoxHandler = e -> { + if (paymentAccountsComboBox.getSelectionModel().getSelectedItem() != null) + onSelectAccount(paymentAccountsComboBox.getSelectionModel().getSelectedItem()); + }; + paymentAccountsComboBox.setOnAction(paymentAccountsComboBoxHandler); + + model.getPaymentAccounts().addListener( + (ListChangeListener) c -> paymentAccountsComboBox.setDisable(model.getPaymentAccounts().size() == 0)); + paymentAccountsComboBox.setDisable(model.getPaymentAccounts().size() == 0); + } + + @Override + protected void deactivate() { + paymentAccountsComboBox.setOnAction(null); + } + + @Override + public void setWizard(Wizard wizard) { + this.wizard = wizard; + } + + @Override + public void hideWizardNavigation() { + + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // UI actions + /////////////////////////////////////////////////////////////////////////////////////////// + + private void onSaveNewAccount(PaymentAccount paymentAccount) { + if (!model.getPaymentAccounts().stream().filter(e -> { + if (e.getAccountName() != null) + return e.getAccountName().equals(paymentAccount.getAccountName()); + else + return false; + }).findAny().isPresent()) { + model.onSaveNewAccount(paymentAccount); + removeNewAccountForm(); + paymentAccountsComboBox.getSelectionModel().clearSelection(); + } else { + new Popup().error("That account name is already used in a saved account. \nPlease use another name.").show(); + } + } + + private void onCancelNewAccount() { + removeNewAccountForm(); + paymentAccountsComboBox.getSelectionModel().clearSelection(); + } + + private void onDeleteAccount(PaymentAccount paymentAccount) { + new Popup().warning("Do you really want to delete the selected payment account?") + .onAction(() -> { + model.onDeleteAccount(paymentAccount); + removeSelectAccountForm(); + paymentAccountsComboBox.getSelectionModel().clearSelection(); + }) + .show(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Base form + /////////////////////////////////////////////////////////////////////////////////////////// + + private void buildForm() { + addTitledGroupBg(root, gridRow, 2, "Manage payment accounts"); + + paymentAccountsComboBox = addLabelComboBox(root, gridRow, "Select account:", Layout.FIRST_ROW_DISTANCE).second; + paymentAccountsComboBox.setPromptText("Select account"); + paymentAccountsComboBox.setConverter(new StringConverter() { + @Override + public String toString(PaymentAccount paymentAccount) { + return paymentAccount.getAccountName(); + } + + @Override + public PaymentAccount fromString(String s) { + return null; + } + }); + + addAccountButton = addButton(root, ++gridRow, "Add new account"); + addAccountButton.setOnAction(event -> addNewAccount()); + } + + // Add new account form + private void addNewAccount() { + paymentAccountsComboBox.getSelectionModel().clearSelection(); + removeAccountRows(); + addAccountButton.setDisable(true); + accountTitledGroupBg = addTitledGroupBg(root, ++gridRow, 1, "Create new account", Layout.GROUP_DISTANCE); + paymentMethodsComboBox = addLabelComboBox(root, gridRow, "Payment method:", Layout.FIRST_ROW_AND_GROUP_DISTANCE).second; + paymentMethodsComboBox.setPromptText("Select payment method"); + paymentMethodsComboBox.setPrefWidth(250); + paymentMethodsComboBox.setItems(FXCollections.observableArrayList(PaymentMethod.ALL_VALUES)); + paymentMethodsComboBox.setConverter(new StringConverter() { + @Override + public String toString(PaymentMethod paymentMethod) { + return BSResources.get(paymentMethod.getId()); + } + + @Override + public PaymentMethod fromString(String s) { + return null; + } + }); + paymentMethodsComboBox.setOnAction(e -> { + if (paymentMethodForm != null) { + FormBuilder.removeRowsFromGridPane(root, 3, paymentMethodForm.getGridRow() + 1); + GridPane.setRowSpan(accountTitledGroupBg, paymentMethodForm.getRowSpan() + 1); + } + gridRow = 2; + paymentMethodForm = getPaymentMethodForm(paymentMethodsComboBox.getSelectionModel().getSelectedItem()); + if (paymentMethodForm != null) { + paymentMethodForm.addFormForAddAccount(); + gridRow = paymentMethodForm.getGridRow(); + Tuple2 tuple2 = add2ButtonsAfterGroup(root, ++gridRow, "Save new account", "Cancel"); + saveNewAccountButton = tuple2.first; + saveNewAccountButton.setOnAction(event -> onSaveNewAccount(paymentMethodForm.getPaymentAccount())); + saveNewAccountButton.disableProperty().bind(paymentMethodForm.allInputsValidProperty().not()); + Button cancelButton = tuple2.second; + cancelButton.setOnAction(event -> onCancelNewAccount()); + GridPane.setRowSpan(accountTitledGroupBg, paymentMethodForm.getRowSpan() + 1); + } + }); + } + + // Select account form + private void onSelectAccount(PaymentAccount paymentAccount) { + removeAccountRows(); + addAccountButton.setDisable(false); + accountTitledGroupBg = addTitledGroupBg(root, ++gridRow, 1, "Selected account", Layout.GROUP_DISTANCE); + paymentMethodForm = getPaymentMethodForm(paymentAccount); + if (paymentMethodForm != null) { + paymentMethodForm.addFormForDisplayAccount(); + gridRow = paymentMethodForm.getGridRow(); + Button deleteAccountButton = addButtonAfterGroup(root, ++gridRow, "Delete account"); + deleteAccountButton.setOnAction(event -> onDeleteAccount(paymentMethodForm.getPaymentAccount())); + GridPane.setRowSpan(accountTitledGroupBg, paymentMethodForm.getRowSpan()); + model.onSelectAccount(paymentAccount); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////////////////////// + + private PaymentMethodForm getPaymentMethodForm(PaymentAccount paymentAccount) { + return getPaymentMethodForm(paymentAccount.getPaymentMethod(), paymentAccount); + } + + private PaymentMethodForm getPaymentMethodForm(PaymentMethod paymentMethod) { + PaymentAccount paymentAccount; + switch (paymentMethod.getId()) { + case PaymentMethod.OK_PAY_ID: + paymentAccount = new OKPayAccount(); + break; + case PaymentMethod.PERFECT_MONEY_ID: + paymentAccount = new PerfectMoneyAccount(); + break; + case PaymentMethod.SEPA_ID: + paymentAccount = new SepaAccount(); + break; + case PaymentMethod.ALI_PAY_ID: + paymentAccount = new AliPayAccount(); + break; + case PaymentMethod.SWISH_ID: + paymentAccount = new SwishAccount(); + break; + case PaymentMethod.BLOCK_CHAINS_ID: + paymentAccount = new BlockChainAccount(); + break; + default: + log.error("Not supported PaymentMethod: " + paymentMethod); + paymentAccount = null; + break; + } + return getPaymentMethodForm(paymentMethod, paymentAccount); + } + + private PaymentMethodForm getPaymentMethodForm(PaymentMethod paymentMethod, PaymentAccount paymentAccount) { + switch (paymentMethod.getId()) { + case PaymentMethod.OK_PAY_ID: + return new OKPayForm(paymentAccount, okPayValidator, inputValidator, root, gridRow); + case PaymentMethod.PERFECT_MONEY_ID: + return new PerfectMoneyForm(paymentAccount, perfectMoneyValidator, inputValidator, root, gridRow); + case PaymentMethod.SEPA_ID: + return new SepaForm(paymentAccount, ibanValidator, bicValidator, inputValidator, root, gridRow); + case PaymentMethod.ALI_PAY_ID: + return new AliPayForm(paymentAccount, aliPayValidator, inputValidator, root, gridRow); + case PaymentMethod.SWISH_ID: + return new SwishForm(paymentAccount, swishValidator, inputValidator, root, gridRow); + case PaymentMethod.BLOCK_CHAINS_ID: + return new BlockChainForm(paymentAccount, altCoinAddressValidator, inputValidator, root, gridRow); + default: + log.error("Not supported PaymentMethod: " + paymentMethod); + return null; + } + } + + private void removeNewAccountForm() { + saveNewAccountButton.disableProperty().unbind(); + removeAccountRows(); + addAccountButton.setDisable(false); + } + + private void removeSelectAccountForm() { + FormBuilder.removeRowsFromGridPane(root, 2, gridRow); + gridRow = 1; + addAccountButton.setDisable(false); + } + + + private void removeAccountRows() { + FormBuilder.removeRowsFromGridPane(root, 2, gridRow); + gridRow = 1; + } + +} + diff --git a/gui/src/main/java/io/bitsquare/gui/main/account/content/paymentsaccount/PaymentAccountViewModel.java b/gui/src/main/java/io/bitsquare/gui/main/account/content/paymentsaccount/PaymentAccountViewModel.java new file mode 100644 index 0000000000..bc06d58153 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/account/content/paymentsaccount/PaymentAccountViewModel.java @@ -0,0 +1,66 @@ +/* + * 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.account.content.paymentsaccount; + +import com.google.inject.Inject; +import io.bitsquare.gui.common.model.ActivatableWithDataModel; +import io.bitsquare.gui.common.model.ViewModel; +import io.bitsquare.payment.PaymentAccount; +import javafx.collections.ObservableList; + +class PaymentAccountViewModel extends ActivatableWithDataModel implements ViewModel { + + + @Inject + public PaymentAccountViewModel(PaymentAccountDataModel dataModel) { + super(dataModel); + } + + @Override + protected void activate() { + } + + @Override + protected void deactivate() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // UI actions + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onSaveNewAccount(PaymentAccount paymentAccount) { + dataModel.onSaveNewAccount(paymentAccount); + } + + public void onDeleteAccount(PaymentAccount paymentAccount) { + dataModel.onDeleteAccount(paymentAccount); + } + + public void onSelectAccount(PaymentAccount paymentAccount) { + dataModel.onSelectAccount(paymentAccount); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + ObservableList getPaymentAccounts() { + return dataModel.paymentAccounts; + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/main/disputes/DisputesView.fxml b/gui/src/main/java/io/bitsquare/gui/main/disputes/DisputesView.fxml new file mode 100644 index 0000000000..b812d7c318 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/disputes/DisputesView.fxml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + diff --git a/gui/src/main/java/io/bitsquare/gui/main/disputes/DisputesView.java b/gui/src/main/java/io/bitsquare/gui/main/disputes/DisputesView.java new file mode 100644 index 0000000000..3393326377 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/disputes/DisputesView.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.disputes; + +import io.bitsquare.arbitration.Arbitrator; +import io.bitsquare.arbitration.ArbitratorManager; +import io.bitsquare.common.crypto.KeyRing; +import io.bitsquare.gui.Navigation; +import io.bitsquare.gui.common.model.Activatable; +import io.bitsquare.gui.common.view.*; +import io.bitsquare.gui.main.MainView; +import io.bitsquare.gui.main.disputes.arbitrator.ArbitratorDisputeView; +import io.bitsquare.gui.main.disputes.trader.TraderDisputeView; +import io.bitsquare.p2p.Address; +import javafx.beans.value.ChangeListener; +import javafx.collections.MapChangeListener; +import javafx.fxml.FXML; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; + +import javax.inject.Inject; + +// will be probably only used for arbitration communication, will be renamed and the icon changed +@FxmlView +public class DisputesView extends ActivatableViewAndModel { + + @FXML + Tab tradersDisputesTab, arbitratorsDisputesTab; + + private final Navigation navigation; + private final ArbitratorManager arbitratorManager; + private final KeyRing keyRing; + + private Navigation.Listener navigationListener; + private ChangeListener tabChangeListener; + private Tab currentTab; + private final ViewLoader viewLoader; + private MapChangeListener arbitratorMapChangeListener; + + @Inject + public DisputesView(CachingViewLoader viewLoader, Navigation navigation, ArbitratorManager arbitratorManager, KeyRing keyRing) { + this.viewLoader = viewLoader; + this.navigation = navigation; + this.arbitratorManager = arbitratorManager; + this.keyRing = keyRing; + } + + @Override + public void initialize() { + log.debug("initialize "); + navigationListener = viewPath -> { + if (viewPath.size() == 3 && viewPath.indexOf(DisputesView.class) == 1) + loadView(viewPath.tip()); + }; + + tabChangeListener = (ov, oldValue, newValue) -> { + if (newValue == tradersDisputesTab) + navigation.navigateTo(MainView.class, DisputesView.class, TraderDisputeView.class); + else if (newValue == arbitratorsDisputesTab) + navigation.navigateTo(MainView.class, DisputesView.class, ArbitratorDisputeView.class); + }; + + arbitratorMapChangeListener = change -> updateArbitratorsDisputesTabDisableState(); + } + + private void updateArbitratorsDisputesTabDisableState() { + boolean isArbitrator = arbitratorManager.getArbitratorsObservableMap().values().stream() + .filter(e -> e.getPubKeyRing() != null && e.getPubKeyRing().equals(keyRing.getPubKeyRing())) + .findAny().isPresent(); + log.debug("arbitratorManager.getArbitratorsObservableMap() " + arbitratorManager.getArbitratorsObservableMap().size()); + log.debug("updateArbitratorsDisputesTabDisableState isArbitrator=" + isArbitrator); + arbitratorsDisputesTab.setDisable(!isArbitrator); + if (arbitratorsDisputesTab.getContent() != null) + arbitratorsDisputesTab.getContent().setDisable(!isArbitrator); + } + + @Override + protected void activate() { + arbitratorManager.applyArbitrators(); + arbitratorManager.getArbitratorsObservableMap().addListener(arbitratorMapChangeListener); + updateArbitratorsDisputesTabDisableState(); + + root.getSelectionModel().selectedItemProperty().addListener(tabChangeListener); + navigation.addListener(navigationListener); + + if (root.getSelectionModel().getSelectedItem() == tradersDisputesTab) + navigation.navigateTo(MainView.class, DisputesView.class, TraderDisputeView.class); + else if (root.getSelectionModel().getSelectedItem() == arbitratorsDisputesTab) + navigation.navigateTo(MainView.class, DisputesView.class, ArbitratorDisputeView.class); + } + + @Override + protected void deactivate() { + arbitratorManager.getArbitratorsObservableMap().removeListener(arbitratorMapChangeListener); + root.getSelectionModel().selectedItemProperty().removeListener(tabChangeListener); + navigation.removeListener(navigationListener); + currentTab = null; + } + + private void loadView(Class viewClass) { + // we want to get activate/deactivate called, so we remove the old view on tab change + if (currentTab != null) + currentTab.setContent(null); + + View view = viewLoader.load(viewClass); + + if (view instanceof ArbitratorDisputeView) + currentTab = arbitratorsDisputesTab; + else if (view instanceof TraderDisputeView) + currentTab = tradersDisputesTab; + + currentTab.setContent(view.getRoot()); + root.getSelectionModel().select(currentTab); + } +} + diff --git a/gui/src/main/java/io/bitsquare/gui/main/disputes/arbitrator/ArbitratorDisputeView.fxml b/gui/src/main/java/io/bitsquare/gui/main/disputes/arbitrator/ArbitratorDisputeView.fxml new file mode 100644 index 0000000000..61c55f05b0 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/disputes/arbitrator/ArbitratorDisputeView.fxml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/gui/src/main/java/io/bitsquare/gui/main/disputes/arbitrator/ArbitratorDisputeView.java b/gui/src/main/java/io/bitsquare/gui/main/disputes/arbitrator/ArbitratorDisputeView.java new file mode 100644 index 0000000000..d00dd430b3 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/disputes/arbitrator/ArbitratorDisputeView.java @@ -0,0 +1,56 @@ +/* + * 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.disputes.arbitrator; + +import io.bitsquare.arbitration.Dispute; +import io.bitsquare.arbitration.DisputeManager; +import io.bitsquare.btc.TradeWalletService; +import io.bitsquare.btc.WalletService; +import io.bitsquare.common.crypto.KeyRing; +import io.bitsquare.gui.Navigation; +import io.bitsquare.gui.common.view.FxmlView; +import io.bitsquare.gui.main.disputes.trader.DisputeSummaryPopup; +import io.bitsquare.gui.main.disputes.trader.TraderDisputeView; +import io.bitsquare.gui.popups.ContractPopup; +import io.bitsquare.gui.popups.TradeDetailsPopup; +import io.bitsquare.gui.util.BSFormatter; +import io.bitsquare.trade.TradeManager; +import javafx.collections.transformation.FilteredList; +import javafx.stage.Stage; + +import javax.inject.Inject; + +// will be probably only used for arbitration communication, will be renamed and the icon changed +@FxmlView +public class ArbitratorDisputeView extends TraderDisputeView { + + @Inject + public ArbitratorDisputeView(DisputeManager disputeManager, KeyRing keyRing, TradeWalletService tradeWalletService, WalletService walletService, + TradeManager tradeManager, Stage stage, BSFormatter formatter, Navigation navigation, + DisputeSummaryPopup disputeSummaryPopup, ContractPopup contractPopup, TradeDetailsPopup tradeDetailsPopup) { + super(disputeManager, keyRing, tradeWalletService, walletService, tradeManager, stage, formatter, navigation, + disputeSummaryPopup, contractPopup, tradeDetailsPopup); + } + + @Override + protected void setFilteredListPredicate(FilteredList filteredList) { + filteredList.setPredicate(dispute -> dispute.getArbitratorPubKeyRing().equals(keyRing.getPubKeyRing())); + } +} + + diff --git a/gui/src/main/java/io/bitsquare/gui/main/disputes/trader/DisputeSummaryPopup.java b/gui/src/main/java/io/bitsquare/gui/main/disputes/trader/DisputeSummaryPopup.java new file mode 100644 index 0000000000..3a0ec84c63 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/disputes/trader/DisputeSummaryPopup.java @@ -0,0 +1,467 @@ +/* + * 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.disputes.trader; + +import io.bitsquare.arbitration.Dispute; +import io.bitsquare.arbitration.DisputeManager; +import io.bitsquare.arbitration.DisputeResult; +import io.bitsquare.btc.AddressEntry; +import io.bitsquare.btc.FeePolicy; +import io.bitsquare.btc.TradeWalletService; +import io.bitsquare.btc.WalletService; +import io.bitsquare.btc.exceptions.TransactionVerificationException; +import io.bitsquare.common.util.Tuple2; +import io.bitsquare.gui.popups.Popup; +import io.bitsquare.gui.util.BSFormatter; +import io.bitsquare.gui.util.Layout; +import io.bitsquare.gui.util.Transitions; +import io.bitsquare.trade.Contract; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.ObjectBinding; +import javafx.geometry.Insets; +import javafx.geometry.VPos; +import javafx.scene.control.*; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import org.bitcoinj.core.AddressFormatException; +import org.bitcoinj.core.Coin; +import org.reactfx.util.FxTimer; + +import javax.inject.Inject; +import java.time.Duration; +import java.util.Date; +import java.util.Optional; + +import static io.bitsquare.gui.util.FormBuilder.*; + +public class DisputeSummaryPopup extends Popup { + private final BSFormatter formatter; + private final DisputeManager disputeManager; + private final WalletService walletService; + private final TradeWalletService tradeWalletService; + private Dispute dispute; + private Optional finalizeDisputeHandlerOptional = Optional.empty(); + private ToggleGroup tradeAmountToggleGroup; + private DisputeResult disputeResult; + private RadioButton buyerIsWinnerRadioButton, sellerIsWinnerRadioButton, shareRadioButton, loserPaysFeeRadioButton, splitFeeRadioButton, + waiveFeeRadioButton; + private Optional peersDisputeOptional; + private Coin arbitratorPayoutAmount, winnerPayoutAmount, loserPayoutAmount, stalematePayoutAmount; + private ToggleGroup feeToggleGroup; + private String role; + private TextArea summaryNotesTextArea; + // keep a reference to not get GCed + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public DisputeSummaryPopup(BSFormatter formatter, DisputeManager disputeManager, WalletService walletService, TradeWalletService tradeWalletService) { + this.formatter = formatter; + this.disputeManager = disputeManager; + this.walletService = walletService; + this.tradeWalletService = tradeWalletService; + } + + public void show(Dispute dispute) { + this.dispute = dispute; + + rowIndex = -1; + width = 850; + createGridPane(); + addContent(); + createPopup(); + } + + public DisputeSummaryPopup onClose(Runnable closeHandler) { + this.closeHandlerOptional = Optional.of(closeHandler); + return this; + } + + public DisputeSummaryPopup onFinalizeDispute(Runnable finalizeDisputeHandler) { + this.finalizeDisputeHandlerOptional = Optional.of(finalizeDisputeHandler); + return this; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void createGridPane() { + super.createGridPane(); + gridPane.setPadding(new Insets(35, 40, 30, 40)); + gridPane.setStyle("-fx-background-color: -bs-content-bg-grey;" + + "-fx-background-radius: 5 5 5 5;" + + "-fx-effect: dropshadow(gaussian, #999, 10, 0, 0, 0);" + + "-fx-background-insets: 10;" + ); + } + + private void addContent() { + Contract contract = dispute.getContract(); + if (dispute.disputeResultProperty().get() == null) + disputeResult = new DisputeResult(dispute.getTradeId(), dispute.getTraderId()); + else + disputeResult = dispute.disputeResultProperty().get(); + + peersDisputeOptional = disputeManager.getDisputesAsObservableList().stream() + .filter(d -> dispute.getTradeId().equals(d.getTradeId()) && dispute.getTraderId() != d.getTraderId()).findFirst(); + + addInfoPane(); + + if (!dispute.isSupportTicket()) + addCheckboxes(); + + addTradeAmountPayoutControls(); + addFeeControls(); + + boolean applyPeersDisputeResult = peersDisputeOptional.isPresent() && peersDisputeOptional.get().isClosed(); + if (applyPeersDisputeResult) { + // If the other peers dispute has been closed we apply the result to ourselves + DisputeResult peersDisputeResult = peersDisputeOptional.get().disputeResultProperty().get(); + disputeResult.setBuyerPayoutAmount(peersDisputeResult.getBuyerPayoutAmount()); + disputeResult.setSellerPayoutAmount(peersDisputeResult.getSellerPayoutAmount()); + disputeResult.setArbitratorPayoutAmount(peersDisputeResult.getArbitratorPayoutAmount()); + disputeResult.setFeePaymentPolicy(peersDisputeResult.getFeePaymentPolicy()); + disputeResult.setWinner(peersDisputeResult.getWinner()); + + if (disputeResult.getBuyerPayoutAmount() != null) { + log.debug("buyerPayoutAmount " + disputeResult.getBuyerPayoutAmount().toFriendlyString()); + log.debug("sellerPayoutAmount " + disputeResult.getSellerPayoutAmount().toFriendlyString()); + log.debug("arbitratorPayoutAmount " + disputeResult.getArbitratorPayoutAmount().toFriendlyString()); + } + + //setFeeRadioButtonState(); + + buyerIsWinnerRadioButton.setDisable(true); + sellerIsWinnerRadioButton.setDisable(true); + shareRadioButton.setDisable(true); + loserPaysFeeRadioButton.setDisable(true); + splitFeeRadioButton.setDisable(true); + waiveFeeRadioButton.setDisable(true); + + calculatePayoutAmounts(disputeResult.getFeePaymentPolicy()); + applyTradeAmountRadioButtonStates(); + } else { + applyPayoutAmounts(disputeResult.feePaymentPolicyProperty().get(), tradeAmountToggleGroup.selectedToggleProperty().get()); + ObjectBinding> changed = Bindings.createObjectBinding( + () -> new Tuple2(disputeResult.feePaymentPolicyProperty().get(), tradeAmountToggleGroup.selectedToggleProperty().get()), + disputeResult.feePaymentPolicyProperty(), + tradeAmountToggleGroup.selectedToggleProperty()); + changed.addListener((observable, oldValue, newValue) -> { + applyPayoutAmounts(newValue.first, newValue.second); + }); + } + + setFeeRadioButtonState(); + + addSummaryNotes(); + addButtons(contract); + } + + private void addInfoPane() { + Contract contract = dispute.getContract(); + addTitledGroupBg(gridPane, ++rowIndex, 12, "Summary"); + addLabelTextField(gridPane, rowIndex, "Trade ID:", dispute.getShortTradeId(), Layout.FIRST_ROW_DISTANCE); + addLabelTextField(gridPane, ++rowIndex, "Ticket opening date:", formatter.formatDateTime(dispute.getOpeningDate())); + if (dispute.isDisputeOpenerIsOfferer()) { + if (dispute.isDisputeOpenerIsBuyer()) + role = "Buyer/offerer"; + else + role = "Seller/offerer"; + } else { + if (dispute.isDisputeOpenerIsBuyer()) + role = "Buyer/taker"; + else + role = "Seller/taker"; + } + addLabelTextField(gridPane, ++rowIndex, "Traders role:", role); + addLabelTextField(gridPane, ++rowIndex, "Trade amount:", formatter.formatCoinWithCode(contract.getTradeAmount())); + addLabelTextField(gridPane, ++rowIndex, "Trade volume:", formatter.formatFiatWithCode(contract.offer.getVolumeByAmount(contract.getTradeAmount()))); + addLabelTextField(gridPane, ++rowIndex, "Price:", formatter.formatFiatWithCode(contract.offer.getPrice())); + } + + private void addCheckboxes() { + Label evidenceLabel = addLabel(gridPane, ++rowIndex, "Evidence:", 10); + GridPane.setValignment(evidenceLabel, VPos.TOP); + CheckBox tamperProofCheckBox = new CheckBox("Tamper proof evidence"); + CheckBox idVerificationCheckBox = new CheckBox("ID Verification"); + CheckBox screenCastCheckBox = new CheckBox("Video/Screencast"); + + tamperProofCheckBox.selectedProperty().bindBidirectional(disputeResult.tamperProofEvidenceProperty()); + idVerificationCheckBox.selectedProperty().bindBidirectional(disputeResult.idVerificationProperty()); + screenCastCheckBox.selectedProperty().bindBidirectional(disputeResult.screenCastProperty()); + + FlowPane checkBoxPane = new FlowPane(); + checkBoxPane.setHgap(20); + checkBoxPane.setVgap(5); + checkBoxPane.getChildren().addAll(tamperProofCheckBox, idVerificationCheckBox, screenCastCheckBox); + GridPane.setRowIndex(checkBoxPane, rowIndex); + GridPane.setColumnIndex(checkBoxPane, 1); + GridPane.setMargin(checkBoxPane, new Insets(10, 0, 0, 0)); + gridPane.getChildren().add(checkBoxPane); + } + + private void addTradeAmountPayoutControls() { + Label distributionLabel = addLabel(gridPane, ++rowIndex, "Trade amount payout:", 10); + GridPane.setValignment(distributionLabel, VPos.TOP); + + buyerIsWinnerRadioButton = new RadioButton("Buyer gets trade amount payout"); + sellerIsWinnerRadioButton = new RadioButton("Seller gets trade amount payout"); + shareRadioButton = new RadioButton("Both gets half trade amount payout"); + VBox radioButtonPane = new VBox(); + radioButtonPane.setSpacing(20); + radioButtonPane.getChildren().addAll(buyerIsWinnerRadioButton, sellerIsWinnerRadioButton, shareRadioButton); + GridPane.setRowIndex(radioButtonPane, rowIndex); + GridPane.setColumnIndex(radioButtonPane, 1); + GridPane.setMargin(radioButtonPane, new Insets(10, 0, 10, 0)); + gridPane.getChildren().add(radioButtonPane); + + tradeAmountToggleGroup = new ToggleGroup(); + buyerIsWinnerRadioButton.setToggleGroup(tradeAmountToggleGroup); + sellerIsWinnerRadioButton.setToggleGroup(tradeAmountToggleGroup); + shareRadioButton.setToggleGroup(tradeAmountToggleGroup); + + shareRadioButton.selectedProperty().addListener((observable, oldValue, newValue) -> { + if (newValue) { + loserPaysFeeRadioButton.setSelected(false); + + if (splitFeeRadioButton != null && !dispute.isSupportTicket()) + splitFeeRadioButton.setSelected(true); + + if (waiveFeeRadioButton != null && dispute.isSupportTicket()) + waiveFeeRadioButton.setSelected(true); + } + + loserPaysFeeRadioButton.setDisable(newValue); + }); + } + + private void addFeeControls() { + Label splitFeeLabel = addLabel(gridPane, ++rowIndex, "Arbitration fee:", 10); + GridPane.setValignment(splitFeeLabel, VPos.TOP); + + loserPaysFeeRadioButton = new RadioButton("Loser pays arbitration fee"); + splitFeeRadioButton = new RadioButton("Split arbitration fee"); + waiveFeeRadioButton = new RadioButton("Waive arbitration fee"); + HBox feeRadioButtonPane = new HBox(); + feeRadioButtonPane.setSpacing(20); + feeRadioButtonPane.getChildren().addAll(loserPaysFeeRadioButton, splitFeeRadioButton, waiveFeeRadioButton); + GridPane.setRowIndex(feeRadioButtonPane, rowIndex); + GridPane.setColumnIndex(feeRadioButtonPane, 1); + GridPane.setMargin(feeRadioButtonPane, new Insets(10, 0, 10, 0)); + gridPane.getChildren().add(feeRadioButtonPane); + + feeToggleGroup = new ToggleGroup(); + loserPaysFeeRadioButton.setToggleGroup(feeToggleGroup); + splitFeeRadioButton.setToggleGroup(feeToggleGroup); + waiveFeeRadioButton.setToggleGroup(feeToggleGroup); + + //setFeeRadioButtonState(); + + feeToggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> { + if (newValue == loserPaysFeeRadioButton) + disputeResult.setFeePaymentPolicy(DisputeResult.FeePaymentPolicy.LOSER); + else if (newValue == splitFeeRadioButton) + disputeResult.setFeePaymentPolicy(DisputeResult.FeePaymentPolicy.SPLIT); + else if (newValue == waiveFeeRadioButton) + disputeResult.setFeePaymentPolicy(DisputeResult.FeePaymentPolicy.WAIVE); + }); + + if (dispute.isSupportTicket()) + feeToggleGroup.selectToggle(waiveFeeRadioButton); + } + + private void setFeeRadioButtonState() { + switch (disputeResult.getFeePaymentPolicy()) { + case LOSER: + feeToggleGroup.selectToggle(loserPaysFeeRadioButton); + break; + case SPLIT: + feeToggleGroup.selectToggle(splitFeeRadioButton); + break; + case WAIVE: + feeToggleGroup.selectToggle(waiveFeeRadioButton); + break; + } + } + + private void addSummaryNotes() { + Label label = addLabel(gridPane, ++rowIndex, "Summary notes:", 0); + GridPane.setValignment(label, VPos.TOP); + + summaryNotesTextArea = new TextArea(); + summaryNotesTextArea.setPromptText("Add summary notes"); + summaryNotesTextArea.setWrapText(true); + summaryNotesTextArea.textProperty().bindBidirectional(disputeResult.summaryNotesProperty()); + GridPane.setRowIndex(summaryNotesTextArea, rowIndex); + GridPane.setColumnIndex(summaryNotesTextArea, 1); + gridPane.getChildren().add(summaryNotesTextArea); + } + + private void addButtons(Contract contract) { + Tuple2 tuple = add2ButtonsAfterGroup(gridPane, ++rowIndex, "Close ticket", "Cancel"); + Button closeTicketButton = tuple.first; + closeTicketButton.disableProperty().bind(Bindings.createBooleanBinding( + () -> tradeAmountToggleGroup.getSelectedToggle() == null + || summaryNotesTextArea.getText() == null + || summaryNotesTextArea.getText().length() == 0, + tradeAmountToggleGroup.selectedToggleProperty(), + summaryNotesTextArea.textProperty())); + + Button cancelButton = tuple.second; + + final Dispute finalPeersDispute = peersDisputeOptional.get(); + closeTicketButton.setOnAction(e -> { + if (dispute.getDepositTxSerialized() != null) { + try { + AddressEntry arbitratorAddressEntry = walletService.getArbitratorAddressEntry(); + disputeResult.setArbitratorAddressAsString(arbitratorAddressEntry.getAddressString()); + disputeResult.setArbitratorPubKey(arbitratorAddressEntry.getPubKey()); + byte[] arbitratorSignature = tradeWalletService.signDisputedPayoutTx( + dispute.getDepositTxSerialized(), + disputeResult.getBuyerPayoutAmount(), + disputeResult.getSellerPayoutAmount(), + disputeResult.getArbitratorPayoutAmount(), + contract.getBuyerPayoutAddressString(), + contract.getSellerPayoutAddressString(), + arbitratorAddressEntry, + contract.getBuyerBtcPubKey(), + contract.getSellerBtcPubKey(), + arbitratorAddressEntry.getPubKey() + ); + disputeResult.setArbitratorSignature(arbitratorSignature); + + closeTicketButton.disableProperty().unbind(); + dispute.setDisputeResult(disputeResult); + + disputeResult.setCloseDate(new Date()); + String text = "Ticket closed on " + formatter.formatDateTime(disputeResult.getCloseDate()) + + "\n\nSummary:" + + "\n" + role + " delivered tamper proof evidence: " + formatter.booleanToYesNo(disputeResult.tamperProofEvidenceProperty().get()) + + "\n" + role + " did ID verification: " + formatter.booleanToYesNo(disputeResult.idVerificationProperty().get()) + + "\n" + role + " did screencast or video: " + formatter.booleanToYesNo(disputeResult.screenCastProperty().get()) + + "\nPayout amount for buyer: " + formatter.formatCoinWithCode(disputeResult.getBuyerPayoutAmount()) + + "\nPayout amount for seller: " + formatter.formatCoinWithCode(disputeResult.getSellerPayoutAmount()) + + "\nArbitrators dispute fee: " + formatter.formatCoinWithCode(disputeResult.getArbitratorPayoutAmount()) + + "\n\nSummary notes:\n" + disputeResult.summaryNotesProperty().get(); + + dispute.setIsClosed(true); + disputeManager.sendDisputeResultMessage(disputeResult, dispute, text); + + if (!finalPeersDispute.isClosed()) + FxTimer.runLater(Duration.ofMillis(Transitions.DEFAULT_DURATION), () -> new Popup().information( + "You need to close also the trading peers ticket!").show()); + + hide(); + + finalizeDisputeHandlerOptional.ifPresent(finalizeDisputeHandler -> finalizeDisputeHandler.run()); + } catch (AddressFormatException | TransactionVerificationException e2) { + e2.printStackTrace(); + } + } else { + log.warn("dispute.getDepositTxOptional is empty"); + } + }); + + cancelButton.setOnAction(e -> { + dispute.setDisputeResult(disputeResult); + hide(); + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Controller + /////////////////////////////////////////////////////////////////////////////////////////// + + private void applyPayoutAmounts(DisputeResult.FeePaymentPolicy feePayment, Toggle selectedTradeAmountToggle) { + calculatePayoutAmounts(feePayment); + if (selectedTradeAmountToggle != null) { + applyPayoutAmountsToDisputeResult(selectedTradeAmountToggle); + applyTradeAmountRadioButtonStates(); + } + } + + private void calculatePayoutAmounts(DisputeResult.FeePaymentPolicy feePayment) { + Contract contract = dispute.getContract(); + Coin refund = FeePolicy.SECURITY_DEPOSIT; + Coin winnerRefund; + Coin loserRefund; + switch (feePayment) { + case SPLIT: + winnerRefund = refund.divide(2L); + loserRefund = winnerRefund; + arbitratorPayoutAmount = refund; + break; + case WAIVE: + winnerRefund = refund; + loserRefund = refund; + arbitratorPayoutAmount = Coin.ZERO; + break; + case LOSER: + default: + winnerRefund = refund; + loserRefund = Coin.ZERO; + arbitratorPayoutAmount = refund; + break; + } + winnerPayoutAmount = contract.getTradeAmount().add(winnerRefund); + loserPayoutAmount = loserRefund; + stalematePayoutAmount = contract.getTradeAmount().divide(2L).add(winnerRefund); + } + + private void applyPayoutAmountsToDisputeResult(Toggle selectedTradeAmountToggle) { + if (selectedTradeAmountToggle == buyerIsWinnerRadioButton) { + disputeResult.setBuyerPayoutAmount(winnerPayoutAmount); + disputeResult.setSellerPayoutAmount(loserPayoutAmount); + disputeResult.setWinner(DisputeResult.Winner.BUYER); + } else if (selectedTradeAmountToggle == sellerIsWinnerRadioButton) { + disputeResult.setBuyerPayoutAmount(loserPayoutAmount); + disputeResult.setSellerPayoutAmount(winnerPayoutAmount); + disputeResult.setWinner(DisputeResult.Winner.SELLER); + } else if (selectedTradeAmountToggle == shareRadioButton) { + disputeResult.setBuyerPayoutAmount(stalematePayoutAmount); + disputeResult.setSellerPayoutAmount(stalematePayoutAmount); + disputeResult.setWinner(DisputeResult.Winner.STALE_MATE); + } + disputeResult.setArbitratorPayoutAmount(arbitratorPayoutAmount); + if (disputeResult.getBuyerPayoutAmount() != null) { + log.debug("buyerPayoutAmount " + disputeResult.getBuyerPayoutAmount().toFriendlyString()); + log.debug("sellerPayoutAmount " + disputeResult.getSellerPayoutAmount().toFriendlyString()); + log.debug("arbitratorPayoutAmount " + disputeResult.getArbitratorPayoutAmount().toFriendlyString()); + } + } + + private void applyTradeAmountRadioButtonStates() { + if (disputeResult.getBuyerPayoutAmount() != null) { + if (disputeResult.getBuyerPayoutAmount().equals(winnerPayoutAmount) && disputeResult.getSellerPayoutAmount().equals(loserPayoutAmount)) + buyerIsWinnerRadioButton.setSelected(true); + else if (disputeResult.getSellerPayoutAmount().equals(winnerPayoutAmount) && disputeResult.getBuyerPayoutAmount().equals(loserPayoutAmount)) + sellerIsWinnerRadioButton.setSelected(true); + else + shareRadioButton.setSelected(true); // there might be a not perfect split if only the trade amount is split but fees are not split + } + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/main/disputes/trader/TraderDisputeView.fxml b/gui/src/main/java/io/bitsquare/gui/main/disputes/trader/TraderDisputeView.fxml new file mode 100644 index 0000000000..c211d0333d --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/disputes/trader/TraderDisputeView.fxml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + diff --git a/gui/src/main/java/io/bitsquare/gui/main/disputes/trader/TraderDisputeView.java b/gui/src/main/java/io/bitsquare/gui/main/disputes/trader/TraderDisputeView.java new file mode 100644 index 0000000000..b002aa0531 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/disputes/trader/TraderDisputeView.java @@ -0,0 +1,763 @@ +/* + * 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.disputes.trader; + +import com.google.common.io.ByteStreams; +import de.jensd.fx.fontawesome.AwesomeDude; +import de.jensd.fx.fontawesome.AwesomeIcon; +import io.bitsquare.arbitration.Dispute; +import io.bitsquare.arbitration.DisputeManager; +import io.bitsquare.arbitration.messages.DisputeMailMessage; +import io.bitsquare.btc.TradeWalletService; +import io.bitsquare.btc.WalletService; +import io.bitsquare.common.UserThread; +import io.bitsquare.common.crypto.KeyRing; +import io.bitsquare.gui.Navigation; +import io.bitsquare.gui.common.view.ActivatableView; +import io.bitsquare.gui.common.view.FxmlView; +import io.bitsquare.gui.components.TableGroupHeadline; +import io.bitsquare.gui.popups.ContractPopup; +import io.bitsquare.gui.popups.Popup; +import io.bitsquare.gui.popups.TradeDetailsPopup; +import io.bitsquare.gui.util.BSFormatter; +import io.bitsquare.gui.util.GUIUtil; +import io.bitsquare.p2p.network.Connection; +import io.bitsquare.trade.Trade; +import io.bitsquare.trade.TradeManager; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.value.ChangeListener; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; +import javafx.collections.transformation.SortedList; +import javafx.geometry.Insets; +import javafx.scene.control.*; +import javafx.scene.image.ImageView; +import javafx.scene.layout.*; +import javafx.scene.paint.Paint; +import javafx.scene.text.TextAlignment; +import javafx.stage.FileChooser; +import javafx.stage.Stage; +import javafx.util.Callback; +import org.reactfx.util.FxTimer; +import org.reactfx.util.Timer; + +import javax.inject.Inject; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +// will be probably only used for arbitration communication, will be renamed and the icon changed +@FxmlView +public class TraderDisputeView extends ActivatableView { + + private final DisputeManager disputeManager; + protected final KeyRing keyRing; + private final TradeWalletService tradeWalletService; + private final WalletService walletService; + private TradeManager tradeManager; + private final Stage stage; + private final BSFormatter formatter; + private final Navigation navigation; + private final DisputeSummaryPopup disputeSummaryPopup; + private ContractPopup contractPopup; + private TradeDetailsPopup tradeDetailsPopup; + + private final List tempAttachments = new ArrayList<>(); + + private TableColumn tradeIdColumn, roleColumn, dateColumn, contractColumn, stateColumn; + private TableView disputesTable; + private Dispute selectedDispute; + private ChangeListener disputeChangeListener; + private ListView messageListView; + private TextArea inputTextArea; + private AnchorPane messagesAnchorPane; + private VBox messagesInputBox; + private ProgressIndicator sendMsgProgressIndicator; + private Label sendMsgInfoLabel; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public TraderDisputeView(DisputeManager disputeManager, KeyRing keyRing, TradeWalletService tradeWalletService, WalletService walletService, + TradeManager tradeManager, Stage stage, BSFormatter formatter, Navigation navigation, DisputeSummaryPopup disputeSummaryPopup, + ContractPopup contractPopup, TradeDetailsPopup tradeDetailsPopup) { + this.disputeManager = disputeManager; + this.keyRing = keyRing; + this.tradeWalletService = tradeWalletService; + this.walletService = walletService; + this.tradeManager = tradeManager; + this.stage = stage; + this.formatter = formatter; + this.navigation = navigation; + this.disputeSummaryPopup = disputeSummaryPopup; + this.contractPopup = contractPopup; + this.tradeDetailsPopup = tradeDetailsPopup; + } + + @Override + public void initialize() { + disputesTable = new TableView<>(); + VBox.setVgrow(disputesTable, Priority.SOMETIMES); + disputesTable.setMinHeight(150); + root.getChildren().add(disputesTable); + + tradeIdColumn = getTradeIdColumn(); + disputesTable.getColumns().add(tradeIdColumn); + roleColumn = getRoleColumn(); + disputesTable.getColumns().add(roleColumn); + dateColumn = getDateColumn(); + disputesTable.getColumns().add(dateColumn); + contractColumn = getContractColumn(); + disputesTable.getColumns().add(contractColumn); + stateColumn = getStateColumn(); + disputesTable.getColumns().add(stateColumn); + + disputesTable.getSortOrder().add(dateColumn); + disputesTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + Label placeholder = new Label("There are no open tickets"); + placeholder.setWrapText(true); + disputesTable.setPlaceholder(placeholder); + disputesTable.getSelectionModel().clearSelection(); + + disputeChangeListener = (observableValue, oldValue, newValue) -> onSelectDispute(newValue); + } + + @Override + protected void activate() { + FilteredList filteredList = new FilteredList<>(disputeManager.getDisputesAsObservableList()); + setFilteredListPredicate(filteredList); + SortedList sortedList = new SortedList(filteredList); + sortedList.setComparator((o1, o2) -> o1.getOpeningDate().compareTo(o2.getOpeningDate())); + disputesTable.setItems(sortedList); + disputesTable.getSelectionModel().selectedItemProperty().addListener(disputeChangeListener); + + Dispute selectedItem = disputesTable.getSelectionModel().getSelectedItem(); + if (selectedItem != null) + disputesTable.getSelectionModel().select(selectedItem); + + scrollToBottom(); + } + + @Override + protected void deactivate() { + disputesTable.getSelectionModel().selectedItemProperty().removeListener(disputeChangeListener); + } + + protected void setFilteredListPredicate(FilteredList filteredList) { + filteredList.setPredicate(dispute -> !dispute.getArbitratorPubKeyRing().equals(keyRing.getPubKeyRing())); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // UI actions + /////////////////////////////////////////////////////////////////////////////////////////// + + private void onOpenContract(Dispute dispute) { + contractPopup.show(dispute); + } + + private void onSendMessage(String inputText, Dispute dispute) { + DisputeMailMessage disputeMailMessage = disputeManager.sendDisputeMailMessage(dispute, inputText, new ArrayList<>(tempAttachments)); + tempAttachments.clear(); + scrollToBottom(); + + inputTextArea.setDisable(true); + inputTextArea.clear(); + + final Timer timer = FxTimer.runLater(Duration.ofMillis(500), () -> { + sendMsgInfoLabel.setVisible(true); + sendMsgInfoLabel.setManaged(true); + sendMsgInfoLabel.setText("Sending Message..."); + + sendMsgProgressIndicator.setProgress(-1); + sendMsgProgressIndicator.setVisible(true); + sendMsgProgressIndicator.setManaged(true); + }); + + disputeMailMessage.arrivedProperty().addListener((observable, oldValue, newValue) -> { + if (newValue) { + hideSendMsgInfo(timer); + } + }); + disputeMailMessage.storedInMailboxProperty().addListener((observable, oldValue, newValue) -> { + if (newValue) { + sendMsgInfoLabel.setVisible(true); + sendMsgInfoLabel.setManaged(true); + sendMsgInfoLabel.setText("Receiver is not online. Message is saved to his mailbox."); + hideSendMsgInfo(timer); + } + }); + } + + private void hideSendMsgInfo(Timer timer) { + timer.stop(); + inputTextArea.setDisable(false); + + FxTimer.runLater(Duration.ofMillis(5000), () -> { + sendMsgInfoLabel.setVisible(false); + sendMsgInfoLabel.setManaged(false); + }); + sendMsgProgressIndicator.setProgress(0); + sendMsgProgressIndicator.setVisible(false); + sendMsgProgressIndicator.setManaged(false); + } + + private void onCloseDispute(Dispute dispute) { + disputeSummaryPopup.onFinalizeDispute(() -> messagesAnchorPane.getChildren().remove(messagesInputBox)).show(dispute); + } + + private void onRequestUpload() { + if (tempAttachments.size() < 3) { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Open file to attach"); + /* if (Utilities.isUnix()) + fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));*/ + File result = fileChooser.showOpenDialog(stage); + if (result != null) { + try { + URL url = result.toURI().toURL(); + try (InputStream inputStream = url.openStream()) { + byte[] filesAsBytes = ByteStreams.toByteArray(inputStream); + if (filesAsBytes.length <= Connection.getMaxMsgSize()) { + tempAttachments.add(new DisputeMailMessage.Attachment(result.getName(), filesAsBytes)); + inputTextArea.setText(inputTextArea.getText() + "\n[Attachment " + result.getName() + "]"); + } else { + new Popup().error("The max. allowed file size is 100 kB.").show(); + } + } catch (java.io.IOException e) { + e.printStackTrace(); + log.error(e.getMessage()); + } + } catch (MalformedURLException e2) { + e2.printStackTrace(); + log.error(e2.getMessage()); + } + } + } else { + new Popup().error("You cannot send more then 3 attachments in one message.").show(); + } + } + + private void onOpenAttachment(DisputeMailMessage.Attachment attachment) { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Save file to disk"); + fileChooser.setInitialFileName(attachment.getFileName()); + /* if (Utilities.isUnix()) + fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));*/ + File file = fileChooser.showSaveDialog(stage); + if (file != null) { + try (FileOutputStream fileOutputStream = new FileOutputStream(file.getAbsolutePath())) { + fileOutputStream.write(attachment.getBytes()); + } catch (IOException e) { + e.printStackTrace(); + System.out.println(e.getMessage()); + } + } + } + + private void onSelectDispute(Dispute dispute) { + if (dispute == null) { + if (root.getChildren().size() > 1) + root.getChildren().remove(1); + + this.selectedDispute = dispute; + } else if (selectedDispute != dispute) { + this.selectedDispute = dispute; + + boolean isTrader = disputeManager.isTrader(dispute); + + TableGroupHeadline tableGroupHeadline = new TableGroupHeadline(); + tableGroupHeadline.setText("Messages"); + tableGroupHeadline.prefWidthProperty().bind(root.widthProperty()); + AnchorPane.setTopAnchor(tableGroupHeadline, 10d); + AnchorPane.setRightAnchor(tableGroupHeadline, 0d); + AnchorPane.setBottomAnchor(tableGroupHeadline, 0d); + AnchorPane.setLeftAnchor(tableGroupHeadline, 0d); + + ObservableList list = dispute.getDisputeMailMessagesAsObservableList(); + SortedList sortedList = new SortedList(list); + sortedList.setComparator((o1, o2) -> o1.getDate().compareTo(o2.getDate())); + list.addListener((ListChangeListener) c -> scrollToBottom()); + messageListView = new ListView<>(sortedList); + messageListView.setId("message-list-view"); + messageListView.prefWidthProperty().bind(root.widthProperty()); + messageListView.setMinHeight(150); + AnchorPane.setTopAnchor(messageListView, 30d); + AnchorPane.setRightAnchor(messageListView, 0d); + AnchorPane.setLeftAnchor(messageListView, 0d); + + messagesAnchorPane = new AnchorPane(); + messagesAnchorPane.prefWidthProperty().bind(root.widthProperty()); + VBox.setVgrow(messagesAnchorPane, Priority.ALWAYS); + + inputTextArea = new TextArea(); + inputTextArea.setPrefHeight(70); + inputTextArea.setWrapText(true); + + Button sendButton = new Button("Send"); + sendButton.setDefaultButton(true); + sendButton.setOnAction(e -> onSendMessage(inputTextArea.getText(), dispute)); + sendButton.setDisable(true); + inputTextArea.textProperty().addListener((observable, oldValue, newValue) -> { + sendButton.setDisable(newValue.length() == 0 && tempAttachments.size() == 0 && dispute.disputeResultProperty().get() == null); + }); + + Button uploadButton = new Button("Add attachments"); + uploadButton.setOnAction(e -> onRequestUpload()); + + sendMsgInfoLabel = new Label(); + sendMsgInfoLabel.setVisible(false); + sendMsgInfoLabel.setManaged(false); + sendMsgInfoLabel.setPadding(new Insets(5, 0, 0, 0)); + + sendMsgProgressIndicator = new ProgressIndicator(0); + sendMsgProgressIndicator.setPrefHeight(24); + sendMsgProgressIndicator.setPrefWidth(24); + sendMsgProgressIndicator.setVisible(false); + sendMsgProgressIndicator.setManaged(false); + + dispute.isClosedProperty().addListener((observable, oldValue, newValue) -> { + messagesInputBox.setVisible(!newValue); + messagesInputBox.setManaged(!newValue); + AnchorPane.setBottomAnchor(messageListView, newValue ? 0d : 120d); + }); + if (!dispute.isClosed()) { + HBox buttonBox = new HBox(); + buttonBox.setSpacing(10); + buttonBox.getChildren().addAll(sendButton, uploadButton, sendMsgProgressIndicator, sendMsgInfoLabel); + + if (!isTrader) { + Button closeDisputeButton = new Button("Close ticket"); + closeDisputeButton.setOnAction(e -> onCloseDispute(dispute)); + closeDisputeButton.setDefaultButton(true); + Pane spacer = new Pane(); + HBox.setHgrow(spacer, Priority.ALWAYS); + buttonBox.getChildren().addAll(spacer, closeDisputeButton); + } + + messagesInputBox = new VBox(); + messagesInputBox.setSpacing(10); + messagesInputBox.getChildren().addAll(inputTextArea, buttonBox); + VBox.setVgrow(buttonBox, Priority.ALWAYS); + + AnchorPane.setRightAnchor(messagesInputBox, 0d); + AnchorPane.setBottomAnchor(messagesInputBox, 5d); + AnchorPane.setLeftAnchor(messagesInputBox, 0d); + + AnchorPane.setBottomAnchor(messageListView, 120d); + + messagesAnchorPane.getChildren().addAll(tableGroupHeadline, messageListView, messagesInputBox); + } else { + AnchorPane.setBottomAnchor(messageListView, 0d); + messagesAnchorPane.getChildren().addAll(tableGroupHeadline, messageListView); + } + + messageListView.setCellFactory(new Callback, ListCell>() { + @Override + public ListCell call(ListView list) { + return new ListCell() { + final Pane bg = new Pane(); + final ImageView arrow = new ImageView(); + final Label headerLabel = new Label(); + final Label messageLabel = new Label(); + final HBox attachmentsBox = new HBox(); + final AnchorPane messageAnchorPane = new AnchorPane(); + final Label statusIcon = new Label(); + final double arrowWidth = 15d; + final double attachmentsBoxHeight = 20d; + final double border = 10d; + final double bottomBorder = 25d; + final double padding = border + 10d; + + { + bg.setMinHeight(30); + messageLabel.setWrapText(true); + headerLabel.setTextAlignment(TextAlignment.CENTER); + attachmentsBox.setSpacing(5); + statusIcon.setStyle("-fx-font-size: 10;"); + messageAnchorPane.getChildren().addAll(bg, arrow, headerLabel, messageLabel, attachmentsBox, statusIcon); + } + + @Override + public void updateItem(final DisputeMailMessage item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + /* messageAnchorPane.prefWidthProperty().bind(EasyBind.map(messageListView.widthProperty(), + w -> (double) w - padding - GUIUtil.getScrollbarWidth(messageListView)));*/ + if (!messageAnchorPane.prefWidthProperty().isBound()) + messageAnchorPane.prefWidthProperty() + .bind(messageListView.widthProperty().subtract(padding + GUIUtil.getScrollbarWidth(messageListView))); + + AnchorPane.setTopAnchor(bg, 15d); + AnchorPane.setBottomAnchor(bg, bottomBorder); + AnchorPane.setTopAnchor(headerLabel, 0d); + AnchorPane.setBottomAnchor(arrow, bottomBorder + 5d); + AnchorPane.setTopAnchor(messageLabel, 25d); + AnchorPane.setBottomAnchor(attachmentsBox, bottomBorder + 10); + + boolean senderIsTrader = item.isSenderIsTrader(); + boolean isMyMsg = isTrader ? senderIsTrader : !senderIsTrader; + + arrow.setVisible(!item.isSystemMessage()); + arrow.setManaged(!item.isSystemMessage()); + statusIcon.setVisible(false); + if (item.isSystemMessage()) { + headerLabel.setStyle("-fx-text-fill: -bs-green; -fx-font-size: 11;"); + bg.setId("message-bubble-green"); + messageLabel.setStyle("-fx-text-fill: white;"); + } else if (isMyMsg) { + headerLabel.setStyle("-fx-text-fill: -fx-accent; -fx-font-size: 11;"); + bg.setId("message-bubble-blue"); + messageLabel.setStyle("-fx-text-fill: white;"); + if (isTrader) + arrow.setId("bubble_arrow_blue_left"); + else + arrow.setId("bubble_arrow_blue_right"); + + sendMsgProgressIndicator.progressProperty().addListener((observable, oldValue, newValue) -> { + if ((double) oldValue == -1 && (double) newValue == 0) { + if (item.arrivedProperty().get()) + showArrivedIcon(); + else if (item.storedInMailboxProperty().get()) + showMailboxIcon(); + } + }); + + if (item.arrivedProperty().get()) + showArrivedIcon(); + else if (item.storedInMailboxProperty().get()) + showMailboxIcon(); + //TODO show that icon on error + /*else if (sendMsgProgressIndicator.getProgress() == 0) + showNotArrivedIcon();*/ + } else { + headerLabel.setStyle("-fx-text-fill: -bs-light-grey; -fx-font-size: 11;"); + bg.setId("message-bubble-grey"); + messageLabel.setStyle("-fx-text-fill: black;"); + if (isTrader) + arrow.setId("bubble_arrow_grey_right"); + else + arrow.setId("bubble_arrow_grey_left"); + } + + if (item.isSystemMessage()) { + AnchorPane.setLeftAnchor(headerLabel, padding); + AnchorPane.setRightAnchor(headerLabel, padding); + AnchorPane.setLeftAnchor(bg, border); + AnchorPane.setRightAnchor(bg, border); + AnchorPane.setLeftAnchor(messageLabel, padding); + AnchorPane.setRightAnchor(messageLabel, padding); + AnchorPane.setLeftAnchor(attachmentsBox, padding); + AnchorPane.setRightAnchor(attachmentsBox, padding); + } else if (senderIsTrader) { + AnchorPane.setLeftAnchor(headerLabel, padding + arrowWidth); + AnchorPane.setLeftAnchor(bg, border + arrowWidth); + AnchorPane.setRightAnchor(bg, border); + AnchorPane.setLeftAnchor(arrow, border); + AnchorPane.setLeftAnchor(messageLabel, padding + arrowWidth); + AnchorPane.setRightAnchor(messageLabel, padding); + AnchorPane.setLeftAnchor(attachmentsBox, padding + arrowWidth); + AnchorPane.setRightAnchor(attachmentsBox, padding); + AnchorPane.setRightAnchor(statusIcon, padding); + } else { + AnchorPane.setRightAnchor(headerLabel, padding + arrowWidth); + AnchorPane.setLeftAnchor(bg, border); + AnchorPane.setRightAnchor(bg, border + arrowWidth); + AnchorPane.setRightAnchor(arrow, border); + AnchorPane.setLeftAnchor(messageLabel, padding); + AnchorPane.setRightAnchor(messageLabel, padding + arrowWidth); + AnchorPane.setLeftAnchor(attachmentsBox, padding); + AnchorPane.setRightAnchor(attachmentsBox, padding + arrowWidth); + AnchorPane.setLeftAnchor(statusIcon, padding); + } + + AnchorPane.setBottomAnchor(statusIcon, 7d); + headerLabel.setText(formatter.formatDateTime(item.getDate())); + messageLabel.setText(item.getMessage()); + if (item.getAttachments().size() > 0) { + AnchorPane.setBottomAnchor(messageLabel, bottomBorder + attachmentsBoxHeight + 10); + attachmentsBox.getChildren().add(new Label("Attachments: ") {{ + setPadding(new Insets(0, 0, 3, 0)); + if (isMyMsg) + setStyle("-fx-text-fill: white;"); + else + setStyle("-fx-text-fill: black;"); + }}); + + item.getAttachments().stream().forEach(attachment -> { + final Label icon = new Label(); + setPadding(new Insets(0, 0, 3, 0)); + if (isMyMsg) + icon.getStyleClass().add("attachment-icon"); + else + icon.getStyleClass().add("attachment-icon-black"); + + AwesomeDude.setIcon(icon, AwesomeIcon.FILE_TEXT); + icon.setPadding(new Insets(-2, 0, 0, 0)); + icon.setTooltip(new Tooltip(attachment.getFileName())); + icon.setOnMouseClicked(event -> onOpenAttachment(attachment)); + attachmentsBox.getChildren().add(icon); + }); + } else { + attachmentsBox.getChildren().clear(); + AnchorPane.setBottomAnchor(messageLabel, bottomBorder + 10); + } + + + // TODO There are still some cell rendering issues on updates + setGraphic(messageAnchorPane); + } else { + messageAnchorPane.prefWidthProperty().unbind(); + + AnchorPane.clearConstraints(bg); + AnchorPane.clearConstraints(headerLabel); + AnchorPane.clearConstraints(arrow); + AnchorPane.clearConstraints(messageLabel); + AnchorPane.clearConstraints(statusIcon); + AnchorPane.clearConstraints(attachmentsBox); + + setGraphic(null); + } + } + + private void showNotArrivedIcon() { + statusIcon.setVisible(true); + AwesomeDude.setIcon(statusIcon, AwesomeIcon.WARNING_SIGN, "14"); + Tooltip.install(statusIcon, new Tooltip("Message did not arrive. Please try to send again.")); + statusIcon.setTextFill(Paint.valueOf("#dd0000")); + } + + private void showMailboxIcon() { + statusIcon.setVisible(true); + AwesomeDude.setIcon(statusIcon, AwesomeIcon.ENVELOPE_ALT, "14"); + Tooltip.install(statusIcon, new Tooltip("Message saved in receivers mailbox")); + statusIcon.setTextFill(Paint.valueOf("#0f87c3")); + } + + private void showArrivedIcon() { + statusIcon.setVisible(true); + AwesomeDude.setIcon(statusIcon, AwesomeIcon.OK, "14"); + Tooltip.install(statusIcon, new Tooltip("Message arrived at receiver")); + statusIcon.setTextFill(Paint.valueOf("#0f87c3")); + } + }; + } + }); + + if (root.getChildren().size() > 1) + root.getChildren().remove(1); + root.getChildren().add(1, messagesAnchorPane); + + scrollToBottom(); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Table + /////////////////////////////////////////////////////////////////////////////////////////// + + private TableColumn getTradeIdColumn() { + TableColumn column = new TableColumn("Trade ID") { + { + setMinWidth(130); + } + }; + column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); + column.setCellFactory( + new Callback, TableCell>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell() { + private Hyperlink hyperlink; + + @Override + public void updateItem(final Dispute item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + Optional tradeOptional = tradeManager.getTradeById(item.getTradeId()); + hyperlink = new Hyperlink(item.getShortTradeId()); + if (tradeOptional.isPresent()) { + hyperlink.setMouseTransparent(false); + Tooltip.install(hyperlink, new Tooltip(item.getShortTradeId())); + hyperlink.setOnAction(event -> tradeDetailsPopup.show(tradeOptional.get())); + } else { + hyperlink.setMouseTransparent(true); + } + setGraphic(hyperlink); + } else { + setGraphic(null); + setId(null); + } + } + }; + } + }); + return column; + } + + private TableColumn getRoleColumn() { + TableColumn column = new TableColumn("Role") { + { + setMinWidth(130); + } + }; + column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); + column.setCellFactory( + new Callback, TableCell>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell() { + @Override + public void updateItem(final Dispute item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + if (item.isDisputeOpenerIsOfferer()) + setText(item.isDisputeOpenerIsBuyer() ? "Buyer/Offerer" : "Seller/Offerer"); + else + setText(item.isDisputeOpenerIsBuyer() ? "Buyer/Taker" : "Seller/Taker"); + } else { + setText(""); + } + } + }; + } + }); + return column; + } + + private TableColumn getDateColumn() { + TableColumn column = new TableColumn("Date") { + { + setMinWidth(130); + } + }; + column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); + column.setCellFactory( + new Callback, TableCell>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell() { + @Override + public void updateItem(final Dispute item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) + setText(formatter.formatDateTime(item.getOpeningDate())); + else + setText(""); + } + }; + } + }); + return column; + } + + private TableColumn getContractColumn() { + TableColumn column = new TableColumn("Contract") { + { + setMinWidth(80); + setSortable(false); + } + }; + column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); + column.setCellFactory( + new Callback, TableCell>() { + + @Override + public TableCell call(TableColumn column) { + return new TableCell() { + final Button button = new Button("Open contract"); + + { + + } + + @Override + public void updateItem(final Dispute item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + button.setOnAction(e -> onOpenContract(item)); + setGraphic(button); + } else { + setGraphic(null); + button.setOnAction(null); + } + } + }; + } + }); + return column; + } + + private TableColumn getStateColumn() { + TableColumn column = new TableColumn("State") { + { + setMinWidth(50); + } + }; + column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); + column.setCellFactory( + new Callback, TableCell>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell() { + + + @Override + public void updateItem(final Dispute item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + item.isClosedProperty().addListener((observable, oldValue, newValue) -> { + setText(newValue ? "Closed" : "Open"); + getTableRow().setOpacity(newValue ? 0.4 : 1); + }); + boolean isClosed = item.isClosed(); + setText(isClosed ? "Closed" : "Open"); + getTableRow().setOpacity(isClosed ? 0.4 : 1); + } else { + setText(""); + } + } + }; + } + }); + return column; + } + + private void scrollToBottom() { + if (messageListView != null) + UserThread.execute(() -> messageListView.scrollTo(Integer.MAX_VALUE)); + } + +} + + diff --git a/gui/src/main/java/io/bitsquare/gui/main/market/MarketView.fxml b/gui/src/main/java/io/bitsquare/gui/main/market/MarketView.fxml new file mode 100644 index 0000000000..604305a7fc --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/market/MarketView.fxml @@ -0,0 +1,28 @@ + + + + + + + + + + diff --git a/gui/src/main/java/io/bitsquare/gui/main/market/MarketView.java b/gui/src/main/java/io/bitsquare/gui/main/market/MarketView.java new file mode 100644 index 0000000000..b0656a9db8 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/market/MarketView.java @@ -0,0 +1,287 @@ +/* + * 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.market; + +import io.bitsquare.common.util.Tuple2; +import io.bitsquare.gui.common.view.ActivatableViewAndModel; +import io.bitsquare.gui.common.view.FxmlView; +import io.bitsquare.gui.main.offer.offerbook.OfferBookListItem; +import io.bitsquare.gui.util.BSFormatter; +import io.bitsquare.locale.TradeCurrency; +import io.bitsquare.trade.offer.Offer; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.collections.ListChangeListener; +import javafx.fxml.FXML; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.chart.AreaChart; +import javafx.scene.chart.NumberAxis; +import javafx.scene.chart.XYChart; +import javafx.scene.control.*; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import javafx.util.Callback; +import javafx.util.StringConverter; +import org.fxmisc.easybind.EasyBind; +import org.fxmisc.easybind.Subscription; + +import javax.inject.Inject; + +@FxmlView +public class MarketView extends ActivatableViewAndModel { + @FXML + Tab tab; + + private NumberAxis xAxis, yAxis; + XYChart.Series seriesBuy, seriesSell; + private ListChangeListener changeListener; + private BSFormatter formatter; + private TableView buyOfferTableView; + private TableView sellOfferTableView; + private AreaChart areaChart; + private ComboBox currencyComboBox; + private Subscription tradeCurrencySubscriber; + private StringProperty priceColumnLabel = new SimpleStringProperty("Price (EUR/BTC)"); + private StringProperty amountColumnLabel = new SimpleStringProperty("Amount (BTC)"); + private StringProperty volumeColumnLabel = new SimpleStringProperty("Volume (EUR)"); + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public MarketView(MarketViewModel model, BSFormatter formatter) { + super(model); + this.formatter = formatter; + + changeListener = c -> updateChartData(); + } + + @Override + public void initialize() { + currencyComboBox = new ComboBox<>(); + currencyComboBox.setPromptText("Select currency"); + currencyComboBox.setConverter(new StringConverter() { + @Override + public String toString(TradeCurrency tradeCurrency) { + return tradeCurrency.getCodeAndName(); + } + + @Override + public TradeCurrency fromString(String s) { + return null; + } + }); + + + Label currencyLabel = new Label("Currency:"); + HBox currencyHBox = new HBox(); + currencyHBox.setSpacing(5); + currencyHBox.setPadding(new Insets(10, -20, 0, 20)); + currencyHBox.setAlignment(Pos.CENTER_LEFT); + currencyHBox.getChildren().addAll(currencyLabel, currencyComboBox); + + createChart(); + + Tuple2, VBox> tupleBuy = getOfferTable(Offer.Direction.BUY); + Tuple2, VBox> tupleSell = getOfferTable(Offer.Direction.SELL); + buyOfferTableView = tupleBuy.first; + sellOfferTableView = tupleSell.first; + + HBox hBox = new HBox(); + hBox.setSpacing(30); + hBox.setAlignment(Pos.CENTER); + hBox.getChildren().addAll(tupleBuy.second, tupleSell.second); + + VBox vBox = new VBox(); + vBox.setSpacing(20); + vBox.setFillWidth(true); + vBox.setPadding(new Insets(10, 20, 10, 20)); + vBox.getChildren().addAll(currencyHBox, areaChart, hBox); + + tab.setContent(vBox); + } + + @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()); + updateChartData(); + }); + + model.getOfferBookListItems().addListener(changeListener); + tradeCurrencySubscriber = EasyBind.subscribe(model.tradeCurrency, + newValue -> { + String code = newValue.getCode(); + areaChart.setTitle("Offer book for " + newValue.getName()); + xAxis.setTickLabelFormatter(new NumberAxis.DefaultFormatter(xAxis, "", " " + code + "/BTC")); + priceColumnLabel.set("Price (" + code + "/BTC)"); + volumeColumnLabel.set("Volume (" + code + ")"); + }); + + buyOfferTableView.setItems(model.getBuyOfferList()); + sellOfferTableView.setItems(model.getSellOfferList()); + + updateChartData(); + } + + @Override + protected void deactivate() { + model.getOfferBookListItems().removeListener(changeListener); + tradeCurrencySubscriber.unsubscribe(); + } + + + private Tuple2, VBox> getOfferTable(Offer.Direction direction) { + TableView tableView = new TableView(); + + // price + TableColumn priceColumn = new TableColumn<>("Price (EUR/BTC)"); + priceColumn.textProperty().bind(priceColumnLabel); + priceColumn.setMinWidth(120); + priceColumn.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); + priceColumn.setCellFactory( + new Callback, TableCell>() { + @Override + public TableCell call( + TableColumn column) { + return new TableCell() { + @Override + public void updateItem(final Offer item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) + setText(formatter.formatFiat(item.getPrice())); + else + setText(""); + } + }; + } + }); + tableView.getColumns().add(priceColumn); + + // amount + TableColumn amountColumn = new TableColumn<>("Amount (BTC)"); + amountColumn.textProperty().bind(amountColumnLabel); + amountColumn.setMinWidth(120); + amountColumn.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); + amountColumn.setCellFactory( + new Callback, TableCell>() { + @Override + public TableCell call( + TableColumn column) { + return new TableCell() { + @Override + public void updateItem(final Offer item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) + setText(formatter.formatCoin(item.getAmount())); + else + setText(""); + } + }; + } + }); + tableView.getColumns().add(amountColumn); + + // volume + TableColumn volumeColumn = new TableColumn<>("Amount (EUR)"); + volumeColumn.setMinWidth(120); + volumeColumn.textProperty().bind(volumeColumnLabel); + volumeColumn.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); + volumeColumn.setCellFactory( + new Callback, TableCell>() { + @Override + public TableCell call( + TableColumn column) { + return new TableCell() { + @Override + public void updateItem(final Offer item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) + setText(formatter.formatFiat(item.getOfferVolume())); + else + setText(""); + } + }; + } + }); + tableView.getColumns().add(volumeColumn); + + tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + Label placeholder = new Label("Currently there are no offers available"); + placeholder.setWrapText(true); + tableView.setPlaceholder(placeholder); + + Label titleLabel = new Label(direction.equals(Offer.Direction.BUY) ? "Offers for buy bitcoin (bid)" : "Offers for sell bitcoin (ask)"); + titleLabel.setStyle("-fx-font-weight: bold; -fx-font-size: 16; -fx-alignment: center"); + titleLabel.prefWidthProperty().bind(tableView.widthProperty()); + + VBox vBox = new VBox(); + vBox.setSpacing(10); + vBox.setFillWidth(true); + vBox.setMinHeight(150); + vBox.getChildren().addAll(titleLabel, tableView); + return new Tuple2<>(tableView, vBox); + } + + + private void createChart() { + xAxis = new NumberAxis(); + xAxis.setForceZeroInRange(false); + xAxis.setAutoRanging(true); + xAxis.setLabel("Price"); + + yAxis = new NumberAxis(); + yAxis.setForceZeroInRange(false); + yAxis.setAutoRanging(true); + yAxis.setLabel("Amount"); + yAxis.setTickLabelFormatter(new NumberAxis.DefaultFormatter(yAxis, "", " BTC")); + + seriesBuy = new XYChart.Series(); + seriesBuy.setName("Offers for buy bitcoin "); + + seriesSell = new XYChart.Series(); + seriesSell.setName("Offers for sell bitcoin"); + + areaChart = new AreaChart<>(xAxis, yAxis); + areaChart.setAnimated(false); + areaChart.setId("charts"); + areaChart.setMinHeight(300); + areaChart.setPadding(new Insets(0, 30, 10, 0)); + areaChart.getData().addAll(seriesBuy, seriesSell); + } + + + private void updateChartData() { + seriesBuy.getData().clear(); + seriesSell.getData().clear(); + + seriesBuy.getData().addAll(model.getBuyData()); + seriesSell.getData().addAll(model.getSellData()); + } + + +} diff --git a/gui/src/main/java/io/bitsquare/gui/main/market/MarketViewModel.java b/gui/src/main/java/io/bitsquare/gui/main/market/MarketViewModel.java new file mode 100644 index 0000000000..b1504f0b71 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/market/MarketViewModel.java @@ -0,0 +1,174 @@ +/* + * 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.market; + +import com.google.common.math.LongMath; +import com.google.inject.Inject; +import io.bitsquare.gui.common.model.ActivatableViewModel; +import io.bitsquare.gui.main.offer.offerbook.OfferBook; +import io.bitsquare.gui.main.offer.offerbook.OfferBookListItem; +import io.bitsquare.locale.CurrencyUtil; +import io.bitsquare.locale.TradeCurrency; +import io.bitsquare.trade.offer.Offer; +import io.bitsquare.user.Preferences; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.scene.chart.XYChart; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +class MarketViewModel extends ActivatableViewModel { + + private final OfferBook offerBook; + private final Preferences preferences; + + final ObjectProperty tradeCurrency = new SimpleObjectProperty<>(CurrencyUtil.getDefaultFiatCurrency()); + private final List buyData = new ArrayList(); + private final List sellData = new ArrayList(); + private final ObservableList offerBookListItems; + private final ListChangeListener listChangeListener; + private final ObservableList buyOfferList = FXCollections.observableArrayList(); + private final ObservableList sellOfferList = FXCollections.observableArrayList(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public MarketViewModel(OfferBook offerBook, Preferences preferences) { + this.offerBook = offerBook; + this.preferences = preferences; + + offerBookListItems = offerBook.getOfferBookListItems(); + listChangeListener = c -> updateChartData(offerBookListItems); + } + + @Override + protected void activate() { + offerBookListItems.addListener(listChangeListener); + offerBook.fillOfferBookListItems(); + //updateChartData(offerBookListItems); + } + + @Override + protected void deactivate() { + offerBookListItems.removeListener(listChangeListener); + } + + private void updateChartData(ObservableList offerBookListItems) { + List offerList = offerBookListItems.stream() + .map(e -> e.getOffer()) + .collect(Collectors.toList()); + + buyOfferList.clear(); + buyOfferList.addAll(offerList + .stream() + .filter(e -> e.getCurrencyCode().equals(tradeCurrency.get().getCode()) + && e.getDirection().equals(Offer.Direction.BUY)) + .sorted((o1, o2) -> { + long a = o1.getPrice().value; + long b = o2.getPrice().value; + if (a != b) + return a < b ? 1 : -1; + return 0; + }) + .collect(Collectors.toList())); + iterateBuyOffers(buyOfferList, Offer.Direction.BUY, buyData); + + sellOfferList.clear(); + sellOfferList.addAll(offerList + .stream() + .filter(e -> e.getCurrencyCode().equals(tradeCurrency.get().getCode()) + && e.getDirection().equals(Offer.Direction.SELL)) + .sorted((o1, o2) -> { + long a = o1.getPrice().value; + long b = o2.getPrice().value; + if (a != b) + return a > b ? 1 : -1; + return 0; + }) + .collect(Collectors.toList())); + iterateBuyOffers(sellOfferList, Offer.Direction.SELL, sellData); + } + + private void iterateBuyOffers(List sortedList, Offer.Direction direction, List data) { + data.clear(); + double accumulatedAmount = 0; + for (Offer offer : sortedList) { + double price = (double) offer.getPrice().value / LongMath.pow(10, offer.getPrice().smallestUnitExponent()); + double amount = (double) offer.getAmount().value / LongMath.pow(10, offer.getAmount().smallestUnitExponent()); + accumulatedAmount += amount; + if (direction.equals(Offer.Direction.BUY)) + data.add(0, new XYChart.Data(price, accumulatedAmount)); + else + data.add(new XYChart.Data(price, accumulatedAmount)); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // UI actions + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onSetTradeCurrency(TradeCurrency tradeCurrency) { + this.tradeCurrency.set(tradeCurrency); + updateChartData(offerBookListItems); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + public List getBuyData() { + return buyData; + } + + public List getSellData() { + return sellData; + } + + public String getCurrencyCode() { + return tradeCurrency.get().getCode(); + } + + public ObservableList getOfferBookListItems() { + return offerBookListItems; + } + + public ObservableList getBuyOfferList() { + return buyOfferList; + } + + public ObservableList getSellOfferList() { + return sellOfferList; + } + + public ObservableList getTradeCurrencies() { + return preferences.getTradeCurrenciesAsObservable(); + } + + public TradeCurrency getTradeCurrency() { + return tradeCurrency.get(); + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/main/offer/offerbook/AllPaymentMethodsEntry.java b/gui/src/main/java/io/bitsquare/gui/main/offer/offerbook/AllPaymentMethodsEntry.java new file mode 100644 index 0000000000..1e9bd73213 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/offer/offerbook/AllPaymentMethodsEntry.java @@ -0,0 +1,26 @@ +/* + * 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.offer.offerbook; + +import io.bitsquare.payment.PaymentMethod; + +public class AllPaymentMethodsEntry extends PaymentMethod { + public AllPaymentMethodsEntry() { + super("All", 0, 0); + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/main/offer/offerbook/AllTradeCurrenciesEntry.java b/gui/src/main/java/io/bitsquare/gui/main/offer/offerbook/AllTradeCurrenciesEntry.java new file mode 100644 index 0000000000..99bc4d0455 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/offer/offerbook/AllTradeCurrenciesEntry.java @@ -0,0 +1,26 @@ +/* + * 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.offer.offerbook; + +import io.bitsquare.locale.TradeCurrency; + +public class AllTradeCurrenciesEntry extends TradeCurrency { + public AllTradeCurrenciesEntry() { + super(null, "All"); + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/main/portfolio/closedtrades/ClosedTradableListItem.java b/gui/src/main/java/io/bitsquare/gui/main/portfolio/closedtrades/ClosedTradableListItem.java new file mode 100644 index 0000000000..344b149f42 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/portfolio/closedtrades/ClosedTradableListItem.java @@ -0,0 +1,36 @@ +/* + * 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.portfolio.closedtrades; + +import io.bitsquare.trade.Tradable; + +/** + * We could remove that wrapper if it is not needed for additional UI only fields. + */ +class ClosedTradableListItem { + + private final Tradable tradable; + + ClosedTradableListItem(Tradable tradable) { + this.tradable = tradable; + } + + Tradable getTradable() { + return tradable; + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/main/portfolio/pendingtrades/steps/ConfirmPaymentReceivedView.java b/gui/src/main/java/io/bitsquare/gui/main/portfolio/pendingtrades/steps/ConfirmPaymentReceivedView.java new file mode 100644 index 0000000000..b2829cb070 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/portfolio/pendingtrades/steps/ConfirmPaymentReceivedView.java @@ -0,0 +1,161 @@ +/* + * 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.portfolio.pendingtrades.steps; + +import io.bitsquare.gui.components.TxIdTextField; +import io.bitsquare.gui.main.portfolio.pendingtrades.PendingTradesViewModel; +import io.bitsquare.gui.util.Layout; +import javafx.beans.value.ChangeListener; +import javafx.event.ActionEvent; +import javafx.geometry.HPos; +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 javafx.scene.layout.HBox; + +import static io.bitsquare.gui.util.FormBuilder.*; + +public class ConfirmPaymentReceivedView extends TradeStepDetailsView { + private final ChangeListener txIdChangeListener; + + private TxIdTextField txIdTextField; + private Button confirmFiatReceivedButton; + private Label statusLabel; + private ProgressIndicator statusProgressIndicator; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, Initialisation + /////////////////////////////////////////////////////////////////////////////////////////// + + public ConfirmPaymentReceivedView(PendingTradesViewModel model) { + super(model); + + txIdChangeListener = (ov, oldValue, newValue) -> txIdTextField.setup(newValue); + } + + @Override + public void doActivate() { + super.doActivate(); + + model.getTxId().addListener(txIdChangeListener); + txIdTextField.setup(model.getTxId().get()); + } + + @Override + public void doDeactivate() { + super.doDeactivate(); + + model.getTxId().removeListener(txIdChangeListener); + txIdTextField.cleanup(); + statusProgressIndicator.setProgress(0); + } + + @Override + protected void displayRequestCheckPayment() { + infoLabel.setStyle(" -fx-text-fill: -bs-error-red;"); + infoLabel.setText("You still have not confirmed the receipt of the payment!\n" + + "Please check you payment processor or bank account to see if the payment has arrived.\n" + + "If you do not confirm receipt until " + + model.getDateFromBlocks(openDisputeTimeInBlocks) + + " the trade will be investigated by the arbitrator."); + } + + @Override + protected void displayOpenForDisputeForm() { + infoLabel.setStyle(" -fx-text-fill: -bs-error-red;"); + infoLabel.setText("You have not confirmed the receipt of the payment!\n" + + "The max. period for the trade has elapsed (" + + model.getDateFromBlocks(openDisputeTimeInBlocks) + ")." + + "\nPlease contact now the arbitrator for opening a dispute."); + + addOpenDisputeButton(); + GridPane.setMargin(openDisputeButton, new Insets(0, 0, 0, 0)); + } + + @Override + protected void disputeInProgress() { + super.disputeInProgress(); + + confirmFiatReceivedButton.setDisable(true); + } + + //////////////////////////////////////////////////////////////////////////////////////// + // UI Handlers + /////////////////////////////////////////////////////////////////////////////////////////// + + private void onPaymentReceived(ActionEvent actionEvent) { + log.debug("onPaymentReceived"); + confirmFiatReceivedButton.setDisable(true); + + statusProgressIndicator.setVisible(true); + statusProgressIndicator.setProgress(-1); + statusLabel.setText("Sending message to trading partner..."); + + model.fiatPaymentReceived(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Setters + /////////////////////////////////////////////////////////////////////////////////////////// + + public void setInfoLabelText(String text) { + if (infoLabel != null) + infoLabel.setText(text); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Build view + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void buildGridEntries() { + addTitledGroupBg(gridPane, gridRow, 1, "Blockchain confirmation"); + txIdTextField = addLabelTxIdTextField(gridPane, gridRow, "Deposit transaction ID:", Layout.FIRST_ROW_DISTANCE).second; + + infoTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 1, "Information", Layout.GROUP_DISTANCE); + infoLabel = addMultilineLabel(gridPane, gridRow, Layout.FIRST_ROW_AND_GROUP_DISTANCE); + + HBox hBox = new HBox(); + hBox.setSpacing(10); + confirmFiatReceivedButton = new Button("Confirm payment receipt"); + confirmFiatReceivedButton.setDefaultButton(true); + confirmFiatReceivedButton.setOnAction(this::onPaymentReceived); + + statusProgressIndicator = new ProgressIndicator(0); + statusProgressIndicator.setPrefHeight(24); + statusProgressIndicator.setPrefWidth(24); + statusProgressIndicator.setVisible(false); + + statusLabel = new Label(); + statusLabel.setPadding(new Insets(5, 0, 0, 0)); + + hBox.getChildren().addAll(confirmFiatReceivedButton, statusProgressIndicator, statusLabel); + GridPane.setRowIndex(hBox, ++gridRow); + GridPane.setColumnIndex(hBox, 0); + GridPane.setHalignment(hBox, HPos.LEFT); + GridPane.setMargin(hBox, new Insets(15, 0, 0, 0)); + gridPane.getChildren().add(hBox); + } +} + + diff --git a/gui/src/main/java/io/bitsquare/gui/main/portfolio/pendingtrades/steps/StartPaymentView.java b/gui/src/main/java/io/bitsquare/gui/main/portfolio/pendingtrades/steps/StartPaymentView.java new file mode 100644 index 0000000000..f9617d9c2e --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/portfolio/pendingtrades/steps/StartPaymentView.java @@ -0,0 +1,186 @@ +/* + * 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.portfolio.pendingtrades.steps; + +import io.bitsquare.common.util.Tuple3; +import io.bitsquare.gui.components.TitledGroupBg; +import io.bitsquare.gui.components.TxIdTextField; +import io.bitsquare.gui.components.paymentmethods.*; +import io.bitsquare.gui.main.portfolio.pendingtrades.PendingTradesViewModel; +import io.bitsquare.gui.util.Layout; +import io.bitsquare.payment.PaymentAccountContractData; +import io.bitsquare.payment.PaymentMethod; +import javafx.beans.value.ChangeListener; +import javafx.event.ActionEvent; +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 static io.bitsquare.gui.util.FormBuilder.*; + +public class StartPaymentView extends TradeStepDetailsView { + private TxIdTextField txIdTextField; + + private Button paymentStartedButton; + private Label statusLabel; + + private final ChangeListener txIdChangeListener; + private ProgressIndicator statusProgressIndicator; + + private TitledGroupBg txConfirmationGroup; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, Initialisation + /////////////////////////////////////////////////////////////////////////////////////////// + + public StartPaymentView(PendingTradesViewModel model) { + super(model); + txIdChangeListener = (ov, oldValue, newValue) -> txIdTextField.setup(newValue); + } + + @Override + public void doActivate() { + super.doActivate(); + + model.getTxId().addListener(txIdChangeListener); + txIdTextField.setup(model.getTxId().get()); + } + + @Override + public void doDeactivate() { + super.doDeactivate(); + + model.getTxId().removeListener(txIdChangeListener); + txIdTextField.cleanup(); + statusProgressIndicator.setProgress(0); + } + + + @Override + protected void displayRequestCheckPayment() { + addDisputeInfoLabel(); + infoLabel.setText("You still have not done your payment!\n" + + "If the seller does not receive your payment until " + + model.getDateFromBlocks(openDisputeTimeInBlocks) + + " the trade will be investigated by the arbitrator." + ); + } + + @Override + protected void displayOpenForDisputeForm() { + addDisputeInfoLabel(); + infoLabel.setText("You have not completed your payment!\n" + + "The max. period for the trade has elapsed (" + + model.getDateFromBlocks(openDisputeTimeInBlocks) + ")." + + "\nPlease contact now the arbitrator for opening a dispute."); + + addOpenDisputeButton(); + GridPane.setMargin(openDisputeButton, new Insets(0, 0, 0, 0)); + GridPane.setColumnIndex(openDisputeButton, 1); + } + + @Override + protected void disputeInProgress() { + super.disputeInProgress(); + + paymentStartedButton.setDisable(true); + } + + @Override + protected void addDisputeInfoLabel() { + if (infoLabel == null) { + // we replace tx id field as there is not enough space + gridPane.getChildren().removeAll(txConfirmationGroup, txIdTextField); + + infoTitledGroupBg = addTitledGroupBg(gridPane, 0, 1, "Information"); + infoLabel = addMultilineLabel(gridPane, 0, Layout.FIRST_ROW_DISTANCE); + infoLabel.setStyle(" -fx-text-fill: -bs-error-red;"); + // grid does not auto update layout correctly + infoLabel.setMinHeight(70); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // UI Handlers + /////////////////////////////////////////////////////////////////////////////////////////// + + private void onPaymentStarted(ActionEvent actionEvent) { + log.debug("onPaymentStarted"); + paymentStartedButton.setDisable(true); + + statusProgressIndicator.setVisible(true); + statusProgressIndicator.setProgress(-1); + statusLabel.setText("Sending message to trading partner..."); + + model.fiatPaymentStarted(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Build view + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void buildGridEntries() { + txConfirmationGroup = addTitledGroupBg(gridPane, gridRow, 1, "Blockchain confirmation"); + txIdTextField = addLabelTxIdTextField(gridPane, gridRow, "Deposit transaction ID:", Layout.FIRST_ROW_DISTANCE).second; + + TitledGroupBg accountTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 1, "Payments details", Layout.GROUP_DISTANCE); + addLabelTextFieldWithCopyIcon(gridPane, gridRow, "Amount to transfer:", model.getFiatAmount(), + Layout.FIRST_ROW_AND_GROUP_DISTANCE); + PaymentAccountContractData paymentAccountContractData = model.dataModel.getSellersPaymentAccountContractData(); + + String paymentMethodName = paymentAccountContractData.getPaymentMethodName(); + switch (paymentMethodName) { + case PaymentMethod.OK_PAY_ID: + gridRow = OKPayForm.addFormForBuyer(gridPane, gridRow, paymentAccountContractData); + break; + case PaymentMethod.PERFECT_MONEY_ID: + gridRow = PerfectMoneyForm.addFormForBuyer(gridPane, gridRow, paymentAccountContractData); + break; + case PaymentMethod.SEPA_ID: + gridRow = SepaForm.addFormForBuyer(gridPane, gridRow, paymentAccountContractData); + break; + case PaymentMethod.SWISH_ID: + gridRow = SwishForm.addFormForBuyer(gridPane, gridRow, paymentAccountContractData); + break; + case PaymentMethod.ALI_PAY_ID: + gridRow = AliPayForm.addFormForBuyer(gridPane, gridRow, paymentAccountContractData); + break; + case PaymentMethod.BLOCK_CHAINS_ID: + gridRow = BlockChainForm.addFormForBuyer(gridPane, gridRow, paymentAccountContractData); + break; + default: + log.error("Not supported PaymentMethod: " + paymentMethodName); + } + + + addLabelTextFieldWithCopyIcon(gridPane, ++gridRow, "Reference:", model.getReference()); + + Tuple3 tuple3 = addButtonWithStatus(gridPane, ++gridRow, "Payment started"); + paymentStartedButton = tuple3.first; + paymentStartedButton.setOnAction(this::onPaymentStarted); + statusProgressIndicator = tuple3.second; + statusLabel = tuple3.third; + + GridPane.setRowSpan(accountTitledGroupBg, gridRow - 1); + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/main/portfolio/pendingtrades/steps/WaitPaymentReceivedView.java b/gui/src/main/java/io/bitsquare/gui/main/portfolio/pendingtrades/steps/WaitPaymentReceivedView.java new file mode 100644 index 0000000000..114b2f2285 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/portfolio/pendingtrades/steps/WaitPaymentReceivedView.java @@ -0,0 +1,109 @@ +/* + * 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.portfolio.pendingtrades.steps; + +import io.bitsquare.gui.components.TxIdTextField; +import io.bitsquare.gui.main.portfolio.pendingtrades.PendingTradesViewModel; +import io.bitsquare.gui.util.Layout; +import javafx.beans.value.ChangeListener; + +import static io.bitsquare.gui.util.FormBuilder.*; + +public class WaitPaymentReceivedView extends TradeStepDetailsView { + private final ChangeListener txIdChangeListener; + private TxIdTextField txIdTextField; + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, Initialisation + /////////////////////////////////////////////////////////////////////////////////////////// + + public WaitPaymentReceivedView(PendingTradesViewModel model) { + super(model); + + txIdChangeListener = (ov, oldValue, newValue) -> txIdTextField.setup(newValue); + } + + @Override + public void doActivate() { + super.doActivate(); + + model.getTxId().addListener(txIdChangeListener); + txIdTextField.setup(model.getTxId().get()); + } + + @Override + public void doDeactivate() { + super.doDeactivate(); + + model.getTxId().removeListener(txIdChangeListener); + txIdTextField.cleanup(); + } + + + @Override + protected void displayRequestCheckPayment() { + infoLabel.setStyle(" -fx-text-fill: -bs-error-red;"); + infoLabel.setText("The seller still has not confirmed your payment!\n" + + "Please check at your payment processor/bank/blockchain if the payment succeeded.\n" + + "If the seller has not confirmed the receipt of your payment until " + + model.getDateFromBlocks(openDisputeTimeInBlocks) + + " the trade will be investigated by the arbitrator."); + } + + @Override + protected void displayOpenForDisputeForm() { + infoLabel.setStyle(" -fx-text-fill: -bs-error-red;"); + infoLabel.setText("The seller has not confirmed your payment!\n" + + "The max. period for the trade has elapsed (" + + model.getDateFromBlocks(openDisputeTimeInBlocks) + + ") and you need to contact now the arbitrator to investigate the problem."); + + addOpenDisputeButton(); + } + + @Override + protected void disputeInProgress() { + super.disputeInProgress(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Setters + /////////////////////////////////////////////////////////////////////////////////////////// + + public void setInfoLabelText(String text) { + if (infoLabel != null) + infoLabel.setText(text); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Build view + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void buildGridEntries() { + addTitledGroupBg(gridPane, gridRow, 1, "Blockchain confirmation"); + txIdTextField = addLabelTxIdTextField(gridPane, gridRow, "Deposit transaction ID:", Layout.FIRST_ROW_DISTANCE).second; + + infoTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 1, "Information", Layout.GROUP_DISTANCE); + infoLabel = addMultilineLabel(gridPane, gridRow, Layout.FIRST_ROW_AND_GROUP_DISTANCE); + } +} + + diff --git a/gui/src/main/java/io/bitsquare/gui/main/portfolio/pendingtrades/steps/WaitPaymentStartedView.java b/gui/src/main/java/io/bitsquare/gui/main/portfolio/pendingtrades/steps/WaitPaymentStartedView.java new file mode 100644 index 0000000000..2f38794046 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/portfolio/pendingtrades/steps/WaitPaymentStartedView.java @@ -0,0 +1,49 @@ +/* + * 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.portfolio.pendingtrades.steps; + +import io.bitsquare.gui.main.portfolio.pendingtrades.PendingTradesViewModel; + +public class WaitPaymentStartedView extends WaitTxInBlockchainView { + public WaitPaymentStartedView(PendingTradesViewModel model) { + super(model); + } + + @Override + public void doActivate() { + super.doActivate(); + } + + @Override + protected void displayRequestCheckPayment() { + // does not make sense to warn here + } + + @Override + protected void displayOpenForDisputeForm() { + addDisputeInfoLabel(); + infoLabel.setText("The buyer has not started his payment!\n" + + "The max. period for the trade has elapsed (" + + model.getDateFromBlocks(openDisputeTimeInBlocks) + ")." + + "\nPlease contact the arbitrator for opening a dispute."); + + addOpenDisputeButton(); + } +} + + diff --git a/gui/src/main/java/io/bitsquare/gui/popups/ContractPopup.java b/gui/src/main/java/io/bitsquare/gui/popups/ContractPopup.java new file mode 100644 index 0000000000..34ca084477 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/popups/ContractPopup.java @@ -0,0 +1,161 @@ +/* + * 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.popups; + +import io.bitsquare.arbitration.Dispute; +import io.bitsquare.gui.util.BSFormatter; +import io.bitsquare.gui.util.Layout; +import io.bitsquare.locale.BSResources; +import io.bitsquare.locale.CountryUtil; +import io.bitsquare.payment.BlockChainAccountContractData; +import io.bitsquare.payment.PaymentAccountContractData; +import io.bitsquare.trade.Contract; +import io.bitsquare.trade.offer.Offer; +import javafx.geometry.Insets; +import javafx.scene.control.Button; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import org.bitcoinj.core.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import java.util.Optional; + +import static io.bitsquare.gui.util.FormBuilder.*; + +public class ContractPopup extends Popup { + protected static final Logger log = LoggerFactory.getLogger(ContractPopup.class); + + private final BSFormatter formatter; + private Dispute dispute; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public ContractPopup(BSFormatter formatter) { + this.formatter = formatter; + } + + public ContractPopup show(Dispute dispute) { + this.dispute = dispute; + + rowIndex = -1; + width = 850; + createGridPane(); + addContent(); + createPopup(); + return this; + } + + public ContractPopup onClose(Runnable closeHandler) { + this.closeHandlerOptional = Optional.of(closeHandler); + return this; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void createGridPane() { + super.createGridPane(); + gridPane.setPadding(new Insets(35, 40, 30, 40)); + gridPane.setStyle("-fx-background-color: -bs-content-bg-grey;" + + "-fx-background-radius: 5 5 5 5;" + + "-fx-effect: dropshadow(gaussian, #999, 10, 0, 0, 0);" + + "-fx-background-insets: 10;" + ); + } + + private void addContent() { + Contract contract = dispute.getContract(); + Offer offer = contract.offer; + + int rows = 16; + if (dispute.getDepositTxSerialized() != null) + rows++; + if (dispute.getPayoutTxSerialized() != null) + rows++; + if (offer.getAcceptedCountryCodes() != null) + rows++; + + boolean isPaymentIdAvailable = false; + PaymentAccountContractData sellerPaymentAccountContractData = contract.getSellerPaymentAccountContractData(); + if (sellerPaymentAccountContractData instanceof BlockChainAccountContractData && + ((BlockChainAccountContractData) sellerPaymentAccountContractData).getPaymentId() != null) { + rows++; + } + addTitledGroupBg(gridPane, ++rowIndex, rows, "Contract"); + addLabelTextFieldWithCopyIcon(gridPane, rowIndex, "Offer ID:", offer.getId(), + Layout.FIRST_ROW_DISTANCE).second.setMouseTransparent(false); + addLabelTextField(gridPane, ++rowIndex, "Offer date:", formatter.formatDateTime(offer.getDate())); + addLabelTextField(gridPane, ++rowIndex, "Trade date:", formatter.formatDateTime(dispute.getTradeDate())); + String direction = offer.getDirection() == Offer.Direction.BUY ? "Offerer as buyer / Taker as seller" : "Offerer as seller / Taker as buyer"; + addLabelTextField(gridPane, ++rowIndex, "Trade type:", direction); + addLabelTextField(gridPane, ++rowIndex, "Price:", formatter.formatFiat(offer.getPrice()) + " " + offer.getCurrencyCode()); + addLabelTextField(gridPane, ++rowIndex, "Trade amount:", formatter.formatCoinWithCode(contract.getTradeAmount())); + addLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, "Buyer bitcoin address:", + contract.getBuyerPayoutAddressString()).second.setMouseTransparent(false); + addLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, "Seller bitcoin address:", + contract.getSellerPayoutAddressString()).second.setMouseTransparent(false); + addLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, "Contract hash:", + Utils.HEX.encode(dispute.getContractHash())).second.setMouseTransparent(false); + addLabelTextField(gridPane, ++rowIndex, "Buyer address:", contract.getBuyerAddress().getFullAddress()); + addLabelTextField(gridPane, ++rowIndex, "Seller address:", contract.getSellerAddress().getFullAddress()); + addLabelTextField(gridPane, ++rowIndex, "Selected arbitrator:", contract.arbitratorAddress.getFullAddress()); + addLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, "Buyer payment details:", + BSResources.get(contract.getBuyerPaymentAccountContractData().getPaymentDetails())).second.setMouseTransparent(false); + addLabelTextField(gridPane, ++rowIndex, "Seller payment details:", + BSResources.get(sellerPaymentAccountContractData.getPaymentDetails())).second.setMouseTransparent(false); + if (isPaymentIdAvailable) + addLabelTextField(gridPane, ++rowIndex, "Seller payment ID:", + ((BlockChainAccountContractData) sellerPaymentAccountContractData).getPaymentId()); + + if (offer.getAcceptedCountryCodes() != null) { + String countries; + Tooltip tooltip = null; + if (CountryUtil.containsAllSepaEuroCountries(offer.getAcceptedCountryCodes())) { + countries = "All Euro countries"; + } else { + countries = CountryUtil.getCodesString(offer.getAcceptedCountryCodes()); + tooltip = new Tooltip(CountryUtil.getNamesByCodesString(offer.getAcceptedCountryCodes())); + } + TextField acceptedCountries = addLabelTextField(gridPane, ++rowIndex, "Accepted taker countries:", countries).second; + if (tooltip != null) acceptedCountries.setTooltip(new Tooltip()); + } + //addLabelTextField(gridPane, ++rowIndex, "Buyer Bitsquare account ID:", contract.getBuyerAccountId()).second.setMouseTransparent(false); + //addLabelTextField(gridPane, ++rowIndex, "Seller Bitsquare account ID:", contract.getSellerAccountId()).second.setMouseTransparent(false); + addLabelTxIdTextField(gridPane, ++rowIndex, "Create offer fee transaction ID:", offer.getOfferFeePaymentTxID()); + addLabelTxIdTextField(gridPane, ++rowIndex, "Take offer fee transaction ID:", contract.takeOfferFeeTxID); + if (dispute.getDepositTxSerialized() != null) + addLabelTxIdTextField(gridPane, ++rowIndex, "Deposit transaction ID:", dispute.getDepositTxId()); + if (dispute.getPayoutTxSerialized() != null) + addLabelTxIdTextField(gridPane, ++rowIndex, "Payout transaction ID:", dispute.getPayoutTxId()); + + Button cancelButton = addButtonAfterGroup(gridPane, ++rowIndex, "Close"); + cancelButton.requestFocus(); + cancelButton.setOnAction(e -> { + closeHandlerOptional.ifPresent(closeHandler -> closeHandler.run()); + hide(); + }); + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/popups/DisplayAlertMessagePopup.java b/gui/src/main/java/io/bitsquare/gui/popups/DisplayAlertMessagePopup.java new file mode 100644 index 0000000000..9c4d502669 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/popups/DisplayAlertMessagePopup.java @@ -0,0 +1,93 @@ +/* + * 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.popups; + +import io.bitsquare.alert.Alert; +import javafx.geometry.Insets; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.GridPane; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.bitsquare.gui.util.FormBuilder.addMultilineLabel; + +public class DisplayAlertMessagePopup extends Popup { + private static final Logger log = LoggerFactory.getLogger(DisplayAlertMessagePopup.class); + private Label msgLabel; + private Alert alert; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public API + /////////////////////////////////////////////////////////////////////////////////////////// + + public DisplayAlertMessagePopup() { + } + + public DisplayAlertMessagePopup show() { + if (headLine == null) + headLine = "Global alert message!"; + + width = 700; + createGridPane(); + addHeadLine(); + addContent(); + createPopup(); + + headLineLabel.setStyle("-fx-text-fill: -bs-error-red; -fx-font-weight: bold; -fx-font-size: 18;"); + + return this; + } + + public DisplayAlertMessagePopup alertMessage(Alert alert) { + this.alert = alert; + return this; + } + + public DisplayAlertMessagePopup onClose(Runnable closeHandler) { + this.closeHandlerOptional = Optional.of(closeHandler); + return this; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + private void addContent() { + checkNotNull(alert, "alertMessage must not be null"); + msgLabel = addMultilineLabel(gridPane, ++rowIndex, alert.message, 10); + msgLabel.setStyle("-fx-text-fill: -bs-error-red;"); + + closeButton = new Button("Cancel"); + 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)); + } + + +} diff --git a/gui/src/main/java/io/bitsquare/gui/popups/EmptyWalletPopup.java b/gui/src/main/java/io/bitsquare/gui/popups/EmptyWalletPopup.java new file mode 100644 index 0000000000..5166af74dc --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/popups/EmptyWalletPopup.java @@ -0,0 +1,159 @@ +/* + * 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.popups; + +import io.bitsquare.btc.FeePolicy; +import io.bitsquare.btc.WalletService; +import io.bitsquare.common.util.Tuple2; +import io.bitsquare.gui.components.InputTextField; +import io.bitsquare.gui.util.BSFormatter; +import io.bitsquare.gui.util.Transitions; +import javafx.geometry.Insets; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import org.bitcoinj.core.AddressFormatException; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; +import org.reactfx.util.FxTimer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.spongycastle.crypto.params.KeyParameter; + +import javax.inject.Inject; +import java.time.Duration; +import java.util.Optional; + +import static io.bitsquare.gui.util.FormBuilder.*; + +public class EmptyWalletPopup extends Popup { + private static final Logger log = LoggerFactory.getLogger(EmptyWalletPopup.class); + private final WalletService walletService; + private final WalletPasswordPopup walletPasswordPopup; + private final BSFormatter formatter; + private Button emptyWalletButton; + private InputTextField addressInputTextField; + private TextField addressTextField; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public EmptyWalletPopup(WalletService walletService, WalletPasswordPopup walletPasswordPopup, BSFormatter formatter) { + this.walletService = walletService; + this.walletPasswordPopup = walletPasswordPopup; + this.formatter = formatter; + } + + public EmptyWalletPopup show() { + if (headLine == null) + headLine = "Empty wallet"; + + width = 700; + createGridPane(); + addHeadLine(); + addContent(); + createPopup(); + return this; + } + + public EmptyWalletPopup onClose(Runnable closeHandler) { + this.closeHandlerOptional = Optional.of(closeHandler); + return this; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + private void addContent() { + addMultilineLabel(gridPane, ++rowIndex, + "Please use that only in emergency case if you cannot access your fund from the UI.\n" + + "Before you use this tool, you should backup your data directory. After you have successfully transferred your wallet balance, remove" + + " the db directory inside the data directory to start with a newly created and consistent data structure.\n" + + "Please make a bug report on Github so that we can investigate what was causing the problem.", + 10); + + Coin totalBalance = walletService.getAvailableBalance(); + boolean isBalanceSufficient = totalBalance.compareTo(FeePolicy.TX_FEE) >= 0; + addressTextField = addLabelTextField(gridPane, ++rowIndex, "Your available wallet balance:", + formatter.formatCoinWithCode(totalBalance), 10).second; + Tuple2 tuple = addLabelInputTextField(gridPane, ++rowIndex, "Your destination address:"); + addressInputTextField = tuple.second; + + emptyWalletButton = new Button("Empty wallet"); + emptyWalletButton.setDefaultButton(isBalanceSufficient); + emptyWalletButton.setDisable(!isBalanceSufficient && addressInputTextField.getText().length() > 0); + emptyWalletButton.setOnAction(e -> { + if (addressInputTextField.getText().length() > 0 && isBalanceSufficient) { + if (walletService.getWallet().isEncrypted()) { + walletPasswordPopup + .onClose(() -> blurAgain()) + .onAesKey(aesKey -> doEmptyWallet(aesKey)) + .show(); + } else { + doEmptyWallet(null); + } + } + }); + + closeButton = new Button("Cancel"); + closeButton.setOnAction(e -> { + hide(); + closeHandlerOptional.ifPresent(closeHandler -> closeHandler.run()); + }); + + HBox hBox = new HBox(); + hBox.setSpacing(10); + GridPane.setRowIndex(hBox, ++rowIndex); + GridPane.setColumnIndex(hBox, 1); + hBox.getChildren().addAll(emptyWalletButton, closeButton); + gridPane.getChildren().add(hBox); + GridPane.setMargin(hBox, new Insets(10, 0, 0, 0)); + } + + private void doEmptyWallet(KeyParameter aesKey) { + emptyWalletButton.setDisable(true); + try { + walletService.emptyWallet(addressInputTextField.getText(), + aesKey, + () -> { + closeButton.setText("Close"); + addressTextField.setText(formatter.formatCoinWithCode(walletService.getAvailableBalance())); + emptyWalletButton.setDisable(true); + log.debug("wallet empty successful"); + FxTimer.runLater(Duration.ofMillis(Transitions.DEFAULT_DURATION), () -> new Popup() + .information("The balance of your wallet was successfully transferred.") + .onClose(() -> blurAgain()).show()); + }, + (errorMessage) -> { + emptyWalletButton.setDisable(false); + log.debug("wallet empty failed " + errorMessage); + }); + } catch (InsufficientMoneyException | AddressFormatException e1) { + e1.printStackTrace(); + log.error(e1.getMessage()); + emptyWalletButton.setDisable(false); + } + } + +} diff --git a/gui/src/main/java/io/bitsquare/gui/popups/FirstTimePopup.java b/gui/src/main/java/io/bitsquare/gui/popups/FirstTimePopup.java new file mode 100644 index 0000000000..f257779ad0 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/popups/FirstTimePopup.java @@ -0,0 +1,84 @@ +/* + * 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.popups; + +import io.bitsquare.user.Preferences; +import javafx.geometry.Insets; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.layout.GridPane; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; + +import static io.bitsquare.gui.util.FormBuilder.addCheckBox; + +public class FirstTimePopup extends WebViewPopup { + private static final Logger log = LoggerFactory.getLogger(FirstTimePopup.class); + private Preferences preferences; + private String id; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public API + /////////////////////////////////////////////////////////////////////////////////////////// + + public FirstTimePopup(Preferences preferences) { + this.preferences = preferences; + } + + @Override + public FirstTimePopup url(String url) { + super.url(url); + return this; + } + + public FirstTimePopup onClose(Runnable closeHandler) { + this.closeHandlerOptional = Optional.of(closeHandler); + return this; + } + + public FirstTimePopup id(String id) { + this.id = id; + return this; + } + + @Override + protected void addHtmlContent() { + super.addHtmlContent(); + + CheckBox dontShowAgain = addCheckBox(gridPane, ++rowIndex, "Don't show again", 10); + dontShowAgain.setOnAction(e -> { + if (dontShowAgain.isSelected()) + preferences.dontShowAgain(id); + }); + 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)); + } + + +} diff --git a/gui/src/main/java/io/bitsquare/gui/popups/OfferDetailsPopup.java b/gui/src/main/java/io/bitsquare/gui/popups/OfferDetailsPopup.java new file mode 100644 index 0000000000..76f2febd0b --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/popups/OfferDetailsPopup.java @@ -0,0 +1,205 @@ +/* + * 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.popups; + +import io.bitsquare.common.util.Tuple2; +import io.bitsquare.gui.Navigation; +import io.bitsquare.gui.main.MainView; +import io.bitsquare.gui.main.account.AccountView; +import io.bitsquare.gui.main.account.content.arbitratorselection.ArbitratorSelectionView; +import io.bitsquare.gui.main.account.settings.AccountSettingsView; +import io.bitsquare.gui.util.BSFormatter; +import io.bitsquare.gui.util.Layout; +import io.bitsquare.locale.BSResources; +import io.bitsquare.locale.CountryUtil; +import io.bitsquare.trade.offer.Offer; +import io.bitsquare.user.Preferences; +import io.bitsquare.user.User; +import javafx.geometry.Insets; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import java.util.Optional; +import java.util.function.Consumer; + +import static io.bitsquare.gui.util.FormBuilder.*; + +public class OfferDetailsPopup extends Popup { + protected static final Logger log = LoggerFactory.getLogger(OfferDetailsPopup.class); + + private final BSFormatter formatter; + private final Preferences preferences; + private User user; + private final Navigation navigation; + private Offer offer; + private Optional> placeOfferHandlerOptional = Optional.empty(); + private Optional takeOfferHandlerOptional = Optional.empty(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public OfferDetailsPopup(BSFormatter formatter, Preferences preferences, User user, Navigation navigation) { + this.formatter = formatter; + this.preferences = preferences; + this.user = user; + this.navigation = navigation; + } + + public OfferDetailsPopup show(Offer offer) { + this.offer = offer; + + rowIndex = -1; + width = 850; + createGridPane(); + addContent(); + createPopup(); + return this; + } + + public OfferDetailsPopup onPlaceOffer(Consumer placeOfferHandler) { + this.placeOfferHandlerOptional = Optional.of(placeOfferHandler); + return this; + } + + public OfferDetailsPopup onTakeOffer(Runnable takeOfferHandler) { + this.takeOfferHandlerOptional = Optional.of(takeOfferHandler); + return this; + } + + public OfferDetailsPopup onClose(Runnable closeHandler) { + this.closeHandlerOptional = Optional.of(closeHandler); + return this; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void createGridPane() { + super.createGridPane(); + gridPane.setPadding(new Insets(35, 40, 30, 40)); + gridPane.setStyle("-fx-background-color: -bs-content-bg-grey;" + + "-fx-background-radius: 5 5 5 5;" + + "-fx-effect: dropshadow(gaussian, #999, 10, 0, 0, 0);" + + "-fx-background-insets: 10;" + ); + } + + private void addContent() { + int rows = 9; + if (offer.getPaymentMethodCountryCode() != null) + rows++; + if (offer.getOfferFeePaymentTxID() != null) + rows++; + if (offer.getAcceptedCountryCodes() != null) + rows++; + if (placeOfferHandlerOptional.isPresent()) + rows -= 2; + + addTitledGroupBg(gridPane, ++rowIndex, rows, "Offer details"); + addLabelTextField(gridPane, rowIndex, "Offer ID:", offer.getId(), Layout.FIRST_ROW_DISTANCE); + addLabelTextField(gridPane, ++rowIndex, "Creation date:", formatter.formatDateTime(offer.getDate())); + addLabelTextField(gridPane, ++rowIndex, "Offer direction:", Offer.Direction.BUY.name()); + addLabelTextField(gridPane, ++rowIndex, "Price:", formatter.formatFiat(offer.getPrice()) + " " + offer.getCurrencyCode()); + addLabelTextField(gridPane, ++rowIndex, "Amount:", formatter.formatCoinWithCode(offer.getAmount())); + addLabelTextField(gridPane, ++rowIndex, "Min. amount:", formatter.formatCoinWithCode(offer.getMinAmount())); + addLabelTextField(gridPane, ++rowIndex, "Payment method:", BSResources.get(offer.getPaymentMethod().getId())); + if (offer.getPaymentMethodCountryCode() != null) + addLabelTextField(gridPane, ++rowIndex, "Offerers country of bank:", offer.getPaymentMethodCountryCode()); + if (offer.getAcceptedCountryCodes() != null) { + String countries; + Tooltip tooltip = null; + if (CountryUtil.containsAllSepaEuroCountries(offer.getAcceptedCountryCodes())) { + countries = "All Euro countries"; + } else { + countries = CountryUtil.getCodesString(offer.getAcceptedCountryCodes()); + tooltip = new Tooltip(CountryUtil.getNamesByCodesString(offer.getAcceptedCountryCodes())); + } + TextField acceptedCountries = addLabelTextField(gridPane, ++rowIndex, "Accepted taker countries:", countries).second; + if (tooltip != null) acceptedCountries.setTooltip(new Tooltip()); + } + addLabelTextField(gridPane, ++rowIndex, "Accepted arbitrators:", formatter.arbitratorAddressesToString(offer.getArbitratorAddresses())); + if (offer.getOfferFeePaymentTxID() != null) + addLabelTxIdTextField(gridPane, ++rowIndex, "Create offer fee transaction ID:", offer.getOfferFeePaymentTxID()); + + if (placeOfferHandlerOptional.isPresent()) { + Tuple2 tuple = add2ButtonsAfterGroup(gridPane, ++rowIndex, "Confirm place offer", "Cancel"); + Button placeButton = tuple.first; + placeButton.setOnAction(e -> { + if (user.getAcceptedArbitrators().size() > 0) { + placeOfferHandlerOptional.get().accept(offer); + } else { + new Popup().warning("You have no arbitrator selected.\n" + + "Please select at least one arbitrator.").show(); + + navigation.navigateTo(MainView.class, AccountView.class, AccountSettingsView.class, ArbitratorSelectionView.class); + } + hide(); + }); + + Button cancelButton = tuple.second; + cancelButton.setOnAction(e -> { + closeHandlerOptional.ifPresent(closeHandler -> closeHandler.run()); + hide(); + }); + + CheckBox checkBox = addCheckBox(gridPane, ++rowIndex, "Don't show again", 5); + checkBox.setSelected(!preferences.getShowPlaceOfferConfirmation()); + checkBox.setOnAction(e -> preferences.setShowPlaceOfferConfirmation(!checkBox.isSelected())); + } else if (takeOfferHandlerOptional.isPresent()) { + Tuple2 tuple = add2ButtonsAfterGroup(gridPane, ++rowIndex, "Confirm take offer", "Cancel"); + Button placeButton = tuple.first; + placeButton.setOnAction(e -> { + if (user.getAcceptedArbitrators().size() > 0) { + takeOfferHandlerOptional.get().run(); + } else { + new Popup().warning("You have no arbitrator selected.\n" + + "Please select at least one arbitrator.").show(); + + navigation.navigateTo(MainView.class, AccountView.class, AccountSettingsView.class, ArbitratorSelectionView.class); + } + hide(); + }); + + Button cancelButton = tuple.second; + cancelButton.setOnAction(e -> { + closeHandlerOptional.ifPresent(closeHandler -> closeHandler.run()); + hide(); + }); + + CheckBox checkBox = addCheckBox(gridPane, ++rowIndex, "Don't show again", 5); + checkBox.setSelected(!preferences.getShowTakeOfferConfirmation()); + checkBox.setOnAction(e -> preferences.setShowTakeOfferConfirmation(!checkBox.isSelected())); + } else { + Button cancelButton = addButtonAfterGroup(gridPane, ++rowIndex, "Close"); + cancelButton.setOnAction(e -> { + closeHandlerOptional.ifPresent(closeHandler -> closeHandler.run()); + hide(); + }); + } + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/popups/OpenEmergencyTicketPopup.java b/gui/src/main/java/io/bitsquare/gui/popups/OpenEmergencyTicketPopup.java new file mode 100644 index 0000000000..1b99ac38bd --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/popups/OpenEmergencyTicketPopup.java @@ -0,0 +1,100 @@ +/* + * 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.popups; + +import io.bitsquare.common.handlers.ResultHandler; +import javafx.geometry.Insets; +import javafx.scene.control.Button; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; + +import static io.bitsquare.gui.util.FormBuilder.addMultilineLabel; + +public class OpenEmergencyTicketPopup extends Popup { + private static final Logger log = LoggerFactory.getLogger(OpenEmergencyTicketPopup.class); + private Button openTicketButton; + private ResultHandler openTicketHandler; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public API + /////////////////////////////////////////////////////////////////////////////////////////// + + public OpenEmergencyTicketPopup() { + } + + public OpenEmergencyTicketPopup show() { + if (headLine == null) + headLine = "Open support ticket"; + + width = 700; + createGridPane(); + addHeadLine(); + addContent(); + createPopup(); + return this; + } + + public OpenEmergencyTicketPopup onOpenTicket(ResultHandler openTicketHandler) { + this.openTicketHandler = openTicketHandler; + return this; + } + + public OpenEmergencyTicketPopup onClose(Runnable closeHandler) { + this.closeHandlerOptional = Optional.of(closeHandler); + return this; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + private void addContent() { + addMultilineLabel(gridPane, ++rowIndex, + "Please use that only in emergency case if you don't get displayed a support or dispute screen in the UI.\n" + + "When you open a ticket the trade will be interrupted and handled by the arbitrator.", + 10); + + + openTicketButton = new Button("Open support ticket"); + openTicketButton.setOnAction(e -> { + openTicketHandler.handleResult(); + hide(); + }); + + closeButton = new Button("Cancel"); + closeButton.setOnAction(e -> { + hide(); + closeHandlerOptional.ifPresent(closeHandler -> closeHandler.run()); + }); + + HBox hBox = new HBox(); + hBox.setSpacing(10); + GridPane.setRowIndex(hBox, ++rowIndex); + GridPane.setColumnIndex(hBox, 1); + hBox.getChildren().addAll(openTicketButton, closeButton); + gridPane.getChildren().add(hBox); + GridPane.setMargin(hBox, new Insets(10, 0, 0, 0)); + } + + +} diff --git a/gui/src/main/java/io/bitsquare/gui/popups/Popup.java b/gui/src/main/java/io/bitsquare/gui/popups/Popup.java new file mode 100644 index 0000000000..2a7f1a47c7 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/popups/Popup.java @@ -0,0 +1,345 @@ +/* + * 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.popups; + +import io.bitsquare.common.util.Utilities; +import io.bitsquare.gui.main.MainView; +import io.bitsquare.gui.util.Transitions; +import io.bitsquare.locale.BSResources; +import javafx.geometry.HPos; +import javafx.geometry.Insets; +import javafx.geometry.Orientation; +import javafx.geometry.Point2D; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.control.Separator; +import javafx.scene.layout.*; +import javafx.scene.paint.Color; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import javafx.stage.Window; +import org.reactfx.util.FxTimer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Duration; +import java.util.Optional; + +public class Popup { + protected final Logger log = LoggerFactory.getLogger(this.getClass()); + + protected final static double DEFAULT_WIDTH = 500; + protected int rowIndex = -1; + protected String headLine; + protected String message; + protected String closeButtonText; + protected String actionButtonText; + protected double width = DEFAULT_WIDTH; + protected Pane owner; + protected GridPane gridPane; + protected Button closeButton; + protected Optional closeHandlerOptional = Optional.empty(); + protected Optional actionHandlerOptional = Optional.empty(); + protected Stage stage; + private boolean showReportErrorButtons; + protected Label messageLabel; + protected String truncatedMessage; + private ProgressIndicator progressIndicator; + private boolean showProgressIndicator; + private Button actionButton; + protected Label headLineLabel; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public API + /////////////////////////////////////////////////////////////////////////////////////////// + + public Popup() { + } + + public Popup show() { + createGridPane(); + addHeadLine(); + + if (showProgressIndicator) + addProgressIndicator(); + + addMessage(); + if (showReportErrorButtons) + addReportErrorButtons(); + + addCloseButton(); + createPopup(); + return this; + } + + public void hide() { + MainView.removeBlur(); + stage.hide(); + } + + public Popup onClose(Runnable closeHandler) { + this.closeHandlerOptional = Optional.of(closeHandler); + return this; + } + + public Popup onAction(Runnable actionHandler) { + this.actionHandlerOptional = Optional.of(actionHandler); + return this; + } + + public Popup headLine(String headLine) { + this.headLine = headLine; + return this; + } + + public Popup information(String message) { + this.headLine = "Information"; + this.message = message; + setTruncatedMessage(); + return this; + } + + public Popup warning(String message) { + this.headLine = "Warning"; + this.message = message; + setTruncatedMessage(); + return this; + } + + public Popup error(String message) { + showReportErrorButtons(); + this.headLine = "Error"; + this.message = message; + setTruncatedMessage(); + return this; + } + + public Popup showReportErrorButtons() { + this.showReportErrorButtons = true; + return this; + } + + public Popup message(String message) { + this.message = message; + setTruncatedMessage(); + return this; + } + + public Popup closeButtonText(String closeButtonText) { + this.closeButtonText = closeButtonText; + return this; + } + + public Popup actionButtonText(String actionButtonText) { + this.actionButtonText = actionButtonText; + return this; + } + + public Popup width(double width) { + this.width = width; + return this; + } + + public Popup showProgressIndicator() { + this.showProgressIndicator = true; + return this; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + protected void createGridPane() { + gridPane = new GridPane(); + gridPane.setHgap(5); + gridPane.setVgap(5); + gridPane.setPadding(new Insets(30, 30, 30, 30)); + gridPane.setPrefWidth(width); + gridPane.setStyle("-fx-background-color: white;" + + "-fx-background-radius: 5 5 5 5;" + + "-fx-effect: dropshadow(gaussian, #999, 10, 0, 0, 0);" + + "-fx-background-insets: 10;" + ); + + ColumnConstraints columnConstraints1 = new ColumnConstraints(); + columnConstraints1.setHalignment(HPos.RIGHT); + columnConstraints1.setHgrow(Priority.SOMETIMES); + ColumnConstraints columnConstraints2 = new ColumnConstraints(); + columnConstraints2.setHgrow(Priority.ALWAYS); + gridPane.getColumnConstraints().addAll(columnConstraints1, columnConstraints2); + } + + protected void blurAgain() { + FxTimer.runLater(Duration.ofMillis(Transitions.DEFAULT_DURATION), () -> MainView.blurLight()); + } + + protected void createPopup() { + if (owner == null) + owner = MainView.getBaseApplicationContainer(); + + stage = new Stage(); + Scene scene = new Scene(gridPane); + scene.getStylesheets().setAll(owner.getScene().getStylesheets()); + scene.setFill(Color.TRANSPARENT); + stage.setScene(scene); + stage.initModality(Modality.APPLICATION_MODAL); + stage.initStyle(StageStyle.TRANSPARENT); + stage.initOwner(owner.getScene().getWindow()); + stage.show(); + + centerPopup(); + + MainView.blurLight(); + } + + protected void centerPopup() { + Window window = owner.getScene().getWindow(); + double titleBarHeight = window.getHeight() - owner.getScene().getHeight(); + Point2D point = owner.localToScene(0, 0); + stage.setX(Math.round(window.getX() + point.getX() + (owner.getWidth() - stage.getWidth()) / 2)); + stage.setY(Math.round(window.getY() + titleBarHeight + point.getY() + (owner.getHeight() - stage.getHeight()) / 2)); + } + + protected void addHeadLine() { + if (headLine != null) { + headLineLabel = new Label(BSResources.get(headLine)); + headLineLabel.setMouseTransparent(true); + headLineLabel.setStyle("-fx-font-size: 16; -fx-text-fill: #333;"); + GridPane.setHalignment(headLineLabel, HPos.LEFT); + GridPane.setRowIndex(headLineLabel, ++rowIndex); + GridPane.setColumnSpan(headLineLabel, 2); + + Separator separator = new Separator(); + separator.setMouseTransparent(true); + separator.setOrientation(Orientation.HORIZONTAL); + separator.setStyle("-fx-background: #ccc;"); + GridPane.setHalignment(separator, HPos.CENTER); + GridPane.setRowIndex(separator, ++rowIndex); + GridPane.setColumnSpan(separator, 2); + + gridPane.getChildren().addAll(headLineLabel, separator); + } + } + + protected void addMessage() { + if (message != null) { + messageLabel = new Label(truncatedMessage); + messageLabel.setMouseTransparent(true); + messageLabel.setWrapText(true); + GridPane.setHalignment(messageLabel, HPos.LEFT); + GridPane.setHgrow(messageLabel, Priority.ALWAYS); + GridPane.setMargin(messageLabel, new Insets(3, 0, 0, 0)); + GridPane.setRowIndex(messageLabel, ++rowIndex); + GridPane.setColumnIndex(messageLabel, 0); + GridPane.setColumnSpan(messageLabel, 2); + gridPane.getChildren().add(messageLabel); + } + } + + private void addReportErrorButtons() { + messageLabel.setText(truncatedMessage + + "\n\nTo help us to improve the software please report the bug at our issue tracker at Github or send it by email to the developers.\n" + + "The error message will be copied to clipboard when you click the below buttons.\n" + + "It will make debugging easier if you can attach the bitsquare.log file which you can find in the application directory."); + + Button githubButton = new Button("Report to Github issue tracker"); + GridPane.setMargin(githubButton, new Insets(20, 0, 0, 0)); + GridPane.setHalignment(githubButton, HPos.RIGHT); + GridPane.setRowIndex(githubButton, ++rowIndex); + GridPane.setColumnIndex(githubButton, 1); + gridPane.getChildren().add(githubButton); + + githubButton.setOnAction(event -> { + Utilities.copyToClipboard(message); + Utilities.openWebPage("https://github.com/bitsquare/bitsquare/issues"); + }); + + Button mailButton = new Button("Report by email"); + GridPane.setHalignment(mailButton, HPos.RIGHT); + GridPane.setRowIndex(mailButton, ++rowIndex); + GridPane.setColumnIndex(mailButton, 1); + gridPane.getChildren().add(mailButton); + mailButton.setOnAction(event -> { + Utilities.copyToClipboard(message); + Utilities.openMail("manfred@bitsquare.io", + "Error report", + "Error message:\n" + message); + }); + } + + protected void addProgressIndicator() { + progressIndicator = new ProgressIndicator(-1); + progressIndicator.setMaxSize(36, 36); + progressIndicator.setMouseTransparent(true); + progressIndicator.setPadding(new Insets(0, 0, 20, 0)); + GridPane.setHalignment(progressIndicator, HPos.CENTER); + GridPane.setRowIndex(progressIndicator, ++rowIndex); + GridPane.setColumnSpan(progressIndicator, 2); + gridPane.getChildren().add(progressIndicator); + } + + protected void addCloseButton() { + closeButton = new Button(closeButtonText == null ? "Close" : closeButtonText); + closeButton.setOnAction(event -> { + hide(); + closeHandlerOptional.ifPresent(closeHandler -> closeHandler.run()); + }); + + if (actionHandlerOptional.isPresent()) { + actionButton = new Button(actionButtonText == null ? "Ok" : actionButtonText); + actionButton.setDefaultButton(true); + actionButton.requestFocus(); + actionButton.setOnAction(event -> { + hide(); + actionHandlerOptional.ifPresent(actionHandler -> actionHandler.run()); + }); + + Pane spacer = new Pane(); + HBox hBox = new HBox(); + hBox.setSpacing(10); + hBox.getChildren().addAll(spacer, closeButton, actionButton); + HBox.setHgrow(spacer, Priority.ALWAYS); + + GridPane.setHalignment(hBox, HPos.RIGHT); + GridPane.setRowIndex(hBox, ++rowIndex); + GridPane.setColumnSpan(hBox, 2); + GridPane.setMargin(hBox, new Insets(20, 0, 0, 0)); + gridPane.getChildren().add(hBox); + } else { + closeButton.setDefaultButton(true); + GridPane.setHalignment(closeButton, HPos.RIGHT); + if (!showReportErrorButtons) + GridPane.setMargin(closeButton, new Insets(20, 0, 0, 0)); + GridPane.setRowIndex(closeButton, ++rowIndex); + GridPane.setColumnIndex(closeButton, 1); + gridPane.getChildren().add(closeButton); + } + + } + + protected void setTruncatedMessage() { + if (message.length() > 500) + truncatedMessage = message.substring(0, 500) + "..."; + else + truncatedMessage = message; + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/popups/SendAlertMessagePopup.java b/gui/src/main/java/io/bitsquare/gui/popups/SendAlertMessagePopup.java new file mode 100644 index 0000000000..fd3a5cab77 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/popups/SendAlertMessagePopup.java @@ -0,0 +1,136 @@ +/* + * 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.popups; + +import io.bitsquare.alert.Alert; +import io.bitsquare.app.BitsquareApp; +import io.bitsquare.gui.components.InputTextField; +import javafx.geometry.Insets; +import javafx.scene.control.Button; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; + +import static io.bitsquare.gui.util.FormBuilder.addLabelInputTextField; + +public class SendAlertMessagePopup extends Popup { + private static final Logger log = LoggerFactory.getLogger(SendAlertMessagePopup.class); + private Button openTicketButton; + private SendAlertMessageHandler sendAlertMessageHandler; + private RemoveAlertMessageHandler removeAlertMessageHandler; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Interface + /////////////////////////////////////////////////////////////////////////////////////////// + public interface SendAlertMessageHandler { + boolean handle(Alert alert, String privKey); + } + + public interface RemoveAlertMessageHandler { + boolean handle(String privKey); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public API + /////////////////////////////////////////////////////////////////////////////////////////// + + public SendAlertMessagePopup() { + } + + public SendAlertMessagePopup show() { + if (headLine == null) + headLine = "Send alert message"; + + width = 700; + createGridPane(); + addHeadLine(); + addContent(); + createPopup(); + return this; + } + + public SendAlertMessagePopup onAddAlertMessage(SendAlertMessageHandler sendAlertMessageHandler) { + this.sendAlertMessageHandler = sendAlertMessageHandler; + return this; + } + + public SendAlertMessagePopup onRemoveAlertMessage(RemoveAlertMessageHandler removeAlertMessageHandler) { + this.removeAlertMessageHandler = removeAlertMessageHandler; + return this; + } + + public SendAlertMessagePopup onClose(Runnable closeHandler) { + this.closeHandlerOptional = Optional.of(closeHandler); + return this; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + private void addContent() { + InputTextField keyInputTextField = addLabelInputTextField(gridPane, ++rowIndex, "Alert private key:", 10).second; + InputTextField alertMessageInputTextField = addLabelInputTextField(gridPane, ++rowIndex, "Alert message:").second; + + if (BitsquareApp.DEV_MODE) { + keyInputTextField.setText("2e41038992f89eef2e4634ff3586e342c68ad9a5a7ffafee866781687f77a9b1"); + alertMessageInputTextField.setText("m1"); + } + + openTicketButton = new Button("Send alert message"); + openTicketButton.setOnAction(e -> { + if (alertMessageInputTextField.getText().length() > 0 && keyInputTextField.getText().length() > 0) { + if (sendAlertMessageHandler.handle(new Alert(alertMessageInputTextField.getText()), keyInputTextField.getText())) + hide(); + else + new Popup().warning("The key you entered was not correct.").width(300).onClose(() -> blurAgain()).show(); + } + }); + + Button removeAlertMessageButton = new Button("Remove alert message"); + removeAlertMessageButton.setOnAction(e -> { + if (keyInputTextField.getText().length() > 0) { + if (removeAlertMessageHandler.handle(keyInputTextField.getText())) + hide(); + else + new Popup().warning("The key you entered was not correct.").width(300).onClose(() -> blurAgain()).show(); + } + }); + + closeButton = new Button("Cancel"); + closeButton.setOnAction(e -> { + hide(); + closeHandlerOptional.ifPresent(closeHandler -> closeHandler.run()); + }); + + HBox hBox = new HBox(); + hBox.setSpacing(10); + GridPane.setRowIndex(hBox, ++rowIndex); + GridPane.setColumnIndex(hBox, 1); + hBox.getChildren().addAll(openTicketButton, removeAlertMessageButton, closeButton); + gridPane.getChildren().add(hBox); + GridPane.setMargin(hBox, new Insets(10, 0, 0, 0)); + } + + +} diff --git a/gui/src/main/java/io/bitsquare/gui/popups/TacPopup.java b/gui/src/main/java/io/bitsquare/gui/popups/TacPopup.java new file mode 100644 index 0000000000..419ac336c5 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/popups/TacPopup.java @@ -0,0 +1,57 @@ +/* + * 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.popups; + +import io.bitsquare.app.BitsquareApp; +import io.bitsquare.common.util.Tuple2; +import javafx.scene.control.Button; + +import java.util.Optional; + +import static io.bitsquare.gui.util.FormBuilder.add2ButtonsAfterGroup; + +public class TacPopup extends WebViewPopup { + + private Optional agreeHandlerOptional; + + public TacPopup onAgree(Runnable agreeHandler) { + this.agreeHandlerOptional = Optional.of(agreeHandler); + return this; + } + + @Override + public TacPopup url(String url) { + super.url(url); + return this; + } + + @Override + protected void addHtmlContent() { + super.addHtmlContent(); + + Tuple2 tuple = add2ButtonsAfterGroup(gridPane, ++rowIndex, "I agree", "Quit"); + Button agreeButton = tuple.first; + Button quitButton = tuple.second; + + agreeButton.setOnAction(e -> { + agreeHandlerOptional.ifPresent(agreeHandler -> agreeHandler.run()); + hide(); + }); + quitButton.setOnAction(e -> BitsquareApp.shutDownHandler.run()); + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/popups/TradeDetailsPopup.java b/gui/src/main/java/io/bitsquare/gui/popups/TradeDetailsPopup.java new file mode 100644 index 0000000000..211d32e5ff --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/popups/TradeDetailsPopup.java @@ -0,0 +1,196 @@ +/* + * 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.popups; + +import io.bitsquare.gui.util.BSFormatter; +import io.bitsquare.gui.util.Layout; +import io.bitsquare.locale.BSResources; +import io.bitsquare.payment.PaymentAccountContractData; +import io.bitsquare.trade.Contract; +import io.bitsquare.trade.Trade; +import io.bitsquare.trade.offer.Offer; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.geometry.Insets; +import javafx.scene.control.Button; +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import java.util.Optional; + +import static io.bitsquare.gui.util.FormBuilder.*; + +public class TradeDetailsPopup extends Popup { + protected static final Logger log = LoggerFactory.getLogger(TradeDetailsPopup.class); + + private final BSFormatter formatter; + private Trade trade; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public TradeDetailsPopup(BSFormatter formatter) { + this.formatter = formatter; + } + + public TradeDetailsPopup show(Trade trade) { + this.trade = trade; + + rowIndex = -1; + width = 850; + createGridPane(); + addContent(); + createPopup(); + return this; + } + + public TradeDetailsPopup onClose(Runnable closeHandler) { + this.closeHandlerOptional = Optional.of(closeHandler); + return this; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void createGridPane() { + super.createGridPane(); + gridPane.setPadding(new Insets(35, 40, 30, 40)); + gridPane.setStyle("-fx-background-color: -bs-content-bg-grey;" + + "-fx-background-radius: 5 5 5 5;" + + "-fx-effect: dropshadow(gaussian, #999, 10, 0, 0, 0);" + + "-fx-background-insets: 10;" + ); + } + + private void addContent() { + Offer offer = trade.getOffer(); + Contract contract = trade.getContract(); + + int rows = 7; + PaymentAccountContractData buyerPaymentAccountContractData = null; + PaymentAccountContractData sellerPaymentAccountContractData = null; + + if (offer.getAcceptedCountryCodes() != null) + rows++; + + if (contract != null) { + buyerPaymentAccountContractData = contract.getBuyerPaymentAccountContractData(); + sellerPaymentAccountContractData = contract.getSellerPaymentAccountContractData(); + if (buyerPaymentAccountContractData != null) + rows++; + + if (sellerPaymentAccountContractData != null) + rows++; + + if (buyerPaymentAccountContractData == null && sellerPaymentAccountContractData == null) + rows++; + + if (contract.takeOfferFeeTxID != null) + rows++; + } + + if (trade.getDepositTx() != null) + rows++; + if (trade.getPayoutTx() != null) + rows++; + if (trade.errorMessageProperty().get() != null) + rows += 2; + + addTitledGroupBg(gridPane, ++rowIndex, rows, "Trade details"); + addLabelTextField(gridPane, rowIndex, "Trade ID:", trade.getId(), Layout.FIRST_ROW_DISTANCE); + addLabelTextField(gridPane, ++rowIndex, "Trade date:", formatter.formatDateTime(trade.getDate())); + String direction = offer.getDirection() == Offer.Direction.BUY ? "Offerer as buyer / Taker as seller" : "Offerer as seller / Taker as buyer"; + addLabelTextField(gridPane, ++rowIndex, "Offer direction:", direction); + addLabelTextField(gridPane, ++rowIndex, "Price:", formatter.formatFiat(offer.getPrice()) + " " + offer.getCurrencyCode()); + addLabelTextField(gridPane, ++rowIndex, "Trade amount:", formatter.formatCoinWithCode(trade.getTradeAmount())); + addLabelTextField(gridPane, ++rowIndex, "Selected arbitrator:", formatter.arbitratorAddressToShortAddress(trade.getArbitratorAddress())); + + if (contract != null) { + if (buyerPaymentAccountContractData != null) + addLabelTextField(gridPane, ++rowIndex, "Buyer payment details:", BSResources.get(buyerPaymentAccountContractData.getPaymentDetails())); + + if (sellerPaymentAccountContractData != null) + addLabelTextField(gridPane, ++rowIndex, "Seller payment details:", BSResources.get(sellerPaymentAccountContractData.getPaymentDetails())); + + if (buyerPaymentAccountContractData == null && sellerPaymentAccountContractData == null) + addLabelTextField(gridPane, ++rowIndex, "Payment method:", BSResources.get(contract.getPaymentMethodName())); + } + + addLabelTxIdTextField(gridPane, ++rowIndex, "Create offer fee transaction ID:", offer.getOfferFeePaymentTxID()); + if (contract != null && contract.takeOfferFeeTxID != null) + addLabelTxIdTextField(gridPane, ++rowIndex, "Take offer fee transaction ID:", contract.takeOfferFeeTxID); + + if (trade.getDepositTx() != null) + addLabelTxIdTextField(gridPane, ++rowIndex, "Deposit transaction ID:", trade.getDepositTx().getHashAsString()); + if (trade.getPayoutTx() != null) + addLabelTxIdTextField(gridPane, ++rowIndex, "Payout transaction ID:", trade.getPayoutTx().getHashAsString()); + + if (trade.errorMessageProperty().get() != null) { + TextArea textArea = addLabelTextArea(gridPane, ++rowIndex, "Error message:", "").second; + textArea.setText(trade.errorMessageProperty().get()); + textArea.setEditable(false); + + IntegerProperty count = new SimpleIntegerProperty(20); + int rowHeight = 10; + textArea.prefHeightProperty().bindBidirectional(count); + textArea.scrollTopProperty().addListener((ov, old, newVal) -> { + if (newVal.intValue() > rowHeight) + count.setValue(count.get() + newVal.intValue() + 10); + }); + textArea.setScrollTop(30); + + TextField state = addLabelTextField(gridPane, ++rowIndex, "Trade state:").second; + state.setText(trade.getState().getPhase().name()); + //TODO better msg display + /* switch (trade.getTradeState().getPhase()) { + case PREPARATION: + state.setText("Take offer fee is already paid."); + break; + case TAKER_FEE_PAID: + state.setText("Take offer fee is already paid."); + break; + case DEPOSIT_PAID: + case FIAT_SENT: + case FIAT_RECEIVED: + state.setText("Deposit is already paid."); + break; + case PAYOUT_PAID: + break; + case WITHDRAWN: + break; + case DISPUTE: + break; + }*/ + } + + Button cancelButton = addButtonAfterGroup(gridPane, ++rowIndex, "Close"); + cancelButton.requestFocus(); + cancelButton.setOnAction(e -> { + closeHandlerOptional.ifPresent(closeHandler -> closeHandler.run()); + hide(); + }); + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/popups/WalletPasswordPopup.java b/gui/src/main/java/io/bitsquare/gui/popups/WalletPasswordPopup.java new file mode 100644 index 0000000000..79ff4563a4 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/popups/WalletPasswordPopup.java @@ -0,0 +1,159 @@ +/* + * 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.popups; + +import io.bitsquare.btc.WalletService; +import io.bitsquare.crypto.ScryptUtil; +import io.bitsquare.gui.components.PasswordTextField; +import io.bitsquare.gui.util.Transitions; +import io.bitsquare.gui.util.validation.PasswordValidator; +import javafx.geometry.Insets; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import org.bitcoinj.core.Wallet; +import org.bitcoinj.crypto.KeyCrypterScrypt; +import org.reactfx.util.FxTimer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.spongycastle.crypto.params.KeyParameter; + +import javax.inject.Inject; +import java.time.Duration; +import java.util.Optional; + +public class WalletPasswordPopup extends Popup { + private static final Logger log = LoggerFactory.getLogger(WalletPasswordPopup.class); + private final WalletService walletService; + private Button unlockButton; + private AesKeyHandler aesKeyHandler; + private PasswordTextField passwordTextField; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Interface + /////////////////////////////////////////////////////////////////////////////////////////// + + public interface AesKeyHandler { + void onAesKey(KeyParameter aesKey); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public WalletPasswordPopup(WalletService walletService) { + this.walletService = walletService; + } + + public WalletPasswordPopup show() { + if (gridPane != null) { + rowIndex = -1; + gridPane.getChildren().clear(); + } + + if (headLine == null) + headLine = "Enter password to unlock"; + + createGridPane(); + addHeadLine(); + addInputFields(); + addButtons(); + createPopup(); + + return this; + } + + public WalletPasswordPopup onClose(Runnable closeHandler) { + this.closeHandlerOptional = Optional.of(closeHandler); + return this; + } + + public WalletPasswordPopup onAesKey(AesKeyHandler aesKeyHandler) { + this.aesKeyHandler = aesKeyHandler; + return this; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + private void addInputFields() { + Label label = new Label("Enter password:"); + label.setWrapText(true); + GridPane.setMargin(label, new Insets(3, 0, 0, 0)); + GridPane.setRowIndex(label, ++rowIndex); + + passwordTextField = new PasswordTextField(); + GridPane.setMargin(passwordTextField, new Insets(3, 0, 0, 0)); + GridPane.setRowIndex(passwordTextField, rowIndex); + GridPane.setColumnIndex(passwordTextField, 1); + PasswordValidator passwordValidator = new PasswordValidator(); + passwordTextField.textProperty().addListener((observable, oldValue, newValue) -> { + unlockButton.setDisable(!passwordValidator.validate(newValue).isValid); + }); + gridPane.getChildren().addAll(label, passwordTextField); + } + + private void addButtons() { + unlockButton = new Button("Unlock"); + unlockButton.setDefaultButton(true); + unlockButton.setDisable(true); + unlockButton.setOnAction(e -> checkPassword()); + + Button cancelButton = new Button("Cancel"); + cancelButton.setOnAction(event -> { + hide(); + closeHandlerOptional.ifPresent(closeHandler -> closeHandler.run()); + }); + + HBox hBox = new HBox(); + hBox.setSpacing(10); + GridPane.setRowIndex(hBox, ++rowIndex); + GridPane.setColumnIndex(hBox, 1); + hBox.getChildren().addAll(unlockButton, cancelButton); + gridPane.getChildren().add(hBox); + } + + private void checkPassword() { + String password = passwordTextField.getText(); + Wallet wallet = walletService.getWallet(); + KeyCrypterScrypt keyCrypterScrypt = (KeyCrypterScrypt) wallet.getKeyCrypter(); + if (keyCrypterScrypt != null) { + ScryptUtil.deriveKeyWithScrypt(keyCrypterScrypt, password, aesKey -> { + if (wallet.checkAESKey(aesKey)) { + if (aesKeyHandler != null) + aesKeyHandler.onAesKey(aesKey); + + hide(); + } else { + FxTimer.runLater(Duration.ofMillis(Transitions.DEFAULT_DURATION), () -> new Popup() + .headLine("Wrong password") + .message("Please try entering your password again, carefully checking for typos or spelling errors.") + .onClose(() -> blurAgain()).show()); + } + }); + } else { + log.error("wallet.getKeyCrypter() is null, than must not happen."); + } + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/popups/WebViewPopup.java b/gui/src/main/java/io/bitsquare/gui/popups/WebViewPopup.java new file mode 100644 index 0000000000..58c163f69b --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/popups/WebViewPopup.java @@ -0,0 +1,82 @@ +/* + * 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.popups; + +import io.bitsquare.common.util.Utilities; +import javafx.geometry.HPos; +import javafx.geometry.Insets; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; +import javafx.scene.web.WebView; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class WebViewPopup extends Popup { + private static final Logger log = LoggerFactory.getLogger(WebViewPopup.class); + + protected WebView webView; + protected String url; + + public static String getLocalUrl(String htmlFile) { + return WebViewPopup.class.getResource("/html/" + htmlFile).toExternalForm(); + } + + public WebViewPopup() { + } + + @Override + public WebViewPopup show() { + width = 700; + + webView = new WebView(); + webView.setPrefHeight(0); + + // open links with http and _blank in default web browser instead of webView + Utilities.setupWebViewPopupHandler(webView.getEngine()); + + webView.getEngine().documentProperty().addListener((observable, oldValue, newValue) -> { + String heightInPx = webView.getEngine() + .executeScript("window.getComputedStyle(document.body, null).getPropertyValue('height')").toString(); + double height = Double.valueOf(heightInPx.replace("px", "")); + webView.setPrefHeight(height); + stage.setMinHeight(height + gridPane.getHeight()); + centerPopup(); + }); + + createGridPane(); + addHtmlContent(); + createPopup(); + return this; + } + + public WebViewPopup url(String url) { + this.url = url; + return this; + } + + protected void addHtmlContent() { + webView.getEngine().load(url); + GridPane.setHalignment(webView, HPos.LEFT); + GridPane.setHgrow(webView, Priority.ALWAYS); + GridPane.setMargin(webView, new Insets(3, 0, 0, 0)); + GridPane.setRowIndex(webView, ++rowIndex); + GridPane.setColumnIndex(webView, 0); + GridPane.setColumnSpan(webView, 2); + gridPane.getChildren().add(webView); + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/util/FormBuilder.java b/gui/src/main/java/io/bitsquare/gui/util/FormBuilder.java new file mode 100644 index 0000000000..ae1141ed67 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/util/FormBuilder.java @@ -0,0 +1,706 @@ +/* + * 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.util; + +import io.bitsquare.common.util.Tuple2; +import io.bitsquare.common.util.Tuple3; +import io.bitsquare.gui.components.*; +import javafx.geometry.HPos; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.geometry.VPos; +import javafx.scene.Node; +import javafx.scene.control.*; +import javafx.scene.layout.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +public class FormBuilder { + private static final Logger log = LoggerFactory.getLogger(FormBuilder.class); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // GridPane + /////////////////////////////////////////////////////////////////////////////////////////// + + public static GridPane addGridPane(Pane parent) { + GridPane gridPane = new GridPane(); + AnchorPane.setLeftAnchor(gridPane, 10d); + AnchorPane.setRightAnchor(gridPane, 10d); + AnchorPane.setTopAnchor(gridPane, 10d); + AnchorPane.setBottomAnchor(gridPane, 10d); + gridPane.setHgap(Layout.GRID_GAP); + gridPane.setVgap(Layout.GRID_GAP); + ColumnConstraints columnConstraints1 = new ColumnConstraints(); + columnConstraints1.setHalignment(HPos.RIGHT); + columnConstraints1.setHgrow(Priority.SOMETIMES); + + ColumnConstraints columnConstraints2 = new ColumnConstraints(); + columnConstraints2.setHgrow(Priority.ALWAYS); + + gridPane.getColumnConstraints().addAll(columnConstraints1, columnConstraints2); + + parent.getChildren().add(gridPane); + return gridPane; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // TitledGroupBg + /////////////////////////////////////////////////////////////////////////////////////////// + + public static TitledGroupBg addTitledGroupBg(GridPane gridPane, int rowIndex, int rowSpan, String title) { + return addTitledGroupBg(gridPane, rowIndex, rowSpan, title, 0); + } + + public static TitledGroupBg addTitledGroupBg(GridPane gridPane, int rowIndex, int rowSpan, String title, double top) { + TitledGroupBg titledGroupBg = new TitledGroupBg(); + titledGroupBg.setText(title); + titledGroupBg.prefWidthProperty().bind(gridPane.widthProperty()); + GridPane.setRowIndex(titledGroupBg, rowIndex); + GridPane.setRowSpan(titledGroupBg, rowSpan); + GridPane.setColumnSpan(titledGroupBg, 2); + GridPane.setMargin(titledGroupBg, new Insets(top, -10, -10, -10)); + gridPane.getChildren().add(titledGroupBg); + return titledGroupBg; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Label + /////////////////////////////////////////////////////////////////////////////////////////// + + public static Label addLabel(GridPane gridPane, int rowIndex, String title) { + return addLabel(gridPane, rowIndex, title, 0); + } + + public static Label addLabel(GridPane gridPane, int rowIndex, String title, double top) { + Label label = new Label(title); + GridPane.setRowIndex(label, rowIndex); + GridPane.setMargin(label, new Insets(top, 0, 0, 0)); + gridPane.getChildren().add(label); + return label; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Multiline Label + /////////////////////////////////////////////////////////////////////////////////////////// + + public static Label addMultilineLabel(GridPane gridPane, int rowIndex) { + return addMultilineLabel(gridPane, rowIndex, 0); + } + + public static Label addMultilineLabel(GridPane gridPane, int rowIndex, String text) { + return addMultilineLabel(gridPane, rowIndex, text, 0); + } + + public static Label addMultilineLabel(GridPane gridPane, int rowIndex, double top) { + return addMultilineLabel(gridPane, rowIndex, "", top); + } + + public static Label addMultilineLabel(GridPane gridPane, int rowIndex, String text, double top) { + Label label = new Label(text); + label.setWrapText(true); + GridPane.setHalignment(label, HPos.LEFT); + GridPane.setRowIndex(label, rowIndex); + GridPane.setColumnSpan(label, 2); + GridPane.setMargin(label, new Insets(top, 0, 0, 0)); + gridPane.getChildren().add(label); + return label; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Label + TextField + /////////////////////////////////////////////////////////////////////////////////////////// + + public static Tuple2 addLabelTextField(GridPane gridPane, int rowIndex, String title) { + return addLabelTextField(gridPane, rowIndex, title, "", 0); + } + + public static Tuple2 addLabelTextField(GridPane gridPane, int rowIndex, String title, String value) { + return addLabelTextField(gridPane, rowIndex, title, value, 0); + } + + public static Tuple2 addLabelTextField(GridPane gridPane, int rowIndex, String title, String value, double top) { + Label label = addLabel(gridPane, rowIndex, title, top); + + TextField textField = new TextField(value); + textField.setEditable(false); + textField.setMouseTransparent(true); + textField.setFocusTraversable(false); + GridPane.setRowIndex(textField, rowIndex); + GridPane.setColumnIndex(textField, 1); + GridPane.setMargin(textField, new Insets(top, 0, 0, 0)); + gridPane.getChildren().add(textField); + + return new Tuple2<>(label, textField); + } + /////////////////////////////////////////////////////////////////////////////////////////// + // Label + TextArea + /////////////////////////////////////////////////////////////////////////////////////////// + + public static Tuple2 addLabelTextArea(GridPane gridPane, int rowIndex, String title, String prompt) { + return addLabelTextArea(gridPane, rowIndex, title, prompt, 0); + } + + public static Tuple2 addLabelTextArea(GridPane gridPane, int rowIndex, String title, String prompt, double top) { + Label label = addLabel(gridPane, rowIndex, title, 0); + GridPane.setMargin(label, new Insets(top, 0, 0, 0)); + GridPane.setValignment(label, VPos.TOP); + + TextArea textArea = new TextArea(); + textArea.setPromptText(prompt); + textArea.setWrapText(true); + GridPane.setRowIndex(textArea, rowIndex); + GridPane.setColumnIndex(textArea, 1); + GridPane.setMargin(textArea, new Insets(top, 0, 0, 0)); + gridPane.getChildren().add(textArea); + + return new Tuple2<>(label, textArea); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Label + DatePicker + /////////////////////////////////////////////////////////////////////////////////////////// + + public static Tuple2 addLabelDatePicker(GridPane gridPane, int rowIndex, String title) { + Label label = addLabel(gridPane, rowIndex, title, 0); + + DatePicker datePicker = new DatePicker(); + GridPane.setRowIndex(datePicker, rowIndex); + GridPane.setColumnIndex(datePicker, 1); + gridPane.getChildren().add(datePicker); + + return new Tuple2<>(label, datePicker); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Label + TxIdTextField + /////////////////////////////////////////////////////////////////////////////////////////// + + public static Tuple2 addLabelTxIdTextField(GridPane gridPane, int rowIndex, String title, String value) { + return addLabelTxIdTextField(gridPane, rowIndex, title, value, 0); + } + + public static Tuple2 addLabelTxIdTextField(GridPane gridPane, int rowIndex, String title, String value, double top) { + Label label = addLabel(gridPane, rowIndex, title, top); + + TxIdTextField txTextField = new TxIdTextField(); + txTextField.setup(value); + GridPane.setRowIndex(txTextField, rowIndex); + GridPane.setColumnIndex(txTextField, 1); + gridPane.getChildren().add(txTextField); + + return new Tuple2<>(label, txTextField); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Label + InputTextField + /////////////////////////////////////////////////////////////////////////////////////////// + + public static Tuple2 addLabelInputTextField(GridPane gridPane, int rowIndex, String title) { + return addLabelInputTextField(gridPane, rowIndex, title, 0); + } + + public static Tuple2 addLabelInputTextField(GridPane gridPane, int rowIndex, String title, double top) { + Label label = addLabel(gridPane, rowIndex, title, top); + + InputTextField inputTextField = new InputTextField(); + GridPane.setRowIndex(inputTextField, rowIndex); + GridPane.setColumnIndex(inputTextField, 1); + GridPane.setMargin(inputTextField, new Insets(top, 0, 0, 0)); + gridPane.getChildren().add(inputTextField); + + return new Tuple2<>(label, inputTextField); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Label + PasswordField + /////////////////////////////////////////////////////////////////////////////////////////// + + public static Tuple2 addLabelPasswordTextField(GridPane gridPane, int rowIndex, String title) { + return addLabelPasswordTextField(gridPane, rowIndex, title, 0); + } + + public static Tuple2 addLabelPasswordTextField(GridPane gridPane, int rowIndex, String title, double top) { + Label label = addLabel(gridPane, rowIndex, title, top); + + PasswordTextField passwordField = new PasswordTextField(); + GridPane.setRowIndex(passwordField, rowIndex); + GridPane.setColumnIndex(passwordField, 1); + GridPane.setMargin(passwordField, new Insets(top, 0, 0, 0)); + gridPane.getChildren().add(passwordField); + + return new Tuple2<>(label, passwordField); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Label + InputTextField + CheckBox + /////////////////////////////////////////////////////////////////////////////////////////// + + public static Tuple3 addLabelInputTextFieldCheckBox(GridPane gridPane, int rowIndex, String title, String checkBoxTitle) { + Label label = addLabel(gridPane, rowIndex, title, 0); + + InputTextField inputTextField = new InputTextField(); + CheckBox checkBox = new CheckBox(checkBoxTitle); + checkBox.setPadding(new Insets(6, 0, 0, 0)); + + HBox hBox = new HBox(); + hBox.setSpacing(10); + hBox.getChildren().addAll(inputTextField, checkBox); + GridPane.setRowIndex(hBox, rowIndex); + GridPane.setColumnIndex(hBox, 1); + gridPane.getChildren().add(hBox); + + return new Tuple3<>(label, inputTextField, checkBox); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Button + CheckBox + /////////////////////////////////////////////////////////////////////////////////////////// + + public static Tuple2 addButtonCheckBox(GridPane gridPane, int rowIndex, String buttonTitle, String checkBoxTitle) { + return addButtonCheckBox(gridPane, rowIndex, buttonTitle, checkBoxTitle, 0); + } + + public static Tuple2 addButtonCheckBox(GridPane gridPane, int rowIndex, String buttonTitle, String checkBoxTitle, double top) { + Button button = new Button(buttonTitle); + button.setDefaultButton(true); + CheckBox checkBox = new CheckBox(checkBoxTitle); + HBox.setMargin(checkBox, new Insets(6, 0, 0, 0)); + + HBox hBox = new HBox(); + hBox.setSpacing(20); + hBox.getChildren().addAll(button, checkBox); + GridPane.setRowIndex(hBox, rowIndex); + GridPane.setColumnIndex(hBox, 1); + hBox.setPadding(new Insets(top, 0, 0, 0)); + gridPane.getChildren().add(hBox); + + return new Tuple2<>(button, checkBox); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // CheckBox + /////////////////////////////////////////////////////////////////////////////////////////// + + public static CheckBox addCheckBox(GridPane gridPane, int rowIndex, String checkBoxTitle) { + return addCheckBox(gridPane, rowIndex, checkBoxTitle, 0); + } + + public static CheckBox addCheckBox(GridPane gridPane, int rowIndex, String checkBoxTitle, double top) { + CheckBox checkBox = new CheckBox(checkBoxTitle); + GridPane.setMargin(checkBox, new Insets(top, 0, 0, 0)); + GridPane.setRowIndex(checkBox, rowIndex); + GridPane.setColumnIndex(checkBox, 1); + gridPane.getChildren().add(checkBox); + return checkBox; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // RadioButton + /////////////////////////////////////////////////////////////////////////////////////////// + + public static RadioButton addRadioButton(GridPane gridPane, int rowIndex, String title) { + RadioButton radioButton = new RadioButton(title); + GridPane.setRowIndex(radioButton, rowIndex); + GridPane.setColumnIndex(radioButton, 1); + gridPane.getChildren().add(radioButton); + return radioButton; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Label + RadioButton + /////////////////////////////////////////////////////////////////////////////////////////// + + public static Tuple2 addLabelRadioButton(GridPane gridPane, int rowIndex, ToggleGroup toggleGroup, String title, String + radioButtonTitle) { + Label label = addLabel(gridPane, rowIndex, title, 0); + + RadioButton radioButton = new RadioButton(radioButtonTitle); + radioButton.setToggleGroup(toggleGroup); + radioButton.setPadding(new Insets(6, 0, 0, 0)); + GridPane.setRowIndex(radioButton, rowIndex); + GridPane.setColumnIndex(radioButton, 1); + gridPane.getChildren().add(radioButton); + + return new Tuple2<>(label, radioButton); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Label + CheckBox + /////////////////////////////////////////////////////////////////////////////////////////// + + public static Tuple2 addLabelCheckBox(GridPane gridPane, int rowIndex, String title, String checkBoxTitle) { + Label label = addLabel(gridPane, rowIndex, title, -3); + + CheckBox checkBox = new CheckBox(checkBoxTitle); + GridPane.setRowIndex(checkBox, rowIndex); + GridPane.setColumnIndex(checkBox, 1); + gridPane.getChildren().add(checkBox); + + return new Tuple2<>(label, checkBox); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Label + ComboBox + /////////////////////////////////////////////////////////////////////////////////////////// + + public static Tuple2 addLabelComboBox(GridPane gridPane, int rowIndex) { + return addLabelComboBox(gridPane, rowIndex, null, 0); + } + + public static Tuple2 addLabelComboBox(GridPane gridPane, int rowIndex, String title) { + return addLabelComboBox(gridPane, rowIndex, title, 0); + } + + public static Tuple2 addLabelComboBox(GridPane gridPane, int rowIndex, String title, double top) { + Label label = null; + if (title != null) + label = addLabel(gridPane, rowIndex, title, top); + + ComboBox comboBox = new ComboBox(); + GridPane.setRowIndex(comboBox, rowIndex); + GridPane.setColumnIndex(comboBox, 1); + GridPane.setMargin(comboBox, new Insets(top, 0, 0, 0)); + gridPane.getChildren().add(comboBox); + + return new Tuple2<>(label, comboBox); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Label + ComboBox + ComboBox + /////////////////////////////////////////////////////////////////////////////////////////// + + public static Tuple3 addLabelComboBoxComboBox(GridPane gridPane, int rowIndex, String title) { + return addLabelComboBoxComboBox(gridPane, rowIndex, title, 0); + } + + public static Tuple3 addLabelComboBoxComboBox(GridPane gridPane, int rowIndex, String title, double top) { + Label label = addLabel(gridPane, rowIndex, title, top); + + HBox hBox = new HBox(); + hBox.setSpacing(10); + + ComboBox comboBox1 = new ComboBox(); + ComboBox comboBox2 = new ComboBox(); + hBox.getChildren().addAll(comboBox1, comboBox2); + + GridPane.setRowIndex(hBox, rowIndex); + GridPane.setColumnIndex(hBox, 1); + // GridPane.setMargin(hBox, new Insets(15, 0, 0, 0)); + gridPane.getChildren().add(hBox); + + return new Tuple3<>(label, comboBox1, comboBox2); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Label + ComboBox + Button + /////////////////////////////////////////////////////////////////////////////////////////// + + public static Tuple3 addLabelComboBoxButton(GridPane gridPane, + int rowIndex, + String title, + String buttonTitle) { + return addLabelComboBoxButton(gridPane, rowIndex, title, buttonTitle, 0); + } + + public static Tuple3 addLabelComboBoxButton(GridPane gridPane, + int rowIndex, + String title, + String buttonTitle, + double top) { + Label label = addLabel(gridPane, rowIndex, title, top); + + HBox hBox = new HBox(); + hBox.setSpacing(10); + + Button button = new Button(buttonTitle); + button.setDefaultButton(true); + + ComboBox comboBox = new ComboBox(); + + hBox.getChildren().addAll(comboBox, button); + + GridPane.setRowIndex(hBox, rowIndex); + GridPane.setColumnIndex(hBox, 1); + GridPane.setMargin(hBox, new Insets(15, 0, 0, 0)); + gridPane.getChildren().add(hBox); + + return new Tuple3<>(label, comboBox, button); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Label + TxIdTextField + /////////////////////////////////////////////////////////////////////////////////////////// + + public static Tuple2 addLabelTxIdTextField(GridPane gridPane, int rowIndex, String title) { + return addLabelTxIdTextField(gridPane, rowIndex, title, 0); + } + + public static Tuple2 addLabelTxIdTextField(GridPane gridPane, int rowIndex, String title, double top) { + Label label = addLabel(gridPane, rowIndex, title, top); + + TxIdTextField txIdTextField = new TxIdTextField(); + GridPane.setRowIndex(txIdTextField, rowIndex); + GridPane.setColumnIndex(txIdTextField, 1); + GridPane.setMargin(txIdTextField, new Insets(top, 0, 0, 0)); + gridPane.getChildren().add(txIdTextField); + + return new Tuple2<>(label, txIdTextField); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Label + TextFieldWithCopyIcon + /////////////////////////////////////////////////////////////////////////////////////////// + + public static Tuple2 addLabelTextFieldWithCopyIcon(GridPane gridPane, int rowIndex, String title, String value) { + return addLabelTextFieldWithCopyIcon(gridPane, rowIndex, title, value, 0); + } + + public static Tuple2 addLabelTextFieldWithCopyIcon(GridPane gridPane, int rowIndex, String title, String value, double top) { + Label label = addLabel(gridPane, rowIndex, title, top); + + TextFieldWithCopyIcon textFieldWithCopyIcon = new TextFieldWithCopyIcon(); + textFieldWithCopyIcon.setText(value); + GridPane.setRowIndex(textFieldWithCopyIcon, rowIndex); + GridPane.setColumnIndex(textFieldWithCopyIcon, 1); + GridPane.setMargin(textFieldWithCopyIcon, new Insets(top, 0, 0, 0)); + gridPane.getChildren().add(textFieldWithCopyIcon); + + return new Tuple2<>(label, textFieldWithCopyIcon); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Label + AddressTextField + /////////////////////////////////////////////////////////////////////////////////////////// + + public static Tuple2 addLabelAddressTextField(GridPane gridPane, int rowIndex, String title) { + Label label = addLabel(gridPane, rowIndex, title, 0); + + AddressTextField addressTextField = new AddressTextField(); + GridPane.setRowIndex(addressTextField, rowIndex); + GridPane.setColumnIndex(addressTextField, 1); + gridPane.getChildren().add(addressTextField); + + return new Tuple2<>(label, addressTextField); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Label + BalanceTextField + /////////////////////////////////////////////////////////////////////////////////////////// + + + public static Tuple2 addLabelBalanceTextField(GridPane gridPane, int rowIndex, String title) { + Label label = addLabel(gridPane, rowIndex, title, 0); + + BalanceTextField balanceTextField = new BalanceTextField(); + GridPane.setRowIndex(balanceTextField, rowIndex); + GridPane.setColumnIndex(balanceTextField, 1); + gridPane.getChildren().add(balanceTextField); + + return new Tuple2<>(label, balanceTextField); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Button + /////////////////////////////////////////////////////////////////////////////////////////// + + public static Button addButton(GridPane gridPane, int rowIndex, String title) { + Button button = new Button(title); + button.setDefaultButton(true); + GridPane.setRowIndex(button, rowIndex); + GridPane.setColumnIndex(button, 1); + gridPane.getChildren().add(button); + return button; + } + + public static Button addButtonAfterGroup(GridPane gridPane, + int rowIndex, + String title) { + Button button = new Button(title); + button.setDefaultButton(true); + GridPane.setRowIndex(button, rowIndex); + GridPane.setColumnIndex(button, 1); + GridPane.setMargin(button, new Insets(15, 0, 0, 0)); + gridPane.getChildren().add(button); + return button; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Button + Button + /////////////////////////////////////////////////////////////////////////////////////////// + + public static Tuple2 add2ButtonsAfterGroup(GridPane gridPane, + int rowIndex, + String title1, + String title2) { + return add2ButtonsAfterGroup(gridPane, rowIndex, title1, title2, 15); + } + + public static Tuple2 add2ButtonsAfterGroup(GridPane gridPane, + int rowIndex, + String title1, + String title2, + double top) { + HBox hBox = new HBox(); + hBox.setSpacing(10); + Button button1 = new Button(title1); + button1.setDefaultButton(true); + Button button2 = new Button(title2); + hBox.getChildren().addAll(button1, button2); + GridPane.setRowIndex(hBox, rowIndex); + GridPane.setColumnIndex(hBox, 1); + GridPane.setMargin(hBox, new Insets(top, 10, 0, 0)); + gridPane.getChildren().add(hBox); + return new Tuple2<>(button1, button2); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Button + ProgressIndicator + Label + /////////////////////////////////////////////////////////////////////////////////////////// + + public static Tuple3 addButtonWithStatus(GridPane gridPane, + int rowIndex, + String buttonTitle) { + return addButtonWithStatus(gridPane, rowIndex, buttonTitle, 15); + } + + public static Tuple3 addButtonWithStatus(GridPane gridPane, + int rowIndex, + String buttonTitle, + double top) { + HBox hBox = new HBox(); + hBox.setSpacing(10); + Button button = new Button(buttonTitle); + button.setDefaultButton(true); + + ProgressIndicator progressIndicator = new ProgressIndicator(0); + progressIndicator.setPrefHeight(24); + progressIndicator.setPrefWidth(24); + progressIndicator.setVisible(false); + + Label label = new Label(); + label.setPadding(new Insets(5, 0, 0, 0)); + + hBox.getChildren().addAll(button, progressIndicator, label); + + GridPane.setRowIndex(hBox, rowIndex); + GridPane.setColumnIndex(hBox, 1); + GridPane.setMargin(hBox, new Insets(top, 0, 0, 0)); + gridPane.getChildren().add(hBox); + + return new Tuple3<>(button, progressIndicator, label); + } + + public static void removeRowFromGridPane(GridPane gridPane, int gridRow) { + removeRowsFromGridPane(gridPane, gridRow, gridRow); + } + + public static void removeRowsFromGridPane(GridPane gridPane, int fromGridRow, int toGridRow) { + List nodes = new CopyOnWriteArrayList<>(gridPane.getChildren()); + nodes.stream() + .filter(e -> GridPane.getRowIndex(e) >= fromGridRow && GridPane.getRowIndex(e) <= toGridRow) + .forEach(e -> gridPane.getChildren().remove(e)); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Trade: HBox, InputTextField, Label + /////////////////////////////////////////////////////////////////////////////////////////// + + public static Tuple3 getValueCurrencyBox() { + return getValueCurrencyBox(""); + } + + public static Tuple3 getValueCurrencyBox(String promptText) { + InputTextField input = new InputTextField(); + input.setPrefWidth(170); + input.setAlignment(Pos.CENTER_RIGHT); + input.setId("text-input-with-currency-text-field"); + input.setPromptText(promptText); + + Label currency = new Label(); + currency.setId("currency-info-label"); + + HBox box = new HBox(); + box.getChildren().addAll(input, currency); + return new Tuple3<>(box, input, currency); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Trade: Label, VBox + /////////////////////////////////////////////////////////////////////////////////////////// + + public static Tuple2 getTradeInputBox(HBox amountValueBox) { + return getTradeInputBox(amountValueBox, ""); + } + + public static Tuple2 getTradeInputBox(HBox amountValueBox, String descriptionText) { + Label descriptionLabel = new Label(descriptionText); + descriptionLabel.setId("input-description-label"); + descriptionLabel.setPrefWidth(170); + + VBox box = new VBox(); + box.setSpacing(4); + box.getChildren().addAll(descriptionLabel, amountValueBox); + return new Tuple2<>(descriptionLabel, box); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Label + List + /////////////////////////////////////////////////////////////////////////////////////////// + + public static Tuple2 addLabelListView(GridPane gridPane, int rowIndex, String title) { + return addLabelListView(gridPane, rowIndex, title, 0); + } + + public static Tuple2 addLabelListView(GridPane gridPane, int rowIndex, String title, double top) { + Label label = addLabel(gridPane, rowIndex, title, top); + + ListView listView = new ListView(); + GridPane.setRowIndex(listView, rowIndex); + GridPane.setColumnIndex(listView, 1); + GridPane.setMargin(listView, new Insets(top, 0, 0, 0)); + gridPane.getChildren().add(listView); + + return new Tuple2<>(label, listView); + } + +} diff --git a/gui/src/main/java/io/bitsquare/gui/util/validation/AliPayValidator.java b/gui/src/main/java/io/bitsquare/gui/util/validation/AliPayValidator.java new file mode 100644 index 0000000000..c002d60c15 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/util/validation/AliPayValidator.java @@ -0,0 +1,39 @@ +/* + * 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.util.validation; + + +public final class AliPayValidator extends InputValidator { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public ValidationResult validate(String input) { + // TODO + return super.validate(input); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private methods + /////////////////////////////////////////////////////////////////////////////////////////// + + +} diff --git a/gui/src/main/java/io/bitsquare/gui/util/validation/AltCoinAddressValidator.java b/gui/src/main/java/io/bitsquare/gui/util/validation/AltCoinAddressValidator.java new file mode 100644 index 0000000000..ad9483c039 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/util/validation/AltCoinAddressValidator.java @@ -0,0 +1,39 @@ +/* + * 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.util.validation; + + +public final class AltCoinAddressValidator extends InputValidator { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public ValidationResult validate(String input) { + // TODO + return super.validate(input); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private methods + /////////////////////////////////////////////////////////////////////////////////////////// + + +} diff --git a/gui/src/main/java/io/bitsquare/gui/util/validation/BICValidator.java b/gui/src/main/java/io/bitsquare/gui/util/validation/BICValidator.java new file mode 100644 index 0000000000..8b74a98883 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/util/validation/BICValidator.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.util.validation; + + +public final class BICValidator extends InputValidator { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public ValidationResult validate(String input) { + // TODO Add validation for primary and secondary IDs according to the selected type + + // IBAN max 34 chars + // bic: max 11 char + return super.validate(input); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private methods + /////////////////////////////////////////////////////////////////////////////////////////// + + +} diff --git a/gui/src/main/java/io/bitsquare/gui/util/validation/EmailValidator.java b/gui/src/main/java/io/bitsquare/gui/util/validation/EmailValidator.java new file mode 100644 index 0000000000..9d5dda1df1 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/util/validation/EmailValidator.java @@ -0,0 +1,39 @@ +/* + * 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.util.validation; + + +public final class EmailValidator extends InputValidator { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public ValidationResult validate(String input) { + // TODO + return super.validate(input); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private methods + /////////////////////////////////////////////////////////////////////////////////////////// + + +} diff --git a/gui/src/main/java/io/bitsquare/gui/util/validation/IBANValidator.java b/gui/src/main/java/io/bitsquare/gui/util/validation/IBANValidator.java new file mode 100644 index 0000000000..2253dd5c11 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/util/validation/IBANValidator.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.util.validation; + + +public final class IBANValidator extends InputValidator { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public ValidationResult validate(String input) { + // TODO Add validation for primary and secondary IDs according to the selected type + + // IBAN max 34 chars + // bic: max 11 char + return super.validate(input); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private methods + /////////////////////////////////////////////////////////////////////////////////////////// + + +} diff --git a/gui/src/main/java/io/bitsquare/gui/util/validation/OKPayValidator.java b/gui/src/main/java/io/bitsquare/gui/util/validation/OKPayValidator.java new file mode 100644 index 0000000000..1b559e00e1 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/util/validation/OKPayValidator.java @@ -0,0 +1,39 @@ +/* + * 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.util.validation; + + +public final class OKPayValidator extends InputValidator { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public ValidationResult validate(String input) { + // TODO + return super.validate(input); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private methods + /////////////////////////////////////////////////////////////////////////////////////////// + + +} diff --git a/gui/src/main/java/io/bitsquare/gui/util/validation/PerfectMoneyValidator.java b/gui/src/main/java/io/bitsquare/gui/util/validation/PerfectMoneyValidator.java new file mode 100644 index 0000000000..aa5f4294da --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/util/validation/PerfectMoneyValidator.java @@ -0,0 +1,39 @@ +/* + * 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.util.validation; + + +public final class PerfectMoneyValidator extends InputValidator { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public ValidationResult validate(String input) { + // TODO + return super.validate(input); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private methods + /////////////////////////////////////////////////////////////////////////////////////////// + + +} diff --git a/gui/src/main/java/io/bitsquare/gui/util/validation/SwishValidator.java b/gui/src/main/java/io/bitsquare/gui/util/validation/SwishValidator.java new file mode 100644 index 0000000000..13c1e11230 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/util/validation/SwishValidator.java @@ -0,0 +1,39 @@ +/* + * 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.util.validation; + + +public final class SwishValidator extends InputValidator { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public ValidationResult validate(String input) { + // TODO + return super.validate(input); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private methods + /////////////////////////////////////////////////////////////////////////////////////////// + + +} diff --git a/gui/src/main/resources/html/base.css b/gui/src/main/resources/html/base.css new file mode 100644 index 0000000000..25d7fbadf6 --- /dev/null +++ b/gui/src/main/resources/html/base.css @@ -0,0 +1,46 @@ +/* + * 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 . + */ + +body { + font-family: sans-serif; + color: #333; + font-size: 13px; +} + +a { + text-decoration: none; +} + +a:link { + color: #0f87c3; +} + +a:visited { + color: #0f87c3; +} + +a:hover { + color: #666666; +} + +a:active { + color: #0f87c3; +} + +h1 { + font-size: 16px; +} \ No newline at end of file diff --git a/gui/src/main/resources/html/tac.html b/gui/src/main/resources/html/tac.html new file mode 100644 index 0000000000..0619a5ceee --- /dev/null +++ b/gui/src/main/resources/html/tac.html @@ -0,0 +1,23 @@ + + + + Insert title here + + + +

USER AGREEMENT

+1. This software is experimental and provided "as is", without warranty of any kind, express or implied, including but +not limited to the warranties of +merchantability, fitness for a particular purpose and noninfringement. +
+In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an +action of contract, tort or otherwise, +arising from, out of or in connection with the software or the use or other dealings in the software. +

+2. The user is responsible to use the software in compliance with local laws. +

+3. The user confirms that he has read and agreed to the rules defined in our +wiki regrading the dispute +process.
+ + \ No newline at end of file diff --git a/gui/src/main/resources/html/tradeWallet.html b/gui/src/main/resources/html/tradeWallet.html new file mode 100644 index 0000000000..51d06bb350 --- /dev/null +++ b/gui/src/main/resources/html/tradeWallet.html @@ -0,0 +1,35 @@ + + + + + + Insert title here + + + +

Information

+Bitsquare does not use a global wallet.
+For every trade a dedicated wallet will be created. Funding of the wallet will be done just in time when it is needed. +For instance when you create an offer or +when you take an offer. Withdrawing from your funds can be done after a trade has been completed.
+That separation of addresses helps to protect users privacy and not leaking information of previous trades to new +traders.
+Please read more background information to that topic at the Bitsquare FAQ. + + \ No newline at end of file diff --git a/gui/src/main/resources/images/attachment.png b/gui/src/main/resources/images/attachment.png new file mode 100644 index 0000000000..617b9a1de1 Binary files /dev/null and b/gui/src/main/resources/images/attachment.png differ diff --git a/gui/src/main/resources/images/attachment@2x.png b/gui/src/main/resources/images/attachment@2x.png new file mode 100644 index 0000000000..bf99d5afa3 Binary files /dev/null and b/gui/src/main/resources/images/attachment@2x.png differ diff --git a/gui/src/main/resources/images/bubble_arrow_blue_left.png b/gui/src/main/resources/images/bubble_arrow_blue_left.png new file mode 100644 index 0000000000..ac3898051c Binary files /dev/null and b/gui/src/main/resources/images/bubble_arrow_blue_left.png differ diff --git a/gui/src/main/resources/images/bubble_arrow_blue_left@2x.png b/gui/src/main/resources/images/bubble_arrow_blue_left@2x.png new file mode 100644 index 0000000000..49edbd2573 Binary files /dev/null and b/gui/src/main/resources/images/bubble_arrow_blue_left@2x.png differ diff --git a/gui/src/main/resources/images/bubble_arrow_blue_right.png b/gui/src/main/resources/images/bubble_arrow_blue_right.png new file mode 100644 index 0000000000..1aaaa14a65 Binary files /dev/null and b/gui/src/main/resources/images/bubble_arrow_blue_right.png differ diff --git a/gui/src/main/resources/images/bubble_arrow_blue_right@2x.png b/gui/src/main/resources/images/bubble_arrow_blue_right@2x.png new file mode 100644 index 0000000000..3c8d17f74c Binary files /dev/null and b/gui/src/main/resources/images/bubble_arrow_blue_right@2x.png differ diff --git a/gui/src/main/resources/images/bubble_arrow_grey_left.png b/gui/src/main/resources/images/bubble_arrow_grey_left.png new file mode 100644 index 0000000000..5d8662e341 Binary files /dev/null and b/gui/src/main/resources/images/bubble_arrow_grey_left.png differ diff --git a/gui/src/main/resources/images/bubble_arrow_grey_left@2x.png b/gui/src/main/resources/images/bubble_arrow_grey_left@2x.png new file mode 100644 index 0000000000..b16e0f2289 Binary files /dev/null and b/gui/src/main/resources/images/bubble_arrow_grey_left@2x.png differ diff --git a/gui/src/main/resources/images/bubble_arrow_grey_right.png b/gui/src/main/resources/images/bubble_arrow_grey_right.png new file mode 100644 index 0000000000..802f4d423a Binary files /dev/null and b/gui/src/main/resources/images/bubble_arrow_grey_right.png differ diff --git a/gui/src/main/resources/images/bubble_arrow_grey_right@2x.png b/gui/src/main/resources/images/bubble_arrow_grey_right@2x.png new file mode 100644 index 0000000000..1c028ece3a Binary files /dev/null and b/gui/src/main/resources/images/bubble_arrow_grey_right@2x.png differ diff --git a/gui/src/main/resources/images/light_close.png b/gui/src/main/resources/images/light_close.png new file mode 100644 index 0000000000..c6350c526a Binary files /dev/null and b/gui/src/main/resources/images/light_close.png differ diff --git a/gui/src/main/resources/images/light_close@2x.png b/gui/src/main/resources/images/light_close@2x.png new file mode 100644 index 0000000000..269ce981ed Binary files /dev/null and b/gui/src/main/resources/images/light_close@2x.png differ diff --git a/gui/src/main/resources/images/nav/disputes.png b/gui/src/main/resources/images/nav/disputes.png new file mode 100644 index 0000000000..69ab9cbd5f Binary files /dev/null and b/gui/src/main/resources/images/nav/disputes.png differ diff --git a/gui/src/main/resources/images/nav/disputes@2x.png b/gui/src/main/resources/images/nav/disputes@2x.png new file mode 100644 index 0000000000..bcd0f84bf3 Binary files /dev/null and b/gui/src/main/resources/images/nav/disputes@2x.png differ diff --git a/gui/src/main/resources/images/nav/disputes_active.png b/gui/src/main/resources/images/nav/disputes_active.png new file mode 100644 index 0000000000..86b0d5404a Binary files /dev/null and b/gui/src/main/resources/images/nav/disputes_active.png differ diff --git a/gui/src/main/resources/images/nav/disputes_active@2x.png b/gui/src/main/resources/images/nav/disputes_active@2x.png new file mode 100644 index 0000000000..13a0864a7d Binary files /dev/null and b/gui/src/main/resources/images/nav/disputes_active@2x.png differ diff --git a/gui/src/main/resources/images/nav/market.png b/gui/src/main/resources/images/nav/market.png new file mode 100644 index 0000000000..f74e93c8b7 Binary files /dev/null and b/gui/src/main/resources/images/nav/market.png differ diff --git a/gui/src/main/resources/images/nav/market@2x.png b/gui/src/main/resources/images/nav/market@2x.png new file mode 100644 index 0000000000..34bf8afe14 Binary files /dev/null and b/gui/src/main/resources/images/nav/market@2x.png differ diff --git a/gui/src/main/resources/images/nav/market_active.png b/gui/src/main/resources/images/nav/market_active.png new file mode 100644 index 0000000000..53491ea044 Binary files /dev/null and b/gui/src/main/resources/images/nav/market_active.png differ diff --git a/gui/src/main/resources/images/nav/market_active@2x.png b/gui/src/main/resources/images/nav/market_active@2x.png new file mode 100644 index 0000000000..0848f0974a Binary files /dev/null and b/gui/src/main/resources/images/nav/market_active@2x.png differ diff --git a/gui/src/test/java/io/bitsquare/gui/AwesomeFontDemo.java b/gui/src/test/java/io/bitsquare/gui/AwesomeFontDemo.java new file mode 100644 index 0000000000..d915ad6f0e --- /dev/null +++ b/gui/src/test/java/io/bitsquare/gui/AwesomeFontDemo.java @@ -0,0 +1,64 @@ +/* + * 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; + +import de.jensd.fx.fontawesome.AwesomeDude; +import de.jensd.fx.fontawesome.AwesomeIcon; +import javafx.application.Application; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.Pane; +import javafx.stage.Stage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +public class AwesomeFontDemo extends Application { + private static final Logger log = LoggerFactory.getLogger(AwesomeFontDemo.class); + + public static void main(String[] args) { + launch(args); + } + + @Override + public void start(Stage primaryStage) { + Pane root = new FlowPane(); + List values = new ArrayList<>(Arrays.asList(AwesomeIcon.values())); + values.sort(new Comparator() { + @Override + public int compare(AwesomeIcon o1, AwesomeIcon o2) { + return o1.name().compareTo(o2.name()); + } + }); + for (AwesomeIcon icon : values) { + Label label = new Label(); + Button button = new Button(icon.name(), label); + AwesomeDude.setIcon(label, icon); + root.getChildren().add(button); + } + + primaryStage.setScene(new Scene(root, 900, 850)); + primaryStage.show(); + } +} diff --git a/gui/src/test/java/io/bitsquare/gui/BindingTest.java b/gui/src/test/java/io/bitsquare/gui/BindingTest.java new file mode 100644 index 0000000000..b49fa4153d --- /dev/null +++ b/gui/src/test/java/io/bitsquare/gui/BindingTest.java @@ -0,0 +1,59 @@ +/* + * 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; + +import javafx.application.Application; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BindingTest extends Application { + private static final Logger log = LoggerFactory.getLogger(BindingTest.class); + private static int counter = 0; + + public static void main(String[] args) { + launch(args); + } + + @Override + public void start(Stage primaryStage) { + VBox root = new VBox(); + root.setSpacing(20); + + Label label = new Label(); + StringProperty txt = new SimpleStringProperty(); + txt.set("-"); + label.textProperty().bind(txt); + + Button button = new Button("count up"); + button.setOnAction(e -> { + txt.set("counter " + counter++); + }); + root.getChildren().addAll(label, button); + + + primaryStage.setScene(new Scene(root, 400, 400)); + primaryStage.show(); + } +} diff --git a/network/pom.xml b/network/pom.xml new file mode 100644 index 0000000000..e4677ec39f --- /dev/null +++ b/network/pom.xml @@ -0,0 +1,45 @@ + + + + parent + io.bitsquare + 0.3.2-SNAPSHOT + + 4.0.0 + + network + + + + io.bitsquare + common + ${project.parent.version} + + + + com.msopentech.thali + universal + 0.0.3-SNAPSHOT + + + org.slf4j + slf4j-simple + + + + + com.msopentech.thali + java + 0.0.3-SNAPSHOT + + + org.slf4j + slf4j-simple + + + + + + \ No newline at end of file diff --git a/network/src/main/java/io/bitsquare/p2p/Address.java b/network/src/main/java/io/bitsquare/p2p/Address.java new file mode 100644 index 0000000000..f49f48b353 --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/Address.java @@ -0,0 +1,48 @@ +package io.bitsquare.p2p; + +import java.io.Serializable; +import java.util.regex.Pattern; + +public class Address implements Serializable { + public final String hostName; + public final int port; + + public Address(String hostName, int port) { + this.hostName = hostName; + this.port = port; + } + + public Address(String fullAddress) { + final String[] split = fullAddress.split(Pattern.quote(":")); + this.hostName = split[0]; + this.port = Integer.parseInt(split[1]); + } + + public String getFullAddress() { + return hostName + ":" + port; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Address)) return false; + + Address address = (Address) o; + + if (port != address.port) return false; + return !(hostName != null ? !hostName.equals(address.hostName) : address.hostName != null); + + } + + @Override + public int hashCode() { + int result = hostName != null ? hostName.hashCode() : 0; + result = 31 * result + port; + return result; + } + + @Override + public String toString() { + return getFullAddress(); + } +} diff --git a/network/src/main/java/io/bitsquare/p2p/AuthenticationException.java b/network/src/main/java/io/bitsquare/p2p/AuthenticationException.java new file mode 100644 index 0000000000..84135a7ba8 --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/AuthenticationException.java @@ -0,0 +1,22 @@ +package io.bitsquare.p2p; + +public class AuthenticationException extends RuntimeException { + public AuthenticationException() { + } + + public AuthenticationException(String message) { + super(message); + } + + public AuthenticationException(String message, Throwable cause) { + super(message, cause); + } + + public AuthenticationException(Throwable cause) { + super(cause); + } + + public AuthenticationException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/network/src/main/java/io/bitsquare/p2p/NetworkStatistics.java b/network/src/main/java/io/bitsquare/p2p/NetworkStatistics.java new file mode 100644 index 0000000000..25a22a6126 --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/NetworkStatistics.java @@ -0,0 +1,14 @@ +package io.bitsquare.p2p; + +import com.google.inject.Inject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class NetworkStatistics { + private static final Logger log = LoggerFactory.getLogger(NetworkStatistics.class); + + @Inject + public NetworkStatistics() { + + } +} diff --git a/network/src/main/java/io/bitsquare/p2p/P2PService.java b/network/src/main/java/io/bitsquare/p2p/P2PService.java index bfabeb4a9a..2873ddbfa6 100644 --- a/network/src/main/java/io/bitsquare/p2p/P2PService.java +++ b/network/src/main/java/io/bitsquare/p2p/P2PService.java @@ -49,6 +49,7 @@ public class P2PService { private static final Logger log = LoggerFactory.getLogger(P2PService.class); private final EncryptionService encryptionService; + private final SetupListener setupListener; private KeyRing keyRing; private final NetworkStatistics networkStatistics; @@ -108,6 +109,58 @@ public class P2PService { // Listeners + setupListener = new SetupListener() { + @Override + public void onTorNodeReady() { + UserThread.execute(() -> p2pServiceListeners.stream().forEach(e -> e.onTorNodeReady())); + + // we don't know yet our own address so we can not filter that from the + // seedNodeAddresses in case we are a seed node + sendGetAllDataMessage(seedNodeAddresses); + } + + @Override + public void onHiddenServiceReady() { + hiddenServiceReady = true; + tryStartAuthentication(); + + UserThread.execute(() -> p2pServiceListeners.stream().forEach(e -> e.onHiddenServiceReady())); + } + + @Override + public void onSetupFailed(Throwable throwable) { + UserThread.execute(() -> p2pServiceListeners.stream().forEach(e -> e.onSetupFailed(throwable))); + } + }; + + networkNode.addConnectionListener(new ConnectionListener() { + @Override + public void onConnection(Connection connection) { + } + + @Override + public void onPeerAddressAuthenticated(Address peerAddress, Connection connection) { + authenticatedPeerAddresses.add(peerAddress); + authenticatedToFirstPeer = true; + + P2PService.this.authenticated = true; + dataStorage.setAuthenticated(true); + UserThread.execute(() -> p2pServiceListeners.stream().forEach(e -> e.onAuthenticated())); + } + + @Override + public void onDisconnect(Reason reason, Connection connection) { + Address peerAddress = connection.getPeerAddress(); + if (peerAddress != null) + authenticatedPeerAddresses.remove(peerAddress); + } + + @Override + public void onError(Throwable throwable) { + log.error("onError self/ConnectionException " + networkNode.getAddress() + "/" + throwable); + } + }); + networkNode.addMessageListener((message, connection) -> { if (message instanceof GetDataSetMessage) { log.trace("Received GetAllDataMessage: " + message); @@ -201,57 +254,7 @@ public class P2PService { if (listener != null) addP2PServiceListener(listener); - networkNode.start(new SetupListener() { - @Override - public void onTorNodeReady() { - UserThread.execute(() -> p2pServiceListeners.stream().forEach(e -> e.onTorNodeReady())); - - // we don't know yet our own address so we can not filter that from the - // seedNodeAddresses in case we are a seed node - sendGetAllDataMessage(seedNodeAddresses); - } - - @Override - public void onHiddenServiceReady() { - hiddenServiceReady = true; - tryStartAuthentication(); - - UserThread.execute(() -> p2pServiceListeners.stream().forEach(e -> e.onHiddenServiceReady())); - } - - @Override - public void onSetupFailed(Throwable throwable) { - UserThread.execute(() -> p2pServiceListeners.stream().forEach(e -> e.onSetupFailed(throwable))); - } - }); - - networkNode.addConnectionListener(new ConnectionListener() { - @Override - public void onConnection(Connection connection) { - } - - @Override - public void onPeerAddressAuthenticated(Address peerAddress, Connection connection) { - authenticatedPeerAddresses.add(peerAddress); - authenticatedToFirstPeer = true; - - P2PService.this.authenticated = true; - dataStorage.setAuthenticated(true); - UserThread.execute(() -> p2pServiceListeners.stream().forEach(e -> e.onAuthenticated())); - } - - @Override - public void onDisconnect(Reason reason, Connection connection) { - Address peerAddress = connection.getPeerAddress(); - if (peerAddress != null) - authenticatedPeerAddresses.remove(peerAddress); - } - - @Override - public void onError(Throwable throwable) { - log.error("onError self/ConnectionException " + networkNode.getAddress() + "/" + throwable); - } - }); + networkNode.start(setupListener); } public void shutDown(Runnable shutDownCompleteHandler) { diff --git a/network/src/main/java/io/bitsquare/p2p/P2PServiceListener.java b/network/src/main/java/io/bitsquare/p2p/P2PServiceListener.java new file mode 100644 index 0000000000..93e42c89bc --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/P2PServiceListener.java @@ -0,0 +1,11 @@ +package io.bitsquare.p2p; + + +import io.bitsquare.p2p.network.SetupListener; + +public interface P2PServiceListener extends SetupListener { + + void onAllDataReceived(); + + void onAuthenticated(); +} diff --git a/network/src/main/java/io/bitsquare/p2p/Utils.java b/network/src/main/java/io/bitsquare/p2p/Utils.java new file mode 100644 index 0000000000..7a8148dbf8 --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/Utils.java @@ -0,0 +1,96 @@ +package io.bitsquare.p2p; + +import io.bitsquare.common.ByteArrayUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.Serializable; +import java.net.ServerSocket; +import java.util.Random; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.zip.DataFormatException; +import java.util.zip.Deflater; +import java.util.zip.Inflater; + +public class Utils { + private static final Logger log = LoggerFactory.getLogger(Utils.class); + + public static int findFreeSystemPort() { + try { + ServerSocket server = new ServerSocket(0); + int port = server.getLocalPort(); + server.close(); + return port; + } catch (IOException ignored) { + } finally { + return new Random().nextInt(10000) + 50000; + } + } + + public static void shutDownExecutorService(ExecutorService executorService) { + shutDownExecutorService(executorService, 200); + } + + public static void shutDownExecutorService(ExecutorService executorService, long waitBeforeShutDown) { + executorService.shutdown(); + try { + executorService.awaitTermination(waitBeforeShutDown, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + executorService.shutdownNow(); + } + + public static byte[] compress(Serializable input) { + return compress(ByteArrayUtils.objectToByteArray(input)); + } + + public static byte[] compress(byte[] input) { + Deflater compressor = new Deflater(); + compressor.setLevel(Deflater.BEST_SPEED); + compressor.setInput(input); + compressor.finish(); + ByteArrayOutputStream bos = new ByteArrayOutputStream(input.length); + byte[] buf = new byte[8192]; + while (!compressor.finished()) { + int count = compressor.deflate(buf); + bos.write(buf, 0, count); + } + try { + bos.close(); + } catch (IOException e) { + } + return bos.toByteArray(); + } + + public static byte[] decompress(byte[] compressedData, int offset, int length) { + Inflater decompressor = new Inflater(); + decompressor.setInput(compressedData, offset, length); + ByteArrayOutputStream bos = new ByteArrayOutputStream(length); + byte[] buf = new byte[8192]; + while (!decompressor.finished()) { + try { + int count = decompressor.inflate(buf); + bos.write(buf, 0, count); + } catch (DataFormatException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + try { + bos.close(); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + return bos.toByteArray(); + } + + public static Serializable decompress(byte[] compressedData) { + return (Serializable) ByteArrayUtils.byteArrayToObject(decompress(compressedData, 0, compressedData.length)); + } + +} diff --git a/network/src/main/java/io/bitsquare/p2p/messaging/DecryptedMailListener.java b/network/src/main/java/io/bitsquare/p2p/messaging/DecryptedMailListener.java new file mode 100644 index 0000000000..533ce41e56 --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/messaging/DecryptedMailListener.java @@ -0,0 +1,8 @@ +package io.bitsquare.p2p.messaging; + +import io.bitsquare.p2p.Address; + +public interface DecryptedMailListener { + + void onMailMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey, Address peerAddress); +} diff --git a/network/src/main/java/io/bitsquare/p2p/messaging/DecryptedMailboxListener.java b/network/src/main/java/io/bitsquare/p2p/messaging/DecryptedMailboxListener.java new file mode 100644 index 0000000000..cd1f435952 --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/messaging/DecryptedMailboxListener.java @@ -0,0 +1,8 @@ +package io.bitsquare.p2p.messaging; + +import io.bitsquare.p2p.Address; + +public interface DecryptedMailboxListener { + + void onMailboxMessageAdded(DecryptedMessageWithPubKey decryptedMessageWithPubKey, Address senderAddress); +} diff --git a/network/src/main/java/io/bitsquare/p2p/messaging/DecryptedMessageWithPubKey.java b/network/src/main/java/io/bitsquare/p2p/messaging/DecryptedMessageWithPubKey.java new file mode 100644 index 0000000000..892d1338cc --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/messaging/DecryptedMessageWithPubKey.java @@ -0,0 +1,64 @@ +/* + * 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.p2p.messaging; + +import io.bitsquare.app.Version; +import io.bitsquare.p2p.Message; + +import java.security.PublicKey; + +public final class DecryptedMessageWithPubKey implements MailMessage { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + + public final Message message; + public final PublicKey signaturePubKey; + + public DecryptedMessageWithPubKey(Message message, PublicKey signaturePubKey) { + this.message = message; + this.signaturePubKey = signaturePubKey; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof DecryptedMessageWithPubKey)) return false; + + DecryptedMessageWithPubKey that = (DecryptedMessageWithPubKey) o; + + if (message != null ? !message.equals(that.message) : that.message != null) return false; + return !(signaturePubKey != null ? !signaturePubKey.equals(that.signaturePubKey) : that.signaturePubKey != null); + + } + + @Override + public int hashCode() { + int result = message != null ? message.hashCode() : 0; + result = 31 * result + (signaturePubKey != null ? signaturePubKey.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "DecryptedMessageWithPubKey{" + + "hashCode=" + hashCode() + + ", message=" + message + + ", signaturePubKey.hashCode()=" + signaturePubKey.hashCode() + + '}'; + } +} diff --git a/network/src/main/java/io/bitsquare/p2p/messaging/MailMessage.java b/network/src/main/java/io/bitsquare/p2p/messaging/MailMessage.java new file mode 100644 index 0000000000..319143185e --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/messaging/MailMessage.java @@ -0,0 +1,24 @@ +/* + * 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.p2p.messaging; + +import io.bitsquare.p2p.Message; + +public interface MailMessage extends Message { + +} diff --git a/network/src/main/java/io/bitsquare/p2p/messaging/MailboxMessage.java b/network/src/main/java/io/bitsquare/p2p/messaging/MailboxMessage.java new file mode 100644 index 0000000000..1306f67401 --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/messaging/MailboxMessage.java @@ -0,0 +1,25 @@ +/* + * 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.p2p.messaging; + + +import io.bitsquare.p2p.Address; + +public interface MailboxMessage extends MailMessage { + Address getSenderAddress(); +} diff --git a/network/src/main/java/io/bitsquare/p2p/messaging/SealedAndSignedMessage.java b/network/src/main/java/io/bitsquare/p2p/messaging/SealedAndSignedMessage.java new file mode 100644 index 0000000000..c0d3be665a --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/messaging/SealedAndSignedMessage.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.p2p.messaging; + +import io.bitsquare.app.Version; +import io.bitsquare.common.util.Utilities; + +import javax.crypto.SealedObject; +import java.security.PublicKey; +import java.util.Arrays; + +/** + * Packs the encrypted symmetric secretKey and the encrypted and signed message into one object. + * SecretKey is encrypted with asymmetric pubKey of peer. Signed message is encrypted with secretKey. + * Using that hybrid encryption model we are not restricted by data size and performance as symmetric encryption is very fast. + */ +public final class SealedAndSignedMessage implements MailMessage { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + + public final SealedObject sealedSecretKey; + public final SealedObject sealedMessage; + public final PublicKey signaturePubKey; + + public SealedAndSignedMessage(SealedObject sealedSecretKey, SealedObject sealedMessage, PublicKey signaturePubKey) { + this.sealedSecretKey = sealedSecretKey; + this.sealedMessage = sealedMessage; + this.signaturePubKey = signaturePubKey; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SealedAndSignedMessage)) return false; + + SealedAndSignedMessage that = (SealedAndSignedMessage) o; + + return Arrays.equals(Utilities.objectToByteArray(this), Utilities.objectToByteArray(that)); + } + + @Override + public int hashCode() { + byte[] bytes = Utilities.objectToByteArray(this); + return bytes != null ? Arrays.hashCode(bytes) : 0; + } + + @Override + public String toString() { + return "SealedAndSignedMessage{" + + "hashCode=" + hashCode() + + '}'; + } +} diff --git a/network/src/main/java/io/bitsquare/p2p/messaging/SendMailMessageListener.java b/network/src/main/java/io/bitsquare/p2p/messaging/SendMailMessageListener.java new file mode 100644 index 0000000000..b93f7590ba --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/messaging/SendMailMessageListener.java @@ -0,0 +1,7 @@ +package io.bitsquare.p2p.messaging; + +public interface SendMailMessageListener { + void onArrived(); + + void onFault(); +} diff --git a/network/src/main/java/io/bitsquare/p2p/messaging/SendMailboxMessageListener.java b/network/src/main/java/io/bitsquare/p2p/messaging/SendMailboxMessageListener.java new file mode 100644 index 0000000000..5fab3488ce --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/messaging/SendMailboxMessageListener.java @@ -0,0 +1,9 @@ +package io.bitsquare.p2p.messaging; + +public interface SendMailboxMessageListener { + void onArrived(); + + void onStoredInMailbox(); + + void onFault(); +} diff --git a/network/src/main/java/io/bitsquare/p2p/network/Connection.java b/network/src/main/java/io/bitsquare/p2p/network/Connection.java new file mode 100644 index 0000000000..b153e628bd --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/network/Connection.java @@ -0,0 +1,365 @@ +package io.bitsquare.p2p.network; + +import io.bitsquare.common.ByteArrayUtils; +import io.bitsquare.p2p.Address; +import io.bitsquare.p2p.Message; +import io.bitsquare.p2p.Utils; +import io.bitsquare.p2p.network.messages.CloseConnectionMessage; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; +import java.net.Socket; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.util.Date; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class Connection { + private static final Logger log = LoggerFactory.getLogger(Connection.class); + private static final int MAX_MSG_SIZE = 5 * 1024 * 1024; // 5 MB of compressed data + private static final int MAX_ILLEGAL_REQUESTS = 5; + private static final int SOCKET_TIMEOUT = 30 * 60 * 1000; // 30 min. + + public static int getMaxMsgSize() { + return MAX_MSG_SIZE; + } + + private final Socket socket; + private final int port; + private final MessageListener messageListener; + private final ConnectionListener connectionListener; + private final String uid; + + private final Map illegalRequests = new ConcurrentHashMap<>(); + private final ExecutorService executorService = Executors.newCachedThreadPool(); + private ObjectOutputStream out; + private ObjectInputStream in; + + private volatile boolean stopped; + private volatile boolean shutDownInProgress; + private volatile boolean inputHandlerStopped; + + private volatile Date lastActivityDate; + @Nullable + private Address peerAddress; + private boolean isAuthenticated; + + + //TODO got java.util.zip.DataFormatException: invalid distance too far back + // java.util.zip.DataFormatException: invalid literal/lengths set + // use GZIPInputStream but problems with blocking + private boolean useCompression = false; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public Connection(Socket socket, MessageListener messageListener, ConnectionListener connectionListener) { + this.socket = socket; + port = socket.getLocalPort(); + this.messageListener = messageListener; + this.connectionListener = connectionListener; + + uid = UUID.randomUUID().toString(); + + try { + socket.setSoTimeout(SOCKET_TIMEOUT); + // Need to access first the ObjectOutputStream otherwise the ObjectInputStream would block + // See: https://stackoverflow.com/questions/5658089/java-creating-a-new-objectinputstream-blocks/5658109#5658109 + // When you construct an ObjectInputStream, in the constructor the class attempts to read a header that + // the associated ObjectOutputStream on the other end of the connection has written. + // It will not return until that header has been read. + if (useCompression) { + out = new ObjectOutputStream(socket.getOutputStream()); + in = new ObjectInputStream(socket.getInputStream()); + } else { + out = new ObjectOutputStream(socket.getOutputStream()); + in = new ObjectInputStream(socket.getInputStream()); + } + executorService.submit(new InputHandler()); + } catch (IOException e) { + handleConnectionException(e); + } + + lastActivityDate = new Date(); + + connectionListener.onConnection(this); + } + + public void onAuthenticationComplete(Address peerAddress, Connection connection) { + isAuthenticated = true; + this.peerAddress = peerAddress; + connectionListener.onPeerAddressAuthenticated(peerAddress, connection); + } + + public boolean isStopped() { + return stopped; + } + + public void sendMessage(Message message) { + if (!stopped) { + try { + log.trace("writeObject " + message + " on connection with port " + port); + if (!stopped) { + Object objectToWrite; + if (useCompression) { + byte[] messageAsBytes = ByteArrayUtils.objectToByteArray(message); + // log.trace("Write object uncompressed data size: " + messageAsBytes.length); + byte[] compressed = Utils.compress(message); + //log.trace("Write object compressed data size: " + compressed.length); + objectToWrite = compressed; + } else { + // log.trace("Write object data size: " + ByteArrayUtils.objectToByteArray(message).length); + objectToWrite = message; + } + out.writeObject(objectToWrite); + out.flush(); + + lastActivityDate = new Date(); + } + } catch (IOException e) { + handleConnectionException(e); + } + } else { + connectionListener.onDisconnect(ConnectionListener.Reason.ALREADY_CLOSED, Connection.this); + } + } + + public void reportIllegalRequest(IllegalRequest illegalRequest) { + log.warn("We got reported an illegal request " + illegalRequest); + int prevCounter = illegalRequests.get(illegalRequest); + if (prevCounter > illegalRequest.limit) { + log.warn("We close connection as we received too many illegal requests.\n" + illegalRequests.toString()); + shutDown(); + } else { + illegalRequests.put(illegalRequest, ++prevCounter); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + public Socket getSocket() { + return socket; + } + + @Nullable + public Address getPeerAddress() { + return peerAddress; + } + + public Date getLastActivityDate() { + return lastActivityDate; + } + + public boolean isAuthenticated() { + return isAuthenticated; + } + + public String getUid() { + return uid; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Setters + /////////////////////////////////////////////////////////////////////////////////////////// + + public void setPeerAddress(Address peerAddress) { + this.peerAddress = peerAddress; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // ShutDown + /////////////////////////////////////////////////////////////////////////////////////////// + + public void shutDown(Runnable completeHandler) { + shutDown(true, completeHandler); + } + + public void shutDown() { + shutDown(true, null); + } + + private void shutDown(boolean sendCloseConnectionMessage) { + shutDown(sendCloseConnectionMessage, null); + } + + private void shutDown(boolean sendCloseConnectionMessage, @Nullable Runnable shutDownCompleteHandler) { + if (!shutDownInProgress) { + log.info("\n\nShutDown connection:" + + "\npeerAddress=" + peerAddress + + "\nuid=" + getUid() + + "\nisAuthenticated=" + isAuthenticated + + "\nsocket.getPort()=" + socket.getPort() + + "\n\n"); + log.debug("ShutDown " + this.getObjectId()); + log.debug("ShutDown connection requested. Connection=" + this.toString()); + + shutDownInProgress = true; + inputHandlerStopped = true; + if (!stopped) { + if (sendCloseConnectionMessage) { + sendMessage(new CloseConnectionMessage()); + try { + // give a bit of time for closing gracefully + Thread.sleep(100); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + } + stopped = true; + connectionListener.onDisconnect(ConnectionListener.Reason.SHUT_DOWN, Connection.this); + + try { + socket.close(); + } catch (SocketException e) { + log.trace("SocketException at shutdown might be expected " + e.getMessage()); + } catch (IOException e) { + e.printStackTrace(); + } finally { + Utils.shutDownExecutorService(executorService); + + log.debug("Connection shutdown complete " + this.toString()); + // dont use executorService as its shut down but call handler on own thread + // to not get interrupted by caller + if (shutDownCompleteHandler != null) + new Thread(shutDownCompleteHandler).start(); + } + } + } + } + + private void handleConnectionException(Exception e) { + if (e instanceof SocketException) { + if (socket.isClosed()) + connectionListener.onDisconnect(ConnectionListener.Reason.SOCKET_CLOSED, Connection.this); + else + connectionListener.onDisconnect(ConnectionListener.Reason.RESET, Connection.this); + } else if (e instanceof SocketTimeoutException) { + connectionListener.onDisconnect(ConnectionListener.Reason.TIMEOUT, Connection.this); + } else if (e instanceof EOFException) { + connectionListener.onDisconnect(ConnectionListener.Reason.PEER_DISCONNECTED, Connection.this); + } else { + log.info("Exception at connection with port " + socket.getLocalPort()); + e.printStackTrace(); + connectionListener.onDisconnect(ConnectionListener.Reason.UNKNOWN, Connection.this); + } + + shutDown(false); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Connection)) return false; + + Connection that = (Connection) o; + + if (port != that.port) return false; + if (uid != null ? !uid.equals(that.uid) : that.uid != null) return false; + return !(peerAddress != null ? !peerAddress.equals(that.peerAddress) : that.peerAddress != null); + + } + + @Override + public int hashCode() { + int result = port; + result = 31 * result + (uid != null ? uid.hashCode() : 0); + result = 31 * result + (peerAddress != null ? peerAddress.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "Connection{" + + "OBJECT ID=" + super.toString().split("@")[1] + + ", uid=" + uid + + ", port=" + port + + ", isAuthenticated=" + isAuthenticated + + ", peerAddress=" + peerAddress + + ", lastActivityDate=" + lastActivityDate + + ", stopped=" + stopped + + ", inputHandlerStopped=" + inputHandlerStopped + + '}'; + } + + public String getObjectId() { + return super.toString().split("@")[1].toString(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // InputHandler + /////////////////////////////////////////////////////////////////////////////////////////// + + private class InputHandler implements Runnable { + @Override + public void run() { + Thread.currentThread().setName("InputHandler-" + socket.getLocalPort()); + while (!inputHandlerStopped) { + try { + log.trace("InputHandler waiting for incoming messages connection=" + Connection.this.getObjectId()); + Object rawInputObject = in.readObject(); + log.trace("New data arrived at inputHandler of connection=" + Connection.this.getObjectId() + + " rawInputObject " + rawInputObject); + + int size = ByteArrayUtils.objectToByteArray(rawInputObject).length; + if (size <= MAX_MSG_SIZE) { + Serializable serializable = null; + if (useCompression) { + if (rawInputObject instanceof byte[]) { + byte[] compressedObjectAsBytes = (byte[]) rawInputObject; + size = compressedObjectAsBytes.length; + //log.trace("Read object compressed data size: " + size); + serializable = Utils.decompress(compressedObjectAsBytes); + } else { + reportIllegalRequest(IllegalRequest.InvalidDataType); + } + } else { + if (rawInputObject instanceof Serializable) { + serializable = (Serializable) rawInputObject; + } else { + reportIllegalRequest(IllegalRequest.InvalidDataType); + } + } + //log.trace("Read object decompressed data size: " + ByteArrayUtils.objectToByteArray(serializable).length); + + // compressed size might be bigger theoretically so we check again after decompression + if (size <= MAX_MSG_SIZE) { + if (serializable instanceof Message) { + lastActivityDate = new Date(); + Message message = (Message) serializable; + if (message instanceof CloseConnectionMessage) + shutDown(false); + else + executorService.submit(() -> messageListener.onMessage(message, Connection.this)); + } else { + reportIllegalRequest(IllegalRequest.InvalidDataType); + } + } else { + log.error("Received decompressed data exceeds max. msg size."); + reportIllegalRequest(IllegalRequest.MaxSizeExceeded); + } + } else { + log.error("Received compressed data exceeds max. msg size."); + reportIllegalRequest(IllegalRequest.MaxSizeExceeded); + } + } catch (IOException | ClassNotFoundException e) { + log.error("Exception" + e); + inputHandlerStopped = true; + handleConnectionException(e); + } + } + } + } +} \ No newline at end of file diff --git a/network/src/main/java/io/bitsquare/p2p/network/ConnectionListener.java b/network/src/main/java/io/bitsquare/p2p/network/ConnectionListener.java new file mode 100644 index 0000000000..e0b7504dbe --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/network/ConnectionListener.java @@ -0,0 +1,25 @@ +package io.bitsquare.p2p.network; + + +import io.bitsquare.p2p.Address; + +public interface ConnectionListener { + + enum Reason { + SOCKET_CLOSED, + RESET, + TIMEOUT, + SHUT_DOWN, + PEER_DISCONNECTED, + ALREADY_CLOSED, + UNKNOWN + } + + void onConnection(Connection connection); + + void onPeerAddressAuthenticated(Address peerAddress, Connection connection); + + void onDisconnect(Reason reason, Connection connection); + + void onError(Throwable throwable); +} diff --git a/network/src/main/java/io/bitsquare/p2p/network/IllegalRequest.java b/network/src/main/java/io/bitsquare/p2p/network/IllegalRequest.java new file mode 100644 index 0000000000..4fa9ef3b7b --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/network/IllegalRequest.java @@ -0,0 +1,13 @@ +package io.bitsquare.p2p.network; + +public enum IllegalRequest { + MaxSizeExceeded(1), + NotAuthenticated(2), + InvalidDataType(2); + + public final int limit; + + IllegalRequest(int limit) { + this.limit = limit; + } +} diff --git a/network/src/main/java/io/bitsquare/p2p/network/LocalhostNetworkNode.java b/network/src/main/java/io/bitsquare/p2p/network/LocalhostNetworkNode.java new file mode 100644 index 0000000000..d4ce538d83 --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/network/LocalhostNetworkNode.java @@ -0,0 +1,132 @@ +package io.bitsquare.p2p.network; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import com.msopentech.thali.java.toronionproxy.JavaOnionProxyContext; +import com.msopentech.thali.java.toronionproxy.JavaOnionProxyManager; +import io.bitsquare.p2p.Address; +import io.nucleo.net.HiddenServiceDescriptor; +import io.nucleo.net.TorNode; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.BindException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.function.Consumer; + +public class LocalhostNetworkNode extends NetworkNode { + private static final Logger log = LoggerFactory.getLogger(LocalhostNetworkNode.class); + + private static int simulateTorDelayTorNode = 0; + private static int simulateTorDelayHiddenService = 0; + private Address address; + + public static void setSimulateTorDelayTorNode(int simulateTorDelayTorNode) { + LocalhostNetworkNode.simulateTorDelayTorNode = simulateTorDelayTorNode; + } + + public static void setSimulateTorDelayHiddenService(int simulateTorDelayHiddenService) { + LocalhostNetworkNode.simulateTorDelayHiddenService = simulateTorDelayHiddenService; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public LocalhostNetworkNode(int port) { + super(port); + } + + @Override + public void start(@Nullable SetupListener setupListener) { + if (setupListener != null) addSetupListener(setupListener); + + executorService = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool()); + + //Tor delay simulation + createTorNode(torNode -> { + setupListeners.stream().forEach(e -> e.onTorNodeReady()); + + // Create Hidden Service (takes about 40 sec.) + createHiddenService(hiddenServiceDescriptor -> { + try { + startServer(new ServerSocket(port)); + } catch (BindException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + + address = new Address("localhost", port); + + + setupListeners.stream().forEach(e -> e.onHiddenServiceReady()); + }); + }); + } + + + @Override + @Nullable + public Address getAddress() { + return address; + } + + @Override + protected Socket getSocket(Address peerAddress) throws IOException { + return new Socket(peerAddress.hostName, peerAddress.port); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Tor delay simulation + /////////////////////////////////////////////////////////////////////////////////////////// + + private void createTorNode(final Consumer resultHandler) { + Callable> task = () -> { + long ts = System.currentTimeMillis(); + log.trace("[simulation] Create TorNode"); + if (simulateTorDelayTorNode > 0) Thread.sleep(simulateTorDelayTorNode); + log.trace("\n\n##### TorNode created [simulation]. Took " + (System.currentTimeMillis() - ts) + " ms\n\n"); + return null; + }; + ListenableFuture> future = executorService.submit(task); + Futures.addCallback(future, new FutureCallback>() { + public void onSuccess(TorNode torNode) { + resultHandler.accept(torNode); + } + + public void onFailure(Throwable throwable) { + log.error("[simulation] TorNode creation failed"); + } + }); + } + + private void createHiddenService(final Consumer resultHandler) { + Callable task = () -> { + long ts = System.currentTimeMillis(); + log.debug("[simulation] Create hidden service"); + if (simulateTorDelayHiddenService > 0) Thread.sleep(simulateTorDelayHiddenService); + log.debug("\n\n##### Hidden service created [simulation]. Took " + (System.currentTimeMillis() - ts) + " ms\n\n"); + return null; + }; + ListenableFuture future = executorService.submit(task); + Futures.addCallback(future, new FutureCallback() { + public void onSuccess(HiddenServiceDescriptor hiddenServiceDescriptor) { + resultHandler.accept(hiddenServiceDescriptor); + } + + public void onFailure(Throwable throwable) { + log.error("[simulation] Hidden service creation failed"); + } + }); + } + +} diff --git a/network/src/main/java/io/bitsquare/p2p/network/MessageListener.java b/network/src/main/java/io/bitsquare/p2p/network/MessageListener.java new file mode 100644 index 0000000000..cb06759d49 --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/network/MessageListener.java @@ -0,0 +1,8 @@ +package io.bitsquare.p2p.network; + +import io.bitsquare.p2p.Message; + +public interface MessageListener { + + void onMessage(Message message, Connection connection); +} diff --git a/network/src/main/java/io/bitsquare/p2p/network/NetworkNode.java b/network/src/main/java/io/bitsquare/p2p/network/NetworkNode.java new file mode 100644 index 0000000000..e97d63014a --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/network/NetworkNode.java @@ -0,0 +1,320 @@ +package io.bitsquare.p2p.network; + +import com.google.common.util.concurrent.*; +import io.bitsquare.p2p.Address; +import io.bitsquare.p2p.Message; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.*; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +import static com.google.common.base.Preconditions.checkNotNull; + +public abstract class NetworkNode implements MessageListener, ConnectionListener { + private static final Logger log = LoggerFactory.getLogger(NetworkNode.class); + + protected final int port; + private final Map outBoundConnections = new ConcurrentHashMap<>(); + private final Map inBoundAuthenticatedConnections = new ConcurrentHashMap<>(); + private final List inBoundTempConnections = new CopyOnWriteArrayList<>(); + private final List messageListeners = new CopyOnWriteArrayList<>(); + private final List connectionListeners = new CopyOnWriteArrayList<>(); + protected final List setupListeners = new CopyOnWriteArrayList<>(); + protected ListeningExecutorService executorService; + private Server server; + private volatile boolean shutDownInProgress; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public NetworkNode(int port) { + this.port = port; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void start() { + start(null); + } + + abstract public void start(@Nullable SetupListener setupListener); + + public SettableFuture sendMessage(@NotNull Address peerAddress, Message message) { + log.trace("sendMessage message=" + message); + checkNotNull(peerAddress, "peerAddress must not be null"); + final SettableFuture resultFuture = SettableFuture.create(); + + Callable task = () -> { + Thread.currentThread().setName("Outgoing-connection-to-" + peerAddress); + Connection connection = outBoundConnections.get(peerAddress); + + if (connection != null && connection.isStopped()) { + // can happen because of threading... + log.trace("We have a connection which is already stopped in outBoundConnections. Connection.uid=" + connection.getUid()); + outBoundConnections.remove(peerAddress); + connection = null; + } + + if (connection == null) { + Optional connectionOptional = inBoundAuthenticatedConnections.values().stream() + .filter(e -> peerAddress.equals(e.getPeerAddress())) + .findAny(); + if (connectionOptional.isPresent()) + connection = connectionOptional.get(); + if (connection != null) + log.trace("We have found a connection in inBoundAuthenticatedConnections. Connection.uid=" + connection.getUid()); + } + if (connection == null) { + Optional connectionOptional = inBoundTempConnections.stream() + .filter(e -> peerAddress.equals(e.getPeerAddress())) + .findAny(); + if (connectionOptional.isPresent()) + connection = connectionOptional.get(); + if (connection != null) + log.trace("We have found a connection in inBoundTempConnections. Connection.uid=" + connection.getUid()); + } + + if (connection == null) { + try { + Socket socket = getSocket(peerAddress); // can take a while when using tor + connection = new Connection(socket, + (message1, connection1) -> NetworkNode.this.onMessage(message1, connection1), + new ConnectionListener() { + @Override + public void onConnection(Connection connection) { + NetworkNode.this.onConnection(connection); + } + + @Override + public void onPeerAddressAuthenticated(Address peerAddress, Connection connection) { + NetworkNode.this.onPeerAddressAuthenticated(peerAddress, connection); + } + + @Override + public void onDisconnect(Reason reason, Connection connection) { + log.trace("onDisconnect at outgoing connection to peerAddress " + peerAddress); + NetworkNode.this.onDisconnect(reason, connection); + } + + @Override + public void onError(Throwable throwable) { + NetworkNode.this.onError(throwable); + } + }); + outBoundConnections.put(peerAddress, connection); + + log.info("\n\nNetworkNode created new outbound connection:" + + "\npeerAddress=" + peerAddress.port + + "\nconnection.uid=" + connection.getUid() + + "\nmessage=" + message + + "\n\n"); + } catch (Throwable t) { + resultFuture.setException(t); + return null; + } + } + + connection.sendMessage(message); + + return connection; + }; + + ListenableFuture future = executorService.submit(task); + Futures.addCallback(future, new FutureCallback() { + public void onSuccess(Connection connection) { + resultFuture.set(connection); + } + + public void onFailure(@NotNull Throwable throwable) { + resultFuture.setException(throwable); + } + }); + return resultFuture; + } + + public SettableFuture sendMessage(Connection connection, Message message) { + final SettableFuture resultFuture = SettableFuture.create(); + + ListenableFuture future = executorService.submit(() -> { + connection.sendMessage(message); + return connection; + }); + Futures.addCallback(future, new FutureCallback() { + public void onSuccess(Connection connection) { + resultFuture.set(connection); + } + + public void onFailure(@NotNull Throwable throwable) { + resultFuture.setException(throwable); + } + }); + return resultFuture; + } + + public Set getAllConnections() { + Set set = new HashSet<>(inBoundAuthenticatedConnections.values()); + set.addAll(outBoundConnections.values()); + set.addAll(inBoundTempConnections); + return set; + } + + public void shutDown(Runnable shutDownCompleteHandler) { + log.info("Shutdown NetworkNode"); + if (!shutDownInProgress) { + shutDownInProgress = true; + if (server != null) { + server.shutDown(); + server = null; + } + + getAllConnections().stream().forEach(e -> e.shutDown()); + + log.info("NetworkNode shutdown complete"); + if (shutDownCompleteHandler != null) new Thread(shutDownCompleteHandler).start(); + ; + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // SetupListener + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addSetupListener(SetupListener setupListener) { + setupListeners.add(setupListener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // ConnectionListener + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addConnectionListener(ConnectionListener connectionListener) { + connectionListeners.add(connectionListener); + } + + public void removeConnectionListener(ConnectionListener connectionListener) { + connectionListeners.remove(connectionListener); + } + + @Override + public void onConnection(Connection connection) { + connectionListeners.stream().forEach(e -> e.onConnection(connection)); + } + + @Override + public void onPeerAddressAuthenticated(Address peerAddress, Connection connection) { + log.trace("onAuthenticationComplete peerAddress=" + peerAddress); + log.trace("onAuthenticationComplete connection=" + connection); + + connectionListeners.stream().forEach(e -> e.onPeerAddressAuthenticated(peerAddress, connection)); + } + + @Override + public void onDisconnect(Reason reason, Connection connection) { + Address peerAddress = connection.getPeerAddress(); + log.trace("onDisconnect connection " + connection + ", peerAddress= " + peerAddress); + if (peerAddress != null) { + inBoundAuthenticatedConnections.remove(peerAddress); + outBoundConnections.remove(peerAddress); + } else { + // try to find if we have connection + outBoundConnections.values().stream() + .filter(e -> e.equals(connection)) + .findAny() + .ifPresent(e -> outBoundConnections.remove(e.getPeerAddress())); + inBoundAuthenticatedConnections.values().stream() + .filter(e -> e.equals(connection)) + .findAny() + .ifPresent(e -> inBoundAuthenticatedConnections.remove(e.getPeerAddress())); + } + inBoundTempConnections.remove(connection); + + connectionListeners.stream().forEach(e -> e.onDisconnect(reason, connection)); + } + + @Override + public void onError(Throwable throwable) { + connectionListeners.stream().forEach(e -> e.onError(throwable)); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // MessageListener + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addMessageListener(MessageListener messageListener) { + messageListeners.add(messageListener); + } + + public void removeMessageListener(MessageListener messageListener) { + messageListeners.remove(messageListener); + } + + @Override + public void onMessage(Message message, Connection connection) { + messageListeners.stream().forEach(e -> e.onMessage(message, connection)); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + protected void startServer(ServerSocket serverSocket) { + server = new Server(serverSocket, (message, connection) -> { + NetworkNode.this.onMessage(message, connection); + }, new ConnectionListener() { + @Override + public void onConnection(Connection connection) { + // we still have not authenticated so put it to the temp list + inBoundTempConnections.add(connection); + NetworkNode.this.onConnection(connection); + } + + @Override + public void onPeerAddressAuthenticated(Address peerAddress, Connection connection) { + NetworkNode.this.onPeerAddressAuthenticated(peerAddress, connection); + // now we know the the peers address is correct and we add it to inBoundConnections and + // remove it from tempConnections + inBoundAuthenticatedConnections.put(peerAddress, connection); + inBoundTempConnections.remove(connection); + } + + @Override + public void onDisconnect(Reason reason, Connection connection) { + Address peerAddress = connection.getPeerAddress(); + log.trace("onDisconnect at incoming connection to peerAddress " + peerAddress); + if (peerAddress != null) + inBoundAuthenticatedConnections.remove(peerAddress); + + inBoundTempConnections.remove(connection); + + NetworkNode.this.onDisconnect(reason, connection); + } + + @Override + public void onError(Throwable throwable) { + NetworkNode.this.onError(throwable); + } + }); + executorService.submit(server); + } + + abstract protected Socket getSocket(Address peerAddress) throws IOException; + + @Nullable + abstract public Address getAddress(); +} diff --git a/network/src/main/java/io/bitsquare/p2p/network/Server.java b/network/src/main/java/io/bitsquare/p2p/network/Server.java new file mode 100644 index 0000000000..947c3ca8fa --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/network/Server.java @@ -0,0 +1,75 @@ +package io.bitsquare.p2p.network; + +import io.bitsquare.p2p.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class Server implements Runnable { + private static final Logger log = LoggerFactory.getLogger(Server.class); + + private final ServerSocket serverSocket; + private final MessageListener messageListener; + private final ConnectionListener connectionListener; + private final ExecutorService executorService = Executors.newCachedThreadPool(); + private final List connections = new CopyOnWriteArrayList<>(); + private volatile boolean stopped; + + + public Server(ServerSocket serverSocket, MessageListener messageListener, ConnectionListener connectionListener) { + this.serverSocket = serverSocket; + this.messageListener = messageListener; + this.connectionListener = connectionListener; + } + + @Override + public void run() { + Thread.currentThread().setName("Server-" + serverSocket.getLocalPort()); + while (!stopped) { + try { + log.info("Ready to accept new clients on port " + serverSocket.getLocalPort()); + final Socket socket = serverSocket.accept(); + log.info("Accepted new client on port " + socket.getLocalPort()); + Connection connection = new Connection(socket, messageListener, connectionListener); + log.info("\n\nServer created new inbound connection:" + + "\nserverSocket.getLocalPort()=" + serverSocket.getLocalPort() + + "\nsocket.getPort()=" + socket.getPort() + + "\nconnection.uid=" + connection.getUid() + + "\n\n"); + + log.info("Server created new socket with port " + socket.getPort()); + connections.add(connection); + } catch (IOException e) { + if (!stopped) + e.printStackTrace(); + } + } + } + + public void shutDown() { + if (!stopped) { + stopped = true; + + connections.stream().forEach(e -> e.shutDown()); + + try { + serverSocket.close(); + } catch (SocketException e) { + log.warn("SocketException at shutdown might be expected " + e.getMessage()); + } catch (IOException e) { + e.printStackTrace(); + } finally { + Utils.shutDownExecutorService(executorService); + log.debug("Server shutdown complete"); + } + } + } +} diff --git a/network/src/main/java/io/bitsquare/p2p/network/ServerListener.java b/network/src/main/java/io/bitsquare/p2p/network/ServerListener.java new file mode 100644 index 0000000000..92e35c701b --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/network/ServerListener.java @@ -0,0 +1,5 @@ +package io.bitsquare.p2p.network; + +public interface ServerListener { + void onSocketHandler(Connection connection); +} diff --git a/network/src/main/java/io/bitsquare/p2p/network/SetupListener.java b/network/src/main/java/io/bitsquare/p2p/network/SetupListener.java new file mode 100644 index 0000000000..9a1acedc05 --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/network/SetupListener.java @@ -0,0 +1,12 @@ +package io.bitsquare.p2p.network; + + +public interface SetupListener { + + void onTorNodeReady(); + + void onHiddenServiceReady(); + + void onSetupFailed(Throwable throwable); + +} diff --git a/network/src/main/java/io/bitsquare/p2p/network/TorNetworkNode.java b/network/src/main/java/io/bitsquare/p2p/network/TorNetworkNode.java new file mode 100644 index 0000000000..40bbde28fd --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/network/TorNetworkNode.java @@ -0,0 +1,369 @@ +package io.bitsquare.p2p.network; + +import com.google.common.util.concurrent.*; +import com.msopentech.thali.java.toronionproxy.JavaOnionProxyContext; +import com.msopentech.thali.java.toronionproxy.JavaOnionProxyManager; +import io.bitsquare.p2p.Address; +import io.bitsquare.p2p.Message; +import io.bitsquare.p2p.Utils; +import io.bitsquare.p2p.network.messages.SelfTestMessage; +import io.nucleo.net.HiddenServiceDescriptor; +import io.nucleo.net.TorNode; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.net.Socket; +import java.util.Random; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +public class TorNetworkNode extends NetworkNode { + private static final Logger log = LoggerFactory.getLogger(TorNetworkNode.class); + private static final Random random = new Random(); + + private static final long TIMEOUT = 5000; + private static final long SELF_TEST_INTERVAL = 10 * 60 * 1000; + private static final int MAX_ERRORS_BEFORE_RESTART = 3; + private static final int MAX_RESTART_ATTEMPTS = 3; + private static final int WAIT_BEFORE_RESTART = 2000; + private static final long SHUT_DOWN_TIMEOUT = 5000; + + private final File torDir; + private TorNode torNode; + private HiddenServiceDescriptor hiddenServiceDescriptor; + private Timer shutDownTimeoutTimer, selfTestTimer, selfTestTimeoutTimer; + private TimerTask selfTestTimeoutTask, selfTestTask; + private AtomicBoolean selfTestRunning = new AtomicBoolean(false); + private int nonce; + private int errorCounter; + private int restartCounter; + private Runnable shutDownCompleteHandler; + private boolean torShutDownComplete, networkNodeShutDownDoneComplete; + + + // ///////////////////////////////////////////////////////////////////////////////////////// + // Constructor + // ///////////////////////////////////////////////////////////////////////////////////////// + + public TorNetworkNode(int port, File torDir) { + super(port); + + this.torDir = torDir; + + selfTestTimeoutTask = new TimerTask() { + @Override + public void run() { + log.error("A timeout occurred at self test"); + stopSelfTestTimer(); + selfTestFailed(); + } + }; + + selfTestTask = new TimerTask() { + @Override + public void run() { + stopTimeoutTimer(); + if (selfTestRunning.get()) { + log.debug("running self test"); + selfTestTimeoutTimer = new Timer(); + selfTestTimeoutTimer.schedule(selfTestTimeoutTask, TIMEOUT); + // might be interrupted by timeout task + if (selfTestRunning.get()) { + nonce = random.nextInt(); + log.trace("send msg with nonce " + nonce); + + try { + SettableFuture future = sendMessage(new Address(hiddenServiceDescriptor.getFullAddress()), new SelfTestMessage(nonce)); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(Connection connection) { + log.trace("Sending self test message succeeded"); + } + + @Override + public void onFailure(Throwable throwable) { + log.error("Error at sending self test message. Exception = " + throwable); + stopTimeoutTimer(); + throwable.printStackTrace(); + selfTestFailed(); + } + }); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } + }; + + addMessageListener(new MessageListener() { + @Override + public void onMessage(Message message, Connection connection) { + if (message instanceof SelfTestMessage) { + if (((SelfTestMessage) message).nonce == nonce) { + runSelfTest(); + } else { + log.error("Nonce not matching our challenge. That should never happen."); + selfTestFailed(); + } + } + } + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void start(@Nullable SetupListener setupListener) { + if (setupListener != null) addSetupListener(setupListener); + + // executorService might have been shutdown before a restart, so we create a new one + executorService = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool()); + + // Create the tor node (takes about 6 sec.) + createTorNode(torDir, torNode -> { + TorNetworkNode.this.torNode = torNode; + + setupListeners.stream().forEach(e -> e.onTorNodeReady()); + + // Create Hidden Service (takes about 40 sec.) + createHiddenService(torNode, port, hiddenServiceDescriptor -> { + TorNetworkNode.this.hiddenServiceDescriptor = hiddenServiceDescriptor; + + startServer(hiddenServiceDescriptor.getServerSocket()); + try { + Thread.sleep(500); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + + setupListeners.stream().forEach(e -> e.onHiddenServiceReady()); + + // we are ready. so we start our periodic self test if our HS is available + // startSelfTest(); + }); + }); + } + + @Override + @Nullable + public Address getAddress() { + if (hiddenServiceDescriptor != null) + return new Address(hiddenServiceDescriptor.getFullAddress()); + else + return null; + } + + public void shutDown(Runnable shutDownCompleteHandler) { + log.info("Shutdown TorNetworkNode"); + this.shutDownCompleteHandler = shutDownCompleteHandler; + checkNotNull(executorService, "executorService must not be null"); + + selfTestRunning.set(false); + stopSelfTestTimer(); + + shutDownTimeoutTimer = new Timer(); + shutDownTimeoutTimer.schedule(new TimerTask() { + @Override + public void run() { + log.error("A timeout occurred at shutDown"); + shutDownExecutorService(); + } + }, SHUT_DOWN_TIMEOUT); + + executorService.submit(() -> super.shutDown(() -> { + networkNodeShutDownDoneComplete = true; + if (torShutDownComplete) + shutDownExecutorService(); + })); + + ListenableFuture future2 = executorService.submit(() -> { + long ts = System.currentTimeMillis(); + log.info("Shutdown torNode"); + try { + if (torNode != null) + torNode.shutdown(); + log.info("Shutdown torNode done after " + (System.currentTimeMillis() - ts) + " ms."); + } catch (IOException e) { + e.printStackTrace(); + log.error("Shutdown torNode failed with exception: " + e.getMessage()); + shutDownExecutorService(); + } + }); + Futures.addCallback(future2, new FutureCallback() { + @Override + public void onSuccess(Object o) { + torShutDownComplete = true; + if (networkNodeShutDownDoneComplete) + shutDownExecutorService(); + } + + @Override + public void onFailure(Throwable throwable) { + throwable.printStackTrace(); + log.error("Shutdown torNode failed with exception: " + throwable.getMessage()); + shutDownExecutorService(); + } + }); + } + + // ///////////////////////////////////////////////////////////////////////////////////////// + // shutdown, restart + // ///////////////////////////////////////////////////////////////////////////////////////// + + private void shutDownExecutorService() { + shutDownTimeoutTimer.cancel(); + ListenableFuture future = executorService.submit(() -> { + long ts = System.currentTimeMillis(); + log.info("Shutdown executorService"); + Utils.shutDownExecutorService(executorService); + log.info("Shutdown executorService done after " + (System.currentTimeMillis() - ts) + " ms."); + }); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(Object o) { + log.info("Shutdown completed"); + new Thread(shutDownCompleteHandler).start(); + } + + @Override + public void onFailure(Throwable throwable) { + throwable.printStackTrace(); + log.error("Shutdown executorService failed with exception: " + throwable.getMessage()); + new Thread(shutDownCompleteHandler).start(); + } + }); + } + + private void restartTor() { + restartCounter++; + if (restartCounter <= MAX_RESTART_ATTEMPTS) { + shutDown(() -> { + try { + Thread.sleep(WAIT_BEFORE_RESTART); + } catch (InterruptedException e) { + e.printStackTrace(); + Thread.currentThread().interrupt(); + } + log.warn("We restart tor as too many self tests failed."); + start(null); + }); + } else { + log.error("We tried to restart tor " + restartCounter + + " times, but we failed to get tor running. We give up now."); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // create tor + /////////////////////////////////////////////////////////////////////////////////////////// + + private void createTorNode(final File torDir, final Consumer resultHandler) { + Callable> task = () -> { + long ts = System.currentTimeMillis(); + if (torDir.mkdirs()) + log.trace("Created directory for tor"); + + log.trace("Create TorNode"); + TorNode torNode1 = new TorNode( + torDir) { + }; + log.trace("\n\n##### TorNode created. Took " + (System.currentTimeMillis() - ts) + " ms\n\n"); + return torNode1; + }; + ListenableFuture> future = executorService.submit(task); + Futures.addCallback(future, new FutureCallback>() { + public void onSuccess(TorNode torNode) { + resultHandler.accept(torNode); + } + + public void onFailure(Throwable throwable) { + log.error("TorNode creation failed"); + restartTor(); + } + }); + } + + private void createHiddenService(final TorNode torNode, final int port, + final Consumer resultHandler) { + Callable task = () -> { + long ts = System.currentTimeMillis(); + log.debug("Create hidden service"); + HiddenServiceDescriptor hiddenServiceDescriptor = torNode.createHiddenService(port); + log.debug("\n\n##### Hidden service created. Address = " + hiddenServiceDescriptor.getFullAddress() + ". Took " + (System.currentTimeMillis() - ts) + " ms\n\n"); + + return hiddenServiceDescriptor; + }; + ListenableFuture future = executorService.submit(task); + Futures.addCallback(future, new FutureCallback() { + public void onSuccess(HiddenServiceDescriptor hiddenServiceDescriptor) { + resultHandler.accept(hiddenServiceDescriptor); + } + + public void onFailure(Throwable throwable) { + log.error("Hidden service creation failed"); + restartTor(); + } + }); + } + + + // ///////////////////////////////////////////////////////////////////////////////////////// + // Self test + // ///////////////////////////////////////////////////////////////////////////////////////// + + private void startSelfTest() { + selfTestRunning.set(true); + //addListener(messageListener); + runSelfTest(); + } + + private void runSelfTest() { + stopSelfTestTimer(); + selfTestTimer = new Timer(); + selfTestTimer.schedule(selfTestTask, SELF_TEST_INTERVAL); + } + + private void stopSelfTestTimer() { + stopTimeoutTimer(); + if (selfTestTimer != null) + selfTestTimer.cancel(); + } + + private void stopTimeoutTimer() { + if (selfTestTimeoutTimer != null) + selfTestTimeoutTimer.cancel(); + } + + private void selfTestFailed() { + errorCounter++; + log.warn("Self test failed. Already " + errorCounter + " failure(s). Max. errors before restart: " + + MAX_ERRORS_BEFORE_RESTART); + if (errorCounter >= MAX_ERRORS_BEFORE_RESTART) + restartTor(); + else + runSelfTest(); + } + + @Override + protected Socket getSocket(Address peerAddress) throws IOException { + checkArgument(peerAddress.hostName.endsWith(".onion"), "PeerAddress is not an onion address"); + + return torNode.connectToHiddenService(peerAddress.hostName, peerAddress.port); + } + + +} diff --git a/network/src/main/java/io/bitsquare/p2p/network/messages/CloseConnectionMessage.java b/network/src/main/java/io/bitsquare/p2p/network/messages/CloseConnectionMessage.java new file mode 100644 index 0000000000..0fdad48a5c --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/network/messages/CloseConnectionMessage.java @@ -0,0 +1,10 @@ +package io.bitsquare.p2p.network.messages; + +import io.bitsquare.app.Version; +import io.bitsquare.p2p.Message; + +public final class CloseConnectionMessage implements Message { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + +} diff --git a/network/src/main/java/io/bitsquare/p2p/network/messages/SelfTestMessage.java b/network/src/main/java/io/bitsquare/p2p/network/messages/SelfTestMessage.java new file mode 100644 index 0000000000..d81d5fb722 --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/network/messages/SelfTestMessage.java @@ -0,0 +1,15 @@ +package io.bitsquare.p2p.network.messages; + +import io.bitsquare.app.Version; +import io.bitsquare.p2p.Message; + +public final class SelfTestMessage implements Message { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + + public final Integer nonce; + + public SelfTestMessage(Integer nonce) { + this.nonce = nonce; + } +} diff --git a/network/src/main/java/io/bitsquare/p2p/routing/AuthenticationListener.java b/network/src/main/java/io/bitsquare/p2p/routing/AuthenticationListener.java new file mode 100644 index 0000000000..989fbb8ea8 --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/routing/AuthenticationListener.java @@ -0,0 +1,17 @@ +package io.bitsquare.p2p.routing; + +import io.bitsquare.p2p.Address; +import io.bitsquare.p2p.network.Connection; + +public abstract class AuthenticationListener implements RoutingListener { + public void onFirstNeighborAdded(Neighbor neighbor) { + } + + public void onNeighborAdded(Neighbor neighbor) { + } + + public void onNeighborRemoved(Address address) { + } + + abstract public void onConnectionAuthenticated(Connection connection); +} diff --git a/network/src/main/java/io/bitsquare/p2p/routing/Neighbor.java b/network/src/main/java/io/bitsquare/p2p/routing/Neighbor.java new file mode 100644 index 0000000000..804710ea22 --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/routing/Neighbor.java @@ -0,0 +1,49 @@ +package io.bitsquare.p2p.routing; + +import io.bitsquare.p2p.Address; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Serializable; + +public class Neighbor implements Serializable { + private static final Logger log = LoggerFactory.getLogger(Neighbor.class); + + public final Address address; + private int pingNonce; + + public Neighbor(Address address) { + this.address = address; + } + + public void setPingNonce(int pingNonce) { + this.pingNonce = pingNonce; + } + + public int getPingNonce() { + return pingNonce; + } + + @Override + public int hashCode() { + return address != null ? address.hashCode() : 0; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Neighbor)) return false; + + Neighbor neighbor = (Neighbor) o; + + return !(address != null ? !address.equals(neighbor.address) : neighbor.address != null); + } + + @Override + public String toString() { + return "Neighbor{" + + "address=" + address + + ", pingNonce=" + pingNonce + + '}'; + } +} diff --git a/network/src/main/java/io/bitsquare/p2p/routing/Routing.java b/network/src/main/java/io/bitsquare/p2p/routing/Routing.java new file mode 100644 index 0000000000..0f051790ba --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/routing/Routing.java @@ -0,0 +1,592 @@ +package io.bitsquare.p2p.routing; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.SettableFuture; +import io.bitsquare.common.UserThread; +import io.bitsquare.p2p.Address; +import io.bitsquare.p2p.Utils; +import io.bitsquare.p2p.network.*; +import io.bitsquare.p2p.routing.messages.*; +import io.bitsquare.p2p.storage.messages.BroadcastMessage; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; + +public class Routing { + private static final Logger log = LoggerFactory.getLogger(Routing.class); + + private static int MAX_CONNECTIONS = 8; + private long startAuthTs; + + public static void setMaxConnections(int maxConnections) { + MAX_CONNECTIONS = maxConnections; + } + + private final NetworkNode networkNode; + private final List
seedNodes; + private final Map nonceMap = new ConcurrentHashMap<>(); + private final List routingListeners = new CopyOnWriteArrayList<>(); + private final Map connectedNeighbors = new ConcurrentHashMap<>(); + private final Map reportedNeighbors = new ConcurrentHashMap<>(); + private final Map authenticationCompleteHandlers = new ConcurrentHashMap<>(); + private final Timer maintenanceTimer = new Timer(); + private final ExecutorService executorService = Executors.newCachedThreadPool(); + private volatile boolean shutDownInProgress; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public Routing(final NetworkNode networkNode, List
seeds) { + this.networkNode = networkNode; + + // We copy it as we remove ourselves later from the list if we are a seed node + this.seedNodes = new CopyOnWriteArrayList<>(seeds); + + networkNode.addMessageListener((message, connection) -> { + if (message instanceof AuthenticationMessage) + processAuthenticationMessage((AuthenticationMessage) message, connection); + else if (message instanceof MaintenanceMessage) + processMaintenanceMessage((MaintenanceMessage) message, connection); + }); + + networkNode.addConnectionListener(new ConnectionListener() { + @Override + public void onConnection(Connection connection) { + } + + @Override + public void onPeerAddressAuthenticated(Address peerAddress, Connection connection) { + } + + @Override + public void onDisconnect(Reason reason, Connection connection) { + // only removes authenticated nodes + if (connection.getPeerAddress() != null) + removeNeighbor(connection.getPeerAddress()); + } + + @Override + public void onError(Throwable throwable) { + } + }); + + networkNode.addSetupListener(new SetupListener() { + @Override + public void onTorNodeReady() { + } + + @Override + public void onHiddenServiceReady() { + // remove ourselves in case we are a seed node + Address myAddress = getAddress(); + if (myAddress != null) + seedNodes.remove(myAddress); + } + + @Override + public void onSetupFailed(Throwable throwable) { + } + }); + + int maintenanceInterval = new Random().nextInt(15 * 60 * 1000) + 15 * 60 * 1000; // 15-30 min. + maintenanceTimer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + disconnectOldConnections(); + pingNeighbors(); + } + }, maintenanceInterval, maintenanceInterval); + } + + private void disconnectOldConnections() { + List authenticatedConnections = networkNode.getAllConnections().stream() + .filter(e -> e.isAuthenticated()) + .collect(Collectors.toList()); + if (authenticatedConnections.size() > MAX_CONNECTIONS) { + authenticatedConnections.sort((o1, o2) -> o1.getLastActivityDate().compareTo(o2.getLastActivityDate())); + log.info("Number of connections exceeds MAX_CONNECTIONS. Current size=" + authenticatedConnections.size()); + Connection connection = authenticatedConnections.remove(0); + log.info("Shutdown oldest connection with last activity date=" + connection.getLastActivityDate() + " / connection=" + connection); + connection.shutDown(() -> disconnectOldConnections()); + try { + Thread.sleep(200); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + } + } + + private void pingNeighbors() { + + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void shutDown() { + if (!shutDownInProgress) { + shutDownInProgress = true; + if (maintenanceTimer != null) + maintenanceTimer.cancel(); + + Utils.shutDownExecutorService(executorService); + } + } + + public void broadcast(BroadcastMessage message, Address sender) { + log.trace("Broadcast message to " + connectedNeighbors.values().size() + " neighbors."); + connectedNeighbors.values().parallelStream() + .filter(e -> !e.address.equals(sender)) + .forEach(neighbor -> { + log.trace("Broadcast message " + message + " from " + getAddress() + " to " + neighbor.address + "."); + SettableFuture future = networkNode.sendMessage(neighbor.address, message); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(Connection connection) { + log.trace("Broadcast from " + getAddress() + " to " + neighbor.address + " succeeded."); + } + + @Override + public void onFailure(@NotNull Throwable throwable) { + log.info("Broadcast failed. " + throwable.getMessage()); + removeNeighbor(neighbor.address); + } + }); + }); + } + + public void addMessageListener(MessageListener messageListener) { + networkNode.addMessageListener(messageListener); + } + + public void removeMessageListener(MessageListener messageListener) { + networkNode.removeMessageListener(messageListener); + } + + public void addRoutingListener(RoutingListener routingListener) { + routingListeners.add(routingListener); + } + + public void removeRoutingListener(RoutingListener routingListener) { + routingListeners.remove(routingListener); + } + + public Map getReportedNeighbors() { + return reportedNeighbors; + } + + public Map getConnectedNeighbors() { + return connectedNeighbors; + } + + public Map getAllNeighbors() { + Map hashMap = new ConcurrentHashMap<>(reportedNeighbors); + hashMap.putAll(connectedNeighbors); + // remove own address and seed nodes + hashMap.remove(getAddress()); + return hashMap; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Authentication + /////////////////////////////////////////////////////////////////////////////////////////// + + // authentication example: + // node2 -> node1 RequestAuthenticationMessage + // node1: close connection + // node1 -> node2 ChallengeMessage on new connection + // node2: authentication to node1 done if nonce ok + // node2 -> node1 GetNeighborsMessage + // node1: authentication to node2 done if nonce ok + // node1 -> node2 NeighborsMessage + + public void startAuthentication(List
connectedSeedNodes) { + connectedSeedNodes.forEach(connectedSeedNode -> { + executorService.submit(() -> { + sendRequestAuthenticationMessage(seedNodes, connectedSeedNode); + try { + // give a random pause of 3-5 sec. before using the next + Thread.sleep(new Random().nextInt(2000) + 3000); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + }); + }); + + } + + private void sendRequestAuthenticationMessage(final List
remainingSeedNodes, final Address address) { + log.info("We try to authenticate to a random seed node. " + address); + startAuthTs = System.currentTimeMillis(); + final boolean[] alreadyConnected = {false}; + connectedNeighbors.values().stream().forEach(e -> { + remainingSeedNodes.remove(e.address); + if (address.equals(e.address)) + alreadyConnected[0] = true; + }); + if (!alreadyConnected[0]) { + int nonce = addToMapAndGetNonce(address); + SettableFuture future = networkNode.sendMessage(address, new RequestAuthenticationMessage(getAddress(), nonce)); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable Connection connection) { + log.info("send RequestAuthenticationMessage to " + address + " succeeded."); + } + + @Override + public void onFailure(@NotNull Throwable throwable) { + log.info("Send RequestAuthenticationMessage to " + address + " failed. Exception:" + throwable.getMessage()); + log.trace("We try to authenticate to another random seed nodes of that list: " + remainingSeedNodes); + getNextSeedNode(remainingSeedNodes); + } + }); + } else { + getNextSeedNode(remainingSeedNodes); + } + } + + private void getNextSeedNode(List
remainingSeedNodes) { + List
remainingSeedNodeAddresses = new CopyOnWriteArrayList<>(remainingSeedNodes); + + Address myAddress = getAddress(); + if (myAddress != null) + remainingSeedNodeAddresses.remove(myAddress); + + if (!remainingSeedNodeAddresses.isEmpty()) { + Collections.shuffle(remainingSeedNodeAddresses); + Address address = remainingSeedNodeAddresses.remove(0); + sendRequestAuthenticationMessage(remainingSeedNodeAddresses, address); + } else { + log.info("No other seed node found. That is expected for the first seed node."); + } + } + + + private void processAuthenticationMessage(AuthenticationMessage message, Connection connection) { + log.trace("processAuthenticationMessage " + message + " from " + connection.getPeerAddress() + " at " + getAddress()); + if (message instanceof RequestAuthenticationMessage) { + RequestAuthenticationMessage requestAuthenticationMessage = (RequestAuthenticationMessage) message; + Address peerAddress = requestAuthenticationMessage.address; + log.trace("RequestAuthenticationMessage from " + peerAddress + " at " + getAddress()); + connection.shutDown(() -> { + // we delay a bit as listeners for connection.onDisconnect are on other threads and might lead to + // inconsistent state (removal of connection from NetworkNode.authenticatedConnections) + try { + Thread.sleep(100); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + + log.trace("processAuthenticationMessage: connection.shutDown complete. RequestAuthenticationMessage from " + peerAddress + " at " + getAddress()); + int nonce = addToMapAndGetNonce(peerAddress); + SettableFuture future = networkNode.sendMessage(peerAddress, new ChallengeMessage(getAddress(), requestAuthenticationMessage.nonce, nonce)); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(Connection connection) { + log.debug("onSuccess "); + + // TODO check nr. of connections, remove older connections (?) + } + + @Override + public void onFailure(Throwable throwable) { + log.debug("onFailure "); + // TODO skip to next node or retry? + SettableFuture future = networkNode.sendMessage(peerAddress, new ChallengeMessage(getAddress(), requestAuthenticationMessage.nonce, nonce)); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(Connection connection) { + log.debug("onSuccess "); + } + + @Override + public void onFailure(Throwable throwable) { + log.debug("onFailure "); + } + }); + } + }); + }); + } else if (message instanceof ChallengeMessage) { + ChallengeMessage challengeMessage = (ChallengeMessage) message; + Address peerAddress = challengeMessage.address; + connection.setPeerAddress(peerAddress); + log.trace("ChallengeMessage from " + peerAddress + " at " + getAddress()); + boolean verified = verifyNonceAndAuthenticatePeerAddress(challengeMessage.requesterNonce, peerAddress); + if (verified) { + HashMap allNeighbors = new HashMap<>(getAllNeighbors()); + SettableFuture future = networkNode.sendMessage(peerAddress, new GetNeighborsMessage(getAddress(), challengeMessage.challengerNonce, allNeighbors)); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(Connection connection) { + log.trace("GetNeighborsMessage sent successfully from " + getAddress() + " to " + peerAddress); + + // we wait to get the success to reduce the time span of the moment of + // authentication at both sides of the connection + setAuthenticated(connection, peerAddress); + } + + @Override + public void onFailure(@NotNull Throwable throwable) { + log.info("GetNeighborsMessage sending failed " + throwable.getMessage()); + removeNeighbor(peerAddress); + } + }); + } + } else if (message instanceof GetNeighborsMessage) { + GetNeighborsMessage getNeighborsMessage = (GetNeighborsMessage) message; + Address peerAddress = getNeighborsMessage.address; + log.trace("GetNeighborsMessage from " + peerAddress + " at " + getAddress()); + boolean verified = verifyNonceAndAuthenticatePeerAddress(getNeighborsMessage.challengerNonce, peerAddress); + if (verified) { + setAuthenticated(connection, peerAddress); + purgeReportedNeighbors(); + HashMap allNeighbors = new HashMap<>(getAllNeighbors()); + SettableFuture future = networkNode.sendMessage(peerAddress, new NeighborsMessage(allNeighbors)); + log.trace("sent NeighborsMessage to " + peerAddress + " from " + getAddress() + " with allNeighbors=" + allNeighbors.values()); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(Connection connection) { + log.trace("NeighborsMessage sent successfully from " + getAddress() + " to " + peerAddress); + } + + @Override + public void onFailure(@NotNull Throwable throwable) { + log.info("NeighborsMessage sending failed " + throwable.getMessage()); + removeNeighbor(peerAddress); + } + }); + + // now we add the reported neighbors to our own set + final HashMap neighbors = ((GetNeighborsMessage) message).neighbors; + log.trace("Received neighbors: " + neighbors); + // remove ourselves + neighbors.remove(getAddress()); + addToReportedNeighbors(neighbors, connection); + } + } else if (message instanceof NeighborsMessage) { + log.trace("NeighborsMessage from " + connection.getPeerAddress() + " at " + getAddress()); + final HashMap neighbors = ((NeighborsMessage) message).neighbors; + log.trace("Received neighbors: " + neighbors); + // remove ourselves + neighbors.remove(getAddress()); + addToReportedNeighbors(neighbors, connection); + + log.info("\n\nAuthenticationComplete\nPeer with address " + connection.getPeerAddress().toString() + + " authenticated (" + connection.getObjectId() + "). Took " + + (System.currentTimeMillis() - startAuthTs) + " ms. \n\n"); + + Runnable authenticationCompleteHandler = authenticationCompleteHandlers.remove(connection.getPeerAddress()); + if (authenticationCompleteHandler != null) + authenticationCompleteHandler.run(); + + authenticateToNextRandomNeighbor(); + } + } + + private void addToReportedNeighbors(HashMap neighbors, Connection connection) { + // we disconnect misbehaving nodes trying to send too many neighbors + // reported neighbors include the peers connected neighbors which is normally max. 8 but we give some headroom + // for safety + if (neighbors.size() > 1100) { + connection.shutDown(); + } else { + reportedNeighbors.putAll(neighbors); + purgeReportedNeighbors(); + } + } + + private void purgeReportedNeighbors() { + int all = getAllNeighbors().size(); + if (all > 1000) { + int diff = all - 100; + ArrayList reportedNeighborsList = new ArrayList<>(reportedNeighbors.values()); + for (int i = 0; i < diff; i++) { + Neighbor neighborToRemove = reportedNeighborsList.remove(new Random().nextInt(reportedNeighborsList.size())); + reportedNeighbors.remove(neighborToRemove.address); + } + } + } + + private void authenticateToNextRandomNeighbor() { + if (getConnectedNeighbors().size() <= MAX_CONNECTIONS) { + Neighbor randomNotConnectedNeighbor = getRandomNotConnectedNeighbor(); + if (randomNotConnectedNeighbor != null) { + log.info("We try to build an authenticated connection to a random neighbor. " + randomNotConnectedNeighbor); + authenticateToPeer(randomNotConnectedNeighbor.address, null, () -> authenticateToNextRandomNeighbor()); + } else { + log.info("No more neighbors available for connecting."); + } + } else { + log.info("We have already enough connections."); + } + } + + public void authenticateToPeer(Address address, @Nullable Runnable authenticationCompleteHandler, @Nullable Runnable faultHandler) { + startAuthTs = System.currentTimeMillis(); + + if (authenticationCompleteHandler != null) + authenticationCompleteHandlers.put(address, authenticationCompleteHandler); + + int nonce = addToMapAndGetNonce(address); + SettableFuture future = networkNode.sendMessage(address, new RequestAuthenticationMessage(getAddress(), nonce)); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(@Nullable Connection connection) { + log.debug("send RequestAuthenticationMessage succeeded"); + } + + @Override + public void onFailure(@NotNull Throwable throwable) { + log.info("send IdMessage failed. " + throwable.getMessage()); + removeNeighbor(address); + if (faultHandler != null) faultHandler.run(); + } + }); + } + + private int addToMapAndGetNonce(Address address) { + int nonce = new Random().nextInt(); + while (nonce == 0) { + nonce = new Random().nextInt(); + } + nonceMap.put(address, nonce); + return nonce; + } + + private boolean verifyNonceAndAuthenticatePeerAddress(int peersNonce, Address peerAddress) { + int nonce = nonceMap.remove(peerAddress); + boolean result = nonce == peersNonce; + return result; + } + + private void setAuthenticated(Connection connection, Address address) { + log.info("We got the connection from " + getAddress() + " to " + address + " authenticated."); + Neighbor neighbor = new Neighbor(address); + addConnectedNeighbor(address, neighbor); + + connection.onAuthenticationComplete(address, connection); + routingListeners.stream().forEach(e -> e.onConnectionAuthenticated(connection)); + } + + private Neighbor getRandomNotConnectedNeighbor() { + List list = reportedNeighbors.values().stream() + .filter(e -> !connectedNeighbors.values().contains(e)) + .collect(Collectors.toList()); + if (list.size() > 0) { + Collections.shuffle(list); + return list.get(0); + } else { + return null; + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Maintenance + /////////////////////////////////////////////////////////////////////////////////////////// + + private void processMaintenanceMessage(MaintenanceMessage message, Connection connection) { + log.debug("Received routing message " + message + " at " + getAddress() + " from " + connection.getPeerAddress()); + if (message instanceof PingMessage) { + SettableFuture future = networkNode.sendMessage(connection, new PongMessage(((PingMessage) message).nonce)); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(Connection connection) { + log.trace("PongMessage sent successfully"); + } + + @Override + public void onFailure(@NotNull Throwable throwable) { + log.info("PongMessage sending failed " + throwable.getMessage()); + removeNeighbor(connection.getPeerAddress()); + } + }); + } else if (message instanceof PongMessage) { + Neighbor neighbor = connectedNeighbors.get(connection.getPeerAddress()); + if (neighbor != null) { + if (((PongMessage) message).nonce != neighbor.getPingNonce()) { + removeNeighbor(neighbor.address); + log.warn("PongMessage invalid: self/peer " + getAddress() + "/" + connection.getPeerAddress()); + } + } + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Neighbors + /////////////////////////////////////////////////////////////////////////////////////////// + + private void removeNeighbor(Address address) { + reportedNeighbors.remove(address); + + Neighbor disconnectedNeighbor; + disconnectedNeighbor = connectedNeighbors.remove(address); + + if (disconnectedNeighbor != null) + UserThread.execute(() -> routingListeners.stream().forEach(e -> e.onNeighborRemoved(address))); + + nonceMap.remove(address); + } + + private void addConnectedNeighbor(Address address, Neighbor neighbor) { + boolean firstNeighborAdded; + connectedNeighbors.put(address, neighbor); + firstNeighborAdded = connectedNeighbors.size() == 1; + + UserThread.execute(() -> routingListeners.stream().forEach(e -> e.onNeighborAdded(neighbor))); + + if (firstNeighborAdded) + UserThread.execute(() -> routingListeners.stream().forEach(e -> e.onFirstNeighborAdded(neighbor))); + + if (connectedNeighbors.size() > MAX_CONNECTIONS) + disconnectOldConnections(); + } + + private Address getAddress() { + return networkNode.getAddress(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////////////////////// + + public void printConnectedNeighborsMap() { + StringBuilder result = new StringBuilder("\nConnected neighbors for node " + getAddress() + ":"); + connectedNeighbors.values().stream().forEach(e -> { + result.append("\n\t" + e.address); + }); + result.append("\n"); + log.info(result.toString()); + } + + public void printReportedNeighborsMap() { + StringBuilder result = new StringBuilder("\nReported neighbors for node " + getAddress() + ":"); + reportedNeighbors.values().stream().forEach(e -> { + result.append("\n\t" + e.address); + }); + result.append("\n"); + log.info(result.toString()); + } + + private String getObjectId() { + return super.toString().split("@")[1].toString(); + } + +} diff --git a/network/src/main/java/io/bitsquare/p2p/routing/RoutingListener.java b/network/src/main/java/io/bitsquare/p2p/routing/RoutingListener.java new file mode 100644 index 0000000000..1cc9258dae --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/routing/RoutingListener.java @@ -0,0 +1,15 @@ +package io.bitsquare.p2p.routing; + +import io.bitsquare.p2p.Address; +import io.bitsquare.p2p.network.Connection; + +public interface RoutingListener { + void onFirstNeighborAdded(Neighbor neighbor); + + void onNeighborAdded(Neighbor neighbor); + + void onNeighborRemoved(Address address); + + // TODO remove + void onConnectionAuthenticated(Connection connection); +} diff --git a/network/src/main/java/io/bitsquare/p2p/routing/messages/AuthenticationMessage.java b/network/src/main/java/io/bitsquare/p2p/routing/messages/AuthenticationMessage.java new file mode 100644 index 0000000000..8448e6240e --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/routing/messages/AuthenticationMessage.java @@ -0,0 +1,6 @@ +package io.bitsquare.p2p.routing.messages; + +import io.bitsquare.p2p.Message; + +public interface AuthenticationMessage extends Message { +} diff --git a/network/src/main/java/io/bitsquare/p2p/routing/messages/ChallengeMessage.java b/network/src/main/java/io/bitsquare/p2p/routing/messages/ChallengeMessage.java new file mode 100644 index 0000000000..ce161b732d --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/routing/messages/ChallengeMessage.java @@ -0,0 +1,28 @@ +package io.bitsquare.p2p.routing.messages; + +import io.bitsquare.app.Version; +import io.bitsquare.p2p.Address; + +public final class ChallengeMessage implements AuthenticationMessage { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + + public final Address address; + public final int requesterNonce; + public final int challengerNonce; + + public ChallengeMessage(Address address, int requesterNonce, int challengerNonce) { + this.address = address; + this.requesterNonce = requesterNonce; + this.challengerNonce = challengerNonce; + } + + @Override + public String toString() { + return "ChallengeMessage{" + + "address=" + address + + ", requesterNonce=" + requesterNonce + + ", challengerNonce=" + challengerNonce + + '}'; + } +} diff --git a/network/src/main/java/io/bitsquare/p2p/routing/messages/GetNeighborsMessage.java b/network/src/main/java/io/bitsquare/p2p/routing/messages/GetNeighborsMessage.java new file mode 100644 index 0000000000..be41e03c2c --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/routing/messages/GetNeighborsMessage.java @@ -0,0 +1,31 @@ +package io.bitsquare.p2p.routing.messages; + +import io.bitsquare.app.Version; +import io.bitsquare.p2p.Address; +import io.bitsquare.p2p.routing.Neighbor; + +import java.util.HashMap; + +public final class GetNeighborsMessage implements AuthenticationMessage { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + + public final Address address; + public final int challengerNonce; + public final HashMap neighbors; + + public GetNeighborsMessage(Address address, int challengerNonce, HashMap neighbors) { + this.address = address; + this.challengerNonce = challengerNonce; + this.neighbors = neighbors; + } + + @Override + public String toString() { + return "GetNeighborsMessage{" + + "address=" + address + + ", challengerNonce=" + challengerNonce + + ", neighbors=" + neighbors + + '}'; + } +} diff --git a/network/src/main/java/io/bitsquare/p2p/routing/messages/MaintenanceMessage.java b/network/src/main/java/io/bitsquare/p2p/routing/messages/MaintenanceMessage.java new file mode 100644 index 0000000000..16c087fc9d --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/routing/messages/MaintenanceMessage.java @@ -0,0 +1,6 @@ +package io.bitsquare.p2p.routing.messages; + +import io.bitsquare.p2p.Message; + +public interface MaintenanceMessage extends Message { +} diff --git a/network/src/main/java/io/bitsquare/p2p/routing/messages/NeighborsMessage.java b/network/src/main/java/io/bitsquare/p2p/routing/messages/NeighborsMessage.java new file mode 100644 index 0000000000..2fd3927ad8 --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/routing/messages/NeighborsMessage.java @@ -0,0 +1,24 @@ +package io.bitsquare.p2p.routing.messages; + +import io.bitsquare.app.Version; +import io.bitsquare.p2p.Address; +import io.bitsquare.p2p.routing.Neighbor; + +import java.util.HashMap; + +public final class NeighborsMessage implements AuthenticationMessage { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + + public final HashMap neighbors; + + public NeighborsMessage(HashMap neighbors) { + this.neighbors = neighbors; + } + + @Override + public String toString() { + return "NeighborsMessage{" + "neighbors=" + neighbors + '}'; + } + +} diff --git a/network/src/main/java/io/bitsquare/p2p/routing/messages/PingMessage.java b/network/src/main/java/io/bitsquare/p2p/routing/messages/PingMessage.java new file mode 100644 index 0000000000..56d46b7b9f --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/routing/messages/PingMessage.java @@ -0,0 +1,21 @@ +package io.bitsquare.p2p.routing.messages; + +import io.bitsquare.app.Version; + +public final class PingMessage implements MaintenanceMessage { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + + public final int nonce; + + public PingMessage(int nonce) { + this.nonce = nonce; + } + + @Override + public String toString() { + return "PingMessage{" + + "nonce=" + nonce + + '}'; + } +} diff --git a/network/src/main/java/io/bitsquare/p2p/routing/messages/PongMessage.java b/network/src/main/java/io/bitsquare/p2p/routing/messages/PongMessage.java new file mode 100644 index 0000000000..628f15e50d --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/routing/messages/PongMessage.java @@ -0,0 +1,21 @@ +package io.bitsquare.p2p.routing.messages; + +import io.bitsquare.app.Version; + +public final class PongMessage implements MaintenanceMessage { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + + public final int nonce; + + public PongMessage(int nonce) { + this.nonce = nonce; + } + + @Override + public String toString() { + return "PongMessage{" + + "nonce=" + nonce + + '}'; + } +} diff --git a/network/src/main/java/io/bitsquare/p2p/routing/messages/RequestAuthenticationMessage.java b/network/src/main/java/io/bitsquare/p2p/routing/messages/RequestAuthenticationMessage.java new file mode 100644 index 0000000000..d299493787 --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/routing/messages/RequestAuthenticationMessage.java @@ -0,0 +1,25 @@ +package io.bitsquare.p2p.routing.messages; + +import io.bitsquare.app.Version; +import io.bitsquare.p2p.Address; + +public final class RequestAuthenticationMessage implements AuthenticationMessage { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + + public final Address address; + public final int nonce; + + public RequestAuthenticationMessage(Address address, int nonce) { + this.address = address; + this.nonce = nonce; + } + + @Override + public String toString() { + return "RequestAuthenticationMessage{" + + "address=" + address + + ", nonce=" + nonce + + '}'; + } +} diff --git a/network/src/main/java/io/bitsquare/p2p/seed/SeedNode.java b/network/src/main/java/io/bitsquare/p2p/seed/SeedNode.java new file mode 100644 index 0000000000..977fa1c512 --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/seed/SeedNode.java @@ -0,0 +1,110 @@ +package io.bitsquare.p2p.seed; + +import io.bitsquare.common.crypto.KeyRing; +import io.bitsquare.crypto.EncryptionService; +import io.bitsquare.p2p.Address; +import io.bitsquare.p2p.P2PService; +import io.bitsquare.p2p.P2PServiceListener; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.*; + +public class SeedNode { + private static final Logger log = LoggerFactory.getLogger(SeedNode.class); + + private int port = 8001; + private boolean useLocalhost = true; + private List
seedNodes; + private P2PService p2PService; + protected boolean stopped; + + public SeedNode() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + // args: port useLocalhost seedNodes + // eg. 4444 true localhost:7777 localhost:8888 + // To stop enter: q + public void processArgs(String[] args) { + if (args.length > 0) { + port = Integer.parseInt(args[0]); + + if (args.length > 1) { + useLocalhost = ("true").equals(args[1]); + + if (args.length > 2) { + seedNodes = new ArrayList<>(); + for (int i = 2; i < args.length; i++) { + seedNodes.add(new Address(args[i])); + } + } + } + } + } + + public void listenForExitCommand() { + Scanner scan = new Scanner(System.in); + String line; + while (!stopped && ((line = scan.nextLine()) != null)) { + if (line.equals("q")) { + Timer timeout = new Timer(); + timeout.schedule(new TimerTask() { + @Override + public void run() { + log.error("Timeout occurred at shutDown request"); + System.exit(1); + } + }, 10 * 1000); + + shutDown(() -> { + timeout.cancel(); + log.debug("Shutdown seed node complete."); + System.exit(0); + }); + } + } + } + + public void createAndStartP2PService() { + createAndStartP2PService(null, null, port, useLocalhost, seedNodes, null); + } + + public void createAndStartP2PService(EncryptionService encryptionService, KeyRing keyRing, int port, boolean useLocalhost, @Nullable List
seedNodes, @Nullable P2PServiceListener listener) { + SeedNodesRepository seedNodesRepository = new SeedNodesRepository(); + if (seedNodes != null && !seedNodes.isEmpty()) { + if (useLocalhost) + seedNodesRepository.setLocalhostSeedNodeAddresses(seedNodes); + else + seedNodesRepository.setTorSeedNodeAddresses(seedNodes); + } + + p2PService = new P2PService(seedNodesRepository, port, new File("bitsquare_seed_node_" + port), useLocalhost, encryptionService, keyRing); + p2PService.start(listener); + } + + public P2PService getP2PService() { + return p2PService; + } + + public void shutDown() { + shutDown(null); + } + + public void shutDown(@Nullable Runnable shutDownCompleteHandler) { + log.debug("Request shutdown seed node"); + if (!stopped) { + stopped = true; + + p2PService.shutDown(() -> { + if (shutDownCompleteHandler != null) new Thread(shutDownCompleteHandler).start(); + }); + } + } +} diff --git a/network/src/main/java/io/bitsquare/p2p/seed/SeedNodesRepository.java b/network/src/main/java/io/bitsquare/p2p/seed/SeedNodesRepository.java new file mode 100644 index 0000000000..8944b16bfb --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/seed/SeedNodesRepository.java @@ -0,0 +1,41 @@ +package io.bitsquare.p2p.seed; + +import io.bitsquare.p2p.Address; + +import java.util.Arrays; +import java.util.List; + +public class SeedNodesRepository { + + + protected List
torSeedNodeAddresses = Arrays.asList( + new Address("3anjm5mw2sr6abx6.onion:8001") + ); + + + protected List
localhostSeedNodeAddresses = Arrays.asList( + new Address("localhost:8001"), + new Address("localhost:8002"), + new Address("localhost:8003") + ); + + public List
getTorSeedNodeAddresses() { + return torSeedNodeAddresses; + } + + public List
geSeedNodeAddresses(boolean useLocalhost) { + return useLocalhost ? localhostSeedNodeAddresses : torSeedNodeAddresses; + } + + public List
getLocalhostSeedNodeAddresses() { + return localhostSeedNodeAddresses; + } + + public void setTorSeedNodeAddresses(List
torSeedNodeAddresses) { + this.torSeedNodeAddresses = torSeedNodeAddresses; + } + + public void setLocalhostSeedNodeAddresses(List
localhostSeedNodeAddresses) { + this.localhostSeedNodeAddresses = localhostSeedNodeAddresses; + } +} diff --git a/network/src/main/java/io/bitsquare/p2p/storage/HashSetChangedListener.java b/network/src/main/java/io/bitsquare/p2p/storage/HashSetChangedListener.java new file mode 100644 index 0000000000..a927cba57e --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/storage/HashSetChangedListener.java @@ -0,0 +1,9 @@ +package io.bitsquare.p2p.storage; + +import io.bitsquare.p2p.storage.data.ProtectedData; + +public interface HashSetChangedListener { + void onAdded(ProtectedData entry); + + void onRemoved(ProtectedData entry); +} diff --git a/network/src/main/java/io/bitsquare/p2p/storage/ProtectedExpirableDataStorage.java b/network/src/main/java/io/bitsquare/p2p/storage/ProtectedExpirableDataStorage.java new file mode 100644 index 0000000000..d1b3ca027d --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/storage/ProtectedExpirableDataStorage.java @@ -0,0 +1,308 @@ +package io.bitsquare.p2p.storage; + +import com.google.common.annotations.VisibleForTesting; +import io.bitsquare.common.UserThread; +import io.bitsquare.common.crypto.CryptoUtil; +import io.bitsquare.crypto.EncryptionService; +import io.bitsquare.p2p.Address; +import io.bitsquare.p2p.network.IllegalRequest; +import io.bitsquare.p2p.network.MessageListener; +import io.bitsquare.p2p.routing.Routing; +import io.bitsquare.p2p.storage.data.*; +import io.bitsquare.p2p.storage.messages.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.math.BigInteger; +import java.security.*; +import java.util.List; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +public class ProtectedExpirableDataStorage { + private static final Logger log = LoggerFactory.getLogger(ProtectedExpirableDataStorage.class); + + @VisibleForTesting + public static int CHECK_TTL_INTERVAL = 10 * 60 * 1000; + + private final Routing routing; + private final EncryptionService encryptionService; + private final Map map = new ConcurrentHashMap<>(); + private final List hashSetChangedListeners = new CopyOnWriteArrayList<>(); + private final Map sequenceNumberMap = new ConcurrentHashMap<>(); + private boolean authenticated; + private final Timer timer = new Timer(); + private volatile boolean shutDownInProgress; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public ProtectedExpirableDataStorage(Routing routing, EncryptionService encryptionService) { + this.routing = routing; + this.encryptionService = encryptionService; + + addMessageListener((message, connection) -> { + if (message instanceof DataMessage) { + if (connection.isAuthenticated()) { + log.trace("ProtectedExpirableDataMessage received " + message + " on connection " + connection); + if (message instanceof AddDataMessage) { + add(((AddDataMessage) message).data, connection.getPeerAddress()); + } else if (message instanceof RemoveDataMessage) { + remove(((RemoveDataMessage) message).data, connection.getPeerAddress()); + } else if (message instanceof RemoveMailboxDataMessage) { + removeMailboxData(((RemoveMailboxDataMessage) message).data, connection.getPeerAddress()); + } + } else { + log.warn("Connection is not authenticated yet. We don't accept storage operations form non-authenticated nodes."); + connection.reportIllegalRequest(IllegalRequest.NotAuthenticated); + } + } + }); + + timer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + log.info("removeExpiredEntries called "); + map.entrySet().stream().filter(entry -> entry.getValue().isExpired()) + .forEach(entry -> map.remove(entry.getKey())); + } + }, + CHECK_TTL_INTERVAL, + CHECK_TTL_INTERVAL); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void shutDown() { + if (!shutDownInProgress) { + shutDownInProgress = true; + timer.cancel(); + routing.shutDown(); + } + } + + public void setAuthenticated(boolean authenticated) { + this.authenticated = authenticated; + } + + public boolean add(ProtectedData protectedData, Address sender) { + BigInteger hashOfPayload = getHashAsBigInteger(protectedData.expirablePayload); + boolean containsKey = map.containsKey(hashOfPayload); + boolean result = checkPublicKeys(protectedData, true) + && isSequenceNrValid(protectedData, hashOfPayload) + && checkSignature(protectedData) + && (!containsKey || checkIfStoredDataMatchesNewData(protectedData, hashOfPayload)) + && doAddProtectedExpirableData(protectedData, hashOfPayload, sender); + + if (result) + sequenceNumberMap.put(hashOfPayload, protectedData.sequenceNumber); + else + log.debug("add failed"); + + return result; + } + + public boolean remove(ProtectedData protectedData, Address sender) { + BigInteger hashOfPayload = getHashAsBigInteger(protectedData.expirablePayload); + boolean containsKey = map.containsKey(hashOfPayload); + if (!containsKey) log.debug("Remove data ignored as we don't have an entry for that data."); + boolean result = containsKey + && checkPublicKeys(protectedData, false) + && isSequenceNrValid(protectedData, hashOfPayload) + && checkSignature(protectedData) + && checkIfStoredDataMatchesNewData(protectedData, hashOfPayload) + && doRemoveProtectedExpirableData(protectedData, hashOfPayload, sender); + + if (result) + sequenceNumberMap.put(hashOfPayload, protectedData.sequenceNumber); + else + log.debug("remove failed"); + + return result; + } + + public boolean removeMailboxData(ProtectedMailboxData protectedMailboxData, Address sender) { + BigInteger hashOfData = getHashAsBigInteger(protectedMailboxData.expirablePayload); + boolean containsKey = map.containsKey(hashOfData); + if (!containsKey) log.debug("Remove data ignored as we don't have an entry for that data."); + boolean result = containsKey + && checkPublicKeys(protectedMailboxData, false) + && isSequenceNrValid(protectedMailboxData, hashOfData) + && protectedMailboxData.receiversPubKey.equals(protectedMailboxData.ownerStoragePubKey) // at remove both keys are the same (only receiver is able to remove data) + && checkSignature(protectedMailboxData) + && checkIfStoredMailboxDataMatchesNewMailboxData(protectedMailboxData, hashOfData) + && doRemoveProtectedExpirableData(protectedMailboxData, hashOfData, sender); + + if (result) + sequenceNumberMap.put(hashOfData, protectedMailboxData.sequenceNumber); + else + log.debug("removeMailboxData failed"); + + return result; + } + + public Map getMap() { + return map; + } + + public ProtectedData getDataWithSignedSeqNr(ExpirablePayload payload, KeyPair ownerStoragePubKey) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException { + BigInteger hashOfData = getHashAsBigInteger(payload); + int sequenceNumber; + if (sequenceNumberMap.containsKey(hashOfData)) + sequenceNumber = sequenceNumberMap.get(hashOfData) + 1; + else + sequenceNumber = 0; + + byte[] hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(payload, sequenceNumber)); + byte[] signature = CryptoUtil.signStorageData(ownerStoragePubKey.getPrivate(), hashOfDataAndSeqNr); + return new ProtectedData(payload, payload.getTTL(), ownerStoragePubKey.getPublic(), sequenceNumber, signature); + } + + public ProtectedMailboxData getMailboxDataWithSignedSeqNr(ExpirableMailboxPayload expirableMailboxPayload, KeyPair storageSignaturePubKey, PublicKey receiversPublicKey) + throws NoSuchAlgorithmException, SignatureException, InvalidKeyException { + BigInteger hashOfData = getHashAsBigInteger(expirableMailboxPayload); + int sequenceNumber; + if (sequenceNumberMap.containsKey(hashOfData)) + sequenceNumber = sequenceNumberMap.get(hashOfData) + 1; + else + sequenceNumber = 0; + + byte[] hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(expirableMailboxPayload, sequenceNumber)); + byte[] signature = CryptoUtil.signStorageData(storageSignaturePubKey.getPrivate(), hashOfDataAndSeqNr); + return new ProtectedMailboxData(expirableMailboxPayload, expirableMailboxPayload.getTTL(), storageSignaturePubKey.getPublic(), sequenceNumber, signature, receiversPublicKey); + } + + public void addHashSetChangedListener(HashSetChangedListener hashSetChangedListener) { + hashSetChangedListeners.add(hashSetChangedListener); + } + + public void addMessageListener(MessageListener messageListener) { + routing.addMessageListener(messageListener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private boolean isSequenceNrValid(ProtectedData data, BigInteger hashOfData) { + int newSequenceNumber = data.sequenceNumber; + Integer storedSequenceNumber = sequenceNumberMap.get(hashOfData); + if (sequenceNumberMap.containsKey(hashOfData) && newSequenceNumber <= storedSequenceNumber) { + log.warn("Sequence number is invalid. That might happen in rare cases. newSequenceNumber=" + + newSequenceNumber + " / storedSequenceNumber=" + storedSequenceNumber); + return false; + } else { + return true; + } + } + + private boolean checkSignature(ProtectedData data) { + byte[] hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(data.expirablePayload, data.sequenceNumber)); + try { + boolean result = CryptoUtil.verifyStorageData(data.ownerStoragePubKey, hashOfDataAndSeqNr, data.signature); + if (!result) + log.error("Signature verification failed at checkSignature. " + + "That should not happen. Consider it might be an attempt of fraud."); + + return result; + } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { + log.error("Signature verification failed at checkSignature"); + return false; + } + } + + private boolean checkPublicKeys(ProtectedData data, boolean isAddOperation) { + boolean result = false; + if (data.expirablePayload instanceof ExpirableMailboxPayload) { + ExpirableMailboxPayload expirableMailboxPayload = (ExpirableMailboxPayload) data.expirablePayload; + if (isAddOperation) + result = expirableMailboxPayload.senderStoragePublicKey.equals(data.ownerStoragePubKey); + else + result = expirableMailboxPayload.receiverStoragePublicKey.equals(data.ownerStoragePubKey); + } else if (data.expirablePayload instanceof PubKeyProtectedExpirablePayload) { + result = ((PubKeyProtectedExpirablePayload) data.expirablePayload).getPubKey().equals(data.ownerStoragePubKey); + } + + if (!result) + log.error("PublicKey of payload data and ProtectedData are not matching. Consider it might be an attempt of fraud"); + return result; + } + + private boolean checkIfStoredDataMatchesNewData(ProtectedData data, BigInteger hashOfData) { + ProtectedData storedData = map.get(hashOfData); + boolean result = getHashAsBigInteger(storedData.expirablePayload).equals(hashOfData) + && storedData.ownerStoragePubKey.equals(data.ownerStoragePubKey); + if (!result) + log.error("New data entry does not match our stored data. Consider it might be an attempt of fraud"); + + return result; + } + + private boolean checkIfStoredMailboxDataMatchesNewMailboxData(ProtectedMailboxData data, BigInteger hashOfData) { + ProtectedData storedData = map.get(hashOfData); + if (storedData instanceof ProtectedMailboxData) { + ProtectedMailboxData storedMailboxData = (ProtectedMailboxData) storedData; + // publicKey is not the same (stored: sender, new: receiver) + boolean result = storedMailboxData.receiversPubKey.equals(data.receiversPubKey) + && getHashAsBigInteger(storedMailboxData.expirablePayload).equals(hashOfData); + if (!result) + log.error("New data entry does not match our stored data. Consider it might be an attempt of fraud"); + + return result; + } else { + log.error("We expected a MailboxData but got other type. That must never happen. storedData=" + storedData); + return false; + } + } + + private boolean doAddProtectedExpirableData(ProtectedData data, BigInteger hashOfData, Address sender) { + map.put(hashOfData, data); + log.trace("Data added to our map and it will be broadcasted to our neighbors."); + UserThread.execute(() -> hashSetChangedListeners.stream().forEach(e -> e.onAdded(data))); + broadcast(new AddDataMessage(data), sender); + + StringBuilder sb = new StringBuilder("\n\nSet after addProtectedExpirableData:\n"); + map.values().stream().forEach(e -> sb.append(e.toString() + "\n\n")); + sb.append("\n\n"); + log.trace(sb.toString()); + return true; + } + + private boolean doRemoveProtectedExpirableData(ProtectedData data, BigInteger hashOfData, Address sender) { + map.remove(hashOfData); + log.trace("Data removed from our map. We broadcast the message to our neighbors."); + UserThread.execute(() -> hashSetChangedListeners.stream().forEach(e -> e.onRemoved(data))); + if (data instanceof ProtectedMailboxData) + broadcast(new RemoveMailboxDataMessage((ProtectedMailboxData) data), sender); + else + broadcast(new RemoveDataMessage(data), sender); + + StringBuilder sb = new StringBuilder("\n\nSet after removeProtectedExpirableData:\n"); + map.values().stream().forEach(e -> sb.append(e.toString() + "\n\n")); + sb.append("\n\n"); + log.trace(sb.toString()); + return true; + } + + private void broadcast(BroadcastMessage message, Address sender) { + if (authenticated) { + routing.broadcast(message, sender); + log.trace("Broadcast message " + message); + } else { + log.trace("Broadcast not allowed because we are not authenticated yet. That is normal after received AllDataMessage at startup."); + } + } + + private BigInteger getHashAsBigInteger(ExpirablePayload payload) { + return new BigInteger(CryptoUtil.getHash(payload)); + } +} diff --git a/network/src/main/java/io/bitsquare/p2p/storage/data/DataAndSeqNr.java b/network/src/main/java/io/bitsquare/p2p/storage/data/DataAndSeqNr.java new file mode 100644 index 0000000000..693d14e0a7 --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/storage/data/DataAndSeqNr.java @@ -0,0 +1,32 @@ +package io.bitsquare.p2p.storage.data; + +import java.io.Serializable; + +public class DataAndSeqNr implements Serializable { + public final Serializable data; + public final int sequenceNumber; + + public DataAndSeqNr(Serializable data, int sequenceNumber) { + this.data = data; + this.sequenceNumber = sequenceNumber; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof DataAndSeqNr)) return false; + + DataAndSeqNr that = (DataAndSeqNr) o; + + if (sequenceNumber != that.sequenceNumber) return false; + return !(data != null ? !data.equals(that.data) : that.data != null); + + } + + @Override + public int hashCode() { + int result = data != null ? data.hashCode() : 0; + result = 31 * result + sequenceNumber; + return result; + } +} diff --git a/network/src/main/java/io/bitsquare/p2p/storage/data/ExpirableMailboxPayload.java b/network/src/main/java/io/bitsquare/p2p/storage/data/ExpirableMailboxPayload.java new file mode 100644 index 0000000000..4fc88f254c --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/storage/data/ExpirableMailboxPayload.java @@ -0,0 +1,52 @@ +package io.bitsquare.p2p.storage.data; + +import io.bitsquare.app.Version; +import io.bitsquare.p2p.messaging.SealedAndSignedMessage; + +import java.security.PublicKey; + +public final class ExpirableMailboxPayload implements ExpirablePayload { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + + public static final long TTL = 10 * 24 * 60 * 60 * 1000; // 10 days + + public final SealedAndSignedMessage sealedAndSignedMessage; + public final PublicKey senderStoragePublicKey; + public final PublicKey receiverStoragePublicKey; + + public ExpirableMailboxPayload(SealedAndSignedMessage sealedAndSignedMessage, PublicKey senderStoragePublicKey, PublicKey receiverStoragePublicKey) { + this.sealedAndSignedMessage = sealedAndSignedMessage; + this.senderStoragePublicKey = senderStoragePublicKey; + this.receiverStoragePublicKey = receiverStoragePublicKey; + } + + @Override + public long getTTL() { + return TTL; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ExpirableMailboxPayload)) return false; + + ExpirableMailboxPayload that = (ExpirableMailboxPayload) o; + + return !(sealedAndSignedMessage != null ? !sealedAndSignedMessage.equals(that.sealedAndSignedMessage) : that.sealedAndSignedMessage != null); + + } + + @Override + public int hashCode() { + return sealedAndSignedMessage != null ? sealedAndSignedMessage.hashCode() : 0; + } + + @Override + public String toString() { + return "MailboxEntry{" + + "hashCode=" + hashCode() + + ", sealedAndSignedMessage=" + sealedAndSignedMessage + + '}'; + } +} diff --git a/network/src/main/java/io/bitsquare/p2p/storage/data/ExpirablePayload.java b/network/src/main/java/io/bitsquare/p2p/storage/data/ExpirablePayload.java new file mode 100644 index 0000000000..c7ec86c064 --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/storage/data/ExpirablePayload.java @@ -0,0 +1,7 @@ +package io.bitsquare.p2p.storage.data; + +import java.io.Serializable; + +public interface ExpirablePayload extends Serializable { + long getTTL(); +} diff --git a/network/src/main/java/io/bitsquare/p2p/storage/data/ProtectedData.java b/network/src/main/java/io/bitsquare/p2p/storage/data/ProtectedData.java new file mode 100644 index 0000000000..4edb5d7e97 --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/storage/data/ProtectedData.java @@ -0,0 +1,66 @@ +package io.bitsquare.p2p.storage.data; + +import com.google.common.annotations.VisibleForTesting; +import io.bitsquare.p2p.storage.ProtectedExpirableDataStorage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.Serializable; +import java.security.PublicKey; +import java.util.Date; + +public class ProtectedData implements Serializable { + private static final Logger log = LoggerFactory.getLogger(ProtectedExpirableDataStorage.class); + + public final ExpirablePayload expirablePayload; + transient public long ttl; + public final PublicKey ownerStoragePubKey; + public final int sequenceNumber; + public final byte[] signature; + @VisibleForTesting + public Date date; + + public ProtectedData(ExpirablePayload expirablePayload, long ttl, PublicKey ownerStoragePubKey, int sequenceNumber, byte[] signature) { + this.expirablePayload = expirablePayload; + this.ttl = ttl; + this.ownerStoragePubKey = ownerStoragePubKey; + this.sequenceNumber = sequenceNumber; + this.signature = signature; + this.date = new Date(); + } + + private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { + try { + in.defaultReadObject(); + ttl = expirablePayload.getTTL(); + + // in case the reported creation date is in the future + // we reset the date to the current time + if (date.getTime() > new Date().getTime()) { + log.warn("Date of object is in future. " + + "That might be ok as clocks are not synced but could be also a spam attack. " + + "date=" + date + " / now=" + new Date()); + date = new Date(); + } + date = new Date(); + + } catch (Throwable t) { + t.printStackTrace(); + } + } + + public boolean isExpired() { + return (new Date().getTime() - date.getTime()) > ttl; + } + + @Override + public String toString() { + return "ProtectedData{" + + "data=\n" + expirablePayload + + ", \nttl=" + ttl + + ", sequenceNumber=" + sequenceNumber + + ", date=" + date + + "\n}"; + } +} diff --git a/network/src/main/java/io/bitsquare/p2p/storage/data/ProtectedMailboxData.java b/network/src/main/java/io/bitsquare/p2p/storage/data/ProtectedMailboxData.java new file mode 100644 index 0000000000..d4cff6f70d --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/storage/data/ProtectedMailboxData.java @@ -0,0 +1,55 @@ +package io.bitsquare.p2p.storage.data; + +import io.bitsquare.p2p.storage.ProtectedExpirableDataStorage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.security.PublicKey; +import java.util.Date; + +public class ProtectedMailboxData extends ProtectedData { + private static final Logger log = LoggerFactory.getLogger(ProtectedExpirableDataStorage.class); + + public final PublicKey receiversPubKey; + + public ProtectedMailboxData(ExpirableMailboxPayload data, long ttl, PublicKey ownerStoragePubKey, int sequenceNumber, byte[] signature, PublicKey receiversPubKey) { + super(data, ttl, ownerStoragePubKey, sequenceNumber, signature); + + this.receiversPubKey = receiversPubKey; + } + + private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { + try { + in.defaultReadObject(); + ttl = expirablePayload.getTTL(); + + // in case the reported creation date is in the future + // we reset the date to the current time + if (date.getTime() > new Date().getTime()) { + log.warn("Date of object is in future. " + + "That might be ok as clocks are not synced but could be also a spam attack. " + + "date=" + date + " / now=" + new Date()); + date = new Date(); + } + date = new Date(); + + } catch (Throwable t) { + t.printStackTrace(); + } + } + + public boolean isExpired() { + return (new Date().getTime() - date.getTime()) > ttl; + } + + @Override + public String toString() { + return "MailboxData{" + + "data=\n" + expirablePayload + + ", \nttl=" + ttl + + ", sequenceNumber=" + sequenceNumber + + ", date=" + date + + "\n}"; + } +} diff --git a/network/src/main/java/io/bitsquare/p2p/storage/data/PubKeyProtectedExpirablePayload.java b/network/src/main/java/io/bitsquare/p2p/storage/data/PubKeyProtectedExpirablePayload.java new file mode 100644 index 0000000000..f5256584eb --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/storage/data/PubKeyProtectedExpirablePayload.java @@ -0,0 +1,7 @@ +package io.bitsquare.p2p.storage.data; + +import java.security.PublicKey; + +public interface PubKeyProtectedExpirablePayload extends ExpirablePayload { + PublicKey getPubKey(); +} diff --git a/network/src/main/java/io/bitsquare/p2p/storage/messages/AddDataMessage.java b/network/src/main/java/io/bitsquare/p2p/storage/messages/AddDataMessage.java new file mode 100644 index 0000000000..30d83dae99 --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/storage/messages/AddDataMessage.java @@ -0,0 +1,37 @@ +package io.bitsquare.p2p.storage.messages; + +import io.bitsquare.app.Version; +import io.bitsquare.p2p.storage.data.ProtectedData; + +public final class AddDataMessage implements DataMessage { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + + public final ProtectedData data; + + public AddDataMessage(ProtectedData data) { + this.data = data; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof AddDataMessage)) return false; + + AddDataMessage that = (AddDataMessage) o; + + return !(data != null ? !data.equals(that.data) : that.data != null); + } + + @Override + public int hashCode() { + return data != null ? data.hashCode() : 0; + } + + @Override + public String toString() { + return "AddDataMessage{" + + "data=" + data + + '}'; + } +} diff --git a/network/src/main/java/io/bitsquare/p2p/storage/messages/BroadcastMessage.java b/network/src/main/java/io/bitsquare/p2p/storage/messages/BroadcastMessage.java new file mode 100644 index 0000000000..df542ccd13 --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/storage/messages/BroadcastMessage.java @@ -0,0 +1,6 @@ +package io.bitsquare.p2p.storage.messages; + +import io.bitsquare.p2p.Message; + +public interface BroadcastMessage extends Message { +} diff --git a/network/src/main/java/io/bitsquare/p2p/storage/messages/DataMessage.java b/network/src/main/java/io/bitsquare/p2p/storage/messages/DataMessage.java new file mode 100644 index 0000000000..cfa1d4f319 --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/storage/messages/DataMessage.java @@ -0,0 +1,5 @@ +package io.bitsquare.p2p.storage.messages; + +// market interface for messages which manipulate data (add, remove) +public interface DataMessage extends BroadcastMessage { +} diff --git a/network/src/main/java/io/bitsquare/p2p/storage/messages/DataSetMessage.java b/network/src/main/java/io/bitsquare/p2p/storage/messages/DataSetMessage.java new file mode 100644 index 0000000000..5a6ed1d5f3 --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/storage/messages/DataSetMessage.java @@ -0,0 +1,41 @@ +package io.bitsquare.p2p.storage.messages; + +import io.bitsquare.app.Version; +import io.bitsquare.p2p.Message; +import io.bitsquare.p2p.storage.data.ProtectedData; + +import java.util.HashSet; + +public final class DataSetMessage implements Message { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + + public final HashSet set; + + public DataSetMessage(HashSet set) { + this.set = set; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof DataSetMessage)) return false; + + DataSetMessage that = (DataSetMessage) o; + + return !(set != null ? !set.equals(that.set) : that.set != null); + + } + + @Override + public int hashCode() { + return set != null ? set.hashCode() : 0; + } + + @Override + public String toString() { + return "AllDataMessage{" + + "set=" + set + + '}'; + } +} diff --git a/network/src/main/java/io/bitsquare/p2p/storage/messages/GetDataSetMessage.java b/network/src/main/java/io/bitsquare/p2p/storage/messages/GetDataSetMessage.java new file mode 100644 index 0000000000..868774c8a6 --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/storage/messages/GetDataSetMessage.java @@ -0,0 +1,15 @@ +package io.bitsquare.p2p.storage.messages; + +import io.bitsquare.app.Version; +import io.bitsquare.p2p.Message; + +public final class GetDataSetMessage implements Message { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + + public final long nonce; + + public GetDataSetMessage(long nonce) { + this.nonce = nonce; + } +} diff --git a/network/src/main/java/io/bitsquare/p2p/storage/messages/RemoveDataMessage.java b/network/src/main/java/io/bitsquare/p2p/storage/messages/RemoveDataMessage.java new file mode 100644 index 0000000000..daedbe7970 --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/storage/messages/RemoveDataMessage.java @@ -0,0 +1,38 @@ +package io.bitsquare.p2p.storage.messages; + +import io.bitsquare.app.Version; +import io.bitsquare.p2p.storage.data.ProtectedData; + +public final class RemoveDataMessage implements DataMessage { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + + public final ProtectedData data; + + public RemoveDataMessage(ProtectedData data) { + this.data = data; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof RemoveDataMessage)) return false; + + RemoveDataMessage that = (RemoveDataMessage) o; + + return !(data != null ? !data.equals(that.data) : that.data != null); + + } + + @Override + public int hashCode() { + return data != null ? data.hashCode() : 0; + } + + @Override + public String toString() { + return "RemoveDataMessage{" + + "data=" + data + + '}'; + } +} diff --git a/network/src/main/java/io/bitsquare/p2p/storage/messages/RemoveMailboxDataMessage.java b/network/src/main/java/io/bitsquare/p2p/storage/messages/RemoveMailboxDataMessage.java new file mode 100644 index 0000000000..11de32aa11 --- /dev/null +++ b/network/src/main/java/io/bitsquare/p2p/storage/messages/RemoveMailboxDataMessage.java @@ -0,0 +1,38 @@ +package io.bitsquare.p2p.storage.messages; + +import io.bitsquare.app.Version; +import io.bitsquare.p2p.storage.data.ProtectedMailboxData; + +public final class RemoveMailboxDataMessage implements DataMessage { + // That object is sent over the wire, so we need to take care of version compatibility. + private static final long serialVersionUID = Version.NETWORK_PROTOCOL_VERSION; + + public final ProtectedMailboxData data; + + public RemoveMailboxDataMessage(ProtectedMailboxData data) { + this.data = data; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof RemoveMailboxDataMessage)) return false; + + RemoveMailboxDataMessage that = (RemoveMailboxDataMessage) o; + + return !(data != null ? !data.equals(that.data) : that.data != null); + + } + + @Override + public int hashCode() { + return data != null ? data.hashCode() : 0; + } + + @Override + public String toString() { + return "RemoveMailboxDataMessage{" + + "data=" + data + + '}'; + } +} diff --git a/network/src/main/resources/logback.xml b/network/src/main/resources/logback.xml new file mode 100644 index 0000000000..1454cf5289 --- /dev/null +++ b/network/src/main/resources/logback.xml @@ -0,0 +1,13 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg %xEx%n + + + + + + + + diff --git a/network/src/test/java/io/bitsquare/p2p/P2PServiceTest.java b/network/src/test/java/io/bitsquare/p2p/P2PServiceTest.java new file mode 100644 index 0000000000..6982842a0c --- /dev/null +++ b/network/src/test/java/io/bitsquare/p2p/P2PServiceTest.java @@ -0,0 +1,362 @@ +package io.bitsquare.p2p; + +import io.bitsquare.common.crypto.CryptoException; +import io.bitsquare.common.crypto.CryptoUtil; +import io.bitsquare.common.crypto.KeyRing; +import io.bitsquare.common.crypto.KeyStorage; +import io.bitsquare.crypto.EncryptionService; +import io.bitsquare.p2p.messaging.DecryptedMessageWithPubKey; +import io.bitsquare.p2p.messaging.MailboxMessage; +import io.bitsquare.p2p.messaging.SealedAndSignedMessage; +import io.bitsquare.p2p.messaging.SendMailboxMessageListener; +import io.bitsquare.p2p.mocks.MockMailboxMessage; +import io.bitsquare.p2p.network.LocalhostNetworkNode; +import io.bitsquare.p2p.routing.Routing; +import io.bitsquare.p2p.seed.SeedNode; +import io.bitsquare.p2p.storage.data.DataAndSeqNr; +import io.bitsquare.p2p.storage.data.ProtectedData; +import io.bitsquare.p2p.storage.mocks.MockData; +import org.junit.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.security.*; +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.concurrent.CountDownLatch; + +// TorNode created. Took 6 sec. +// Hidden service created. Took 40-50 sec. +// Connection establishment takes about 4 sec. + +// need to define seed node addresses first before using tor version +@Ignore +public class P2PServiceTest { + private static final Logger log = LoggerFactory.getLogger(P2PServiceTest.class); + + boolean useLocalhost = true; + private ArrayList
seedNodes; + private int sleepTime; + private KeyRing keyRing1, keyRing2, keyRing3; + private EncryptionService encryptionService1, encryptionService2, encryptionService3; + private P2PService p2PService1, p2PService2, p2PService3; + private SeedNode seedNode1, seedNode2, seedNode3; + + @Before + public void setup() throws InterruptedException, CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, CryptoException { + LocalhostNetworkNode.setSimulateTorDelayTorNode(10); + LocalhostNetworkNode.setSimulateTorDelayHiddenService(100); + Routing.setMaxConnections(8); + + keyRing1 = new KeyRing(new KeyStorage(new File("temp_keyStorage1"))); + keyRing2 = new KeyRing(new KeyStorage(new File("temp_keyStorage2"))); + keyRing3 = new KeyRing(new KeyStorage(new File("temp_keyStorage3"))); + encryptionService1 = new EncryptionService(keyRing1); + encryptionService2 = new EncryptionService(keyRing2); + encryptionService3 = new EncryptionService(keyRing3); + + seedNodes = new ArrayList<>(); + if (useLocalhost) { + seedNodes.add(new Address("localhost:8001")); + seedNodes.add(new Address("localhost:8002")); + seedNodes.add(new Address("localhost:8003")); + sleepTime = 100; + + } else { + seedNodes.add(new Address("3omjuxn7z73pxoee.onion:8001")); + seedNodes.add(new Address("j24fxqyghjetgpdx.onion:8002")); + seedNodes.add(new Address("45367tl6unwec6kw.onion:8003")); + sleepTime = 1000; + } + + seedNode1 = TestUtils.getAndStartSeedNode(8001, encryptionService1, keyRing1, useLocalhost, seedNodes); + p2PService1 = seedNode1.getP2PService(); + p2PService2 = TestUtils.getAndAuthenticateP2PService(8002, encryptionService2, keyRing2, useLocalhost, seedNodes); + } + + @After + public void tearDown() throws InterruptedException { + Thread.sleep(sleepTime); + + if (seedNode1 != null) { + CountDownLatch shutDownLatch = new CountDownLatch(1); + seedNode1.shutDown(() -> shutDownLatch.countDown()); + shutDownLatch.await(); + } + if (seedNode2 != null) { + CountDownLatch shutDownLatch = new CountDownLatch(1); + seedNode2.shutDown(() -> shutDownLatch.countDown()); + shutDownLatch.await(); + } + if (seedNode3 != null) { + CountDownLatch shutDownLatch = new CountDownLatch(1); + seedNode3.shutDown(() -> shutDownLatch.countDown()); + shutDownLatch.await(); + } + if (p2PService1 != null) { + CountDownLatch shutDownLatch = new CountDownLatch(1); + p2PService1.shutDown(() -> shutDownLatch.countDown()); + shutDownLatch.await(); + } + if (p2PService2 != null) { + CountDownLatch shutDownLatch = new CountDownLatch(1); + p2PService2.shutDown(() -> shutDownLatch.countDown()); + shutDownLatch.await(); + } + if (p2PService3 != null) { + CountDownLatch shutDownLatch = new CountDownLatch(1); + p2PService3.shutDown(() -> shutDownLatch.countDown()); + shutDownLatch.await(); + } + } + + @Test + public void testAdversaryAttacks() throws InterruptedException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, CryptoException, SignatureException, InvalidKeyException { + p2PService3 = TestUtils.getAndAuthenticateP2PService(8003, encryptionService3, keyRing3, useLocalhost, seedNodes); + + MockData origData = new MockData("mockData1", keyRing1.getStorageSignatureKeyPair().getPublic()); + + p2PService1.addData(origData); + Assert.assertEquals(1, p2PService1.getDataMap().size()); + Assert.assertEquals(1, p2PService2.getDataMap().size()); + Assert.assertEquals(1, p2PService3.getDataMap().size()); + + + // p2PService3 is adversary + KeyPair msgSignatureKeyPairAdversary = keyRing3.getStorageSignatureKeyPair(); + + // try to remove data -> fails + Assert.assertFalse(p2PService3.removeData(origData)); + Thread.sleep(sleepTime); + Assert.assertEquals(1, p2PService1.getDataMap().size()); + Assert.assertEquals(1, p2PService2.getDataMap().size()); + Assert.assertEquals(1, p2PService3.getDataMap().size()); + + + // try to add manipulated data -> fails + Assert.assertFalse(p2PService3.removeData(new MockData("mockData2", origData.publicKey))); + Thread.sleep(sleepTime); + Assert.assertEquals(1, p2PService1.getDataMap().size()); + Assert.assertEquals(1, p2PService2.getDataMap().size()); + Assert.assertEquals(1, p2PService3.getDataMap().size()); + + // try to manipulate seq nr. -> fails + ProtectedData origProtectedData = p2PService3.getDataMap().values().stream().findFirst().get(); + ProtectedData protectedDataManipulated = new ProtectedData(origProtectedData.expirablePayload, origProtectedData.ttl, origProtectedData.ownerStoragePubKey, origProtectedData.sequenceNumber + 1, origProtectedData.signature); + Assert.assertFalse(p2PService3.removeData(protectedDataManipulated.expirablePayload)); + Thread.sleep(sleepTime); + Assert.assertEquals(1, p2PService1.getDataMap().size()); + Assert.assertEquals(1, p2PService2.getDataMap().size()); + Assert.assertEquals(1, p2PService3.getDataMap().size()); + + // try to manipulate seq nr. + pubKey -> fails + protectedDataManipulated = new ProtectedData(origProtectedData.expirablePayload, origProtectedData.ttl, msgSignatureKeyPairAdversary.getPublic(), origProtectedData.sequenceNumber + 1, origProtectedData.signature); + Assert.assertFalse(p2PService3.removeData(protectedDataManipulated.expirablePayload)); + Thread.sleep(sleepTime); + Assert.assertEquals(1, p2PService1.getDataMap().size()); + Assert.assertEquals(1, p2PService2.getDataMap().size()); + Assert.assertEquals(1, p2PService3.getDataMap().size()); + + // try to manipulate seq nr. + pubKey + sig -> fails + int sequenceNumberManipulated = origProtectedData.sequenceNumber + 1; + byte[] hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(origProtectedData.expirablePayload, sequenceNumberManipulated)); + byte[] signature = CryptoUtil.signStorageData(msgSignatureKeyPairAdversary.getPrivate(), hashOfDataAndSeqNr); + protectedDataManipulated = new ProtectedData(origProtectedData.expirablePayload, origProtectedData.ttl, msgSignatureKeyPairAdversary.getPublic(), sequenceNumberManipulated, signature); + Assert.assertFalse(p2PService3.removeData(protectedDataManipulated.expirablePayload)); + Thread.sleep(sleepTime); + Assert.assertEquals(1, p2PService1.getDataMap().size()); + Assert.assertEquals(1, p2PService2.getDataMap().size()); + Assert.assertEquals(1, p2PService3.getDataMap().size()); + + + // data owner removes -> ok + Assert.assertTrue(p2PService1.removeData(origData)); + Thread.sleep(sleepTime); + Assert.assertEquals(0, p2PService1.getDataMap().size()); + Assert.assertEquals(0, p2PService2.getDataMap().size()); + Assert.assertEquals(0, p2PService3.getDataMap().size()); + + // adversary manage to change data before it is broadcasted to others + // data owner is connected only to adversary -> he change data at onMessage and broadcast manipulated data + // first he tries to use the orig. pubKey in the data -> fails as pub keys not matching + MockData manipulatedData = new MockData("mockData1_manipulated", origData.publicKey); + sequenceNumberManipulated = 0; + hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(manipulatedData, sequenceNumberManipulated)); + signature = CryptoUtil.signStorageData(msgSignatureKeyPairAdversary.getPrivate(), hashOfDataAndSeqNr); + protectedDataManipulated = new ProtectedData(origProtectedData.expirablePayload, origProtectedData.ttl, msgSignatureKeyPairAdversary.getPublic(), sequenceNumberManipulated, signature); + Assert.assertFalse(p2PService3.addData(protectedDataManipulated.expirablePayload)); + Thread.sleep(sleepTime); + Assert.assertEquals(0, p2PService1.getDataMap().size()); + Assert.assertEquals(0, p2PService2.getDataMap().size()); + Assert.assertEquals(0, p2PService3.getDataMap().size()); + + // then he tries to use his pubKey but orig data payload -> fails as pub keys nto matching + manipulatedData = new MockData("mockData1_manipulated", msgSignatureKeyPairAdversary.getPublic()); + sequenceNumberManipulated = 0; + hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(manipulatedData, sequenceNumberManipulated)); + signature = CryptoUtil.signStorageData(msgSignatureKeyPairAdversary.getPrivate(), hashOfDataAndSeqNr); + protectedDataManipulated = new ProtectedData(origProtectedData.expirablePayload, origProtectedData.ttl, msgSignatureKeyPairAdversary.getPublic(), sequenceNumberManipulated, signature); + Assert.assertFalse(p2PService3.addData(protectedDataManipulated.expirablePayload)); + Thread.sleep(sleepTime); + Assert.assertEquals(0, p2PService1.getDataMap().size()); + Assert.assertEquals(0, p2PService2.getDataMap().size()); + Assert.assertEquals(0, p2PService3.getDataMap().size()); + + // then he tries to use his pubKey -> now he succeeds, but its same as adding a completely new msg and + // payload data has adversary's pubKey so he could hijack the owners data + manipulatedData = new MockData("mockData1_manipulated", msgSignatureKeyPairAdversary.getPublic()); + sequenceNumberManipulated = 0; + hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(manipulatedData, sequenceNumberManipulated)); + signature = CryptoUtil.signStorageData(msgSignatureKeyPairAdversary.getPrivate(), hashOfDataAndSeqNr); + protectedDataManipulated = new ProtectedData(manipulatedData, origProtectedData.ttl, msgSignatureKeyPairAdversary.getPublic(), sequenceNumberManipulated, signature); + Assert.assertTrue(p2PService3.addData(protectedDataManipulated.expirablePayload)); + Thread.sleep(sleepTime); + Assert.assertEquals(1, p2PService1.getDataMap().size()); + Assert.assertEquals(1, p2PService2.getDataMap().size()); + Assert.assertEquals(1, p2PService3.getDataMap().size()); + + // let clean up. he can remove his own data + Assert.assertTrue(p2PService3.removeData(manipulatedData)); + Thread.sleep(sleepTime); + Assert.assertEquals(0, p2PService1.getDataMap().size()); + Assert.assertEquals(0, p2PService2.getDataMap().size()); + Assert.assertEquals(0, p2PService3.getDataMap().size()); + + + // finally he tries both previous attempts with same data - > same as before + manipulatedData = new MockData("mockData1", origData.publicKey); + sequenceNumberManipulated = 0; + hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(manipulatedData, sequenceNumberManipulated)); + signature = CryptoUtil.signStorageData(msgSignatureKeyPairAdversary.getPrivate(), hashOfDataAndSeqNr); + protectedDataManipulated = new ProtectedData(origProtectedData.expirablePayload, origProtectedData.ttl, msgSignatureKeyPairAdversary.getPublic(), sequenceNumberManipulated, signature); + Assert.assertFalse(p2PService3.addData(protectedDataManipulated.expirablePayload)); + Thread.sleep(sleepTime); + Assert.assertEquals(0, p2PService1.getDataMap().size()); + Assert.assertEquals(0, p2PService2.getDataMap().size()); + Assert.assertEquals(0, p2PService3.getDataMap().size()); + + manipulatedData = new MockData("mockData1", msgSignatureKeyPairAdversary.getPublic()); + sequenceNumberManipulated = 0; + hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(manipulatedData, sequenceNumberManipulated)); + signature = CryptoUtil.signStorageData(msgSignatureKeyPairAdversary.getPrivate(), hashOfDataAndSeqNr); + protectedDataManipulated = new ProtectedData(manipulatedData, origProtectedData.ttl, msgSignatureKeyPairAdversary.getPublic(), sequenceNumberManipulated, signature); + Assert.assertTrue(p2PService3.addData(protectedDataManipulated.expirablePayload)); + Thread.sleep(sleepTime); + Assert.assertEquals(1, p2PService1.getDataMap().size()); + Assert.assertEquals(1, p2PService2.getDataMap().size()); + Assert.assertEquals(1, p2PService3.getDataMap().size()); + + // lets reset map + Assert.assertTrue(p2PService3.removeData(protectedDataManipulated.expirablePayload)); + Thread.sleep(sleepTime); + Assert.assertEquals(0, p2PService1.getDataMap().size()); + Assert.assertEquals(0, p2PService2.getDataMap().size()); + Assert.assertEquals(0, p2PService3.getDataMap().size()); + + // owner can add any time his data + Assert.assertTrue(p2PService1.addData(origData)); + Thread.sleep(sleepTime); + Assert.assertEquals(1, p2PService1.getDataMap().size()); + Assert.assertEquals(1, p2PService2.getDataMap().size()); + Assert.assertEquals(1, p2PService3.getDataMap().size()); + } + + //@Test + public void testSendMailboxMessageToOnlinePeer() throws InterruptedException, CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, CryptoException { + LocalhostNetworkNode.setSimulateTorDelayTorNode(0); + LocalhostNetworkNode.setSimulateTorDelayHiddenService(0); + + // send to online peer + CountDownLatch latch2 = new CountDownLatch(2); + MockMailboxMessage mockMessage = new MockMailboxMessage("MockMailboxMessage", p2PService2.getAddress()); + p2PService2.addMessageListener((message, connection) -> { + log.trace("message " + message); + if (message instanceof SealedAndSignedMessage) { + try { + DecryptedMessageWithPubKey decryptedMessageWithPubKey = encryptionService2.decryptAndVerifyMessage((SealedAndSignedMessage) message); + Assert.assertEquals(mockMessage, decryptedMessageWithPubKey.message); + Assert.assertEquals(p2PService2.getAddress(), ((MailboxMessage) decryptedMessageWithPubKey.message).getSenderAddress()); + latch2.countDown(); + } catch (CryptoException e) { + e.printStackTrace(); + } + } + }); + + p2PService1.sendEncryptedMailboxMessage( + p2PService2.getAddress(), + keyRing2.getPubKeyRing(), + mockMessage, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.trace("Message arrived at peer."); + latch2.countDown(); + } + + @Override + public void onStoredInMailbox() { + log.trace("Message stored in mailbox."); + } + + @Override + public void onFault() { + log.error("onFault"); + } + } + ); + latch2.await(); + } + + //@Test + public void testSendMailboxMessageToOfflinePeer() throws InterruptedException, CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, CryptoException { + LocalhostNetworkNode.setSimulateTorDelayTorNode(0); + LocalhostNetworkNode.setSimulateTorDelayHiddenService(0); + + // send msg to offline peer + MockMailboxMessage mockMessage = new MockMailboxMessage( + "MockMailboxMessage", + p2PService2.getAddress() + ); + CountDownLatch latch2 = new CountDownLatch(1); + p2PService2.sendEncryptedMailboxMessage( + new Address("localhost:8003"), + keyRing3.getPubKeyRing(), + mockMessage, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.trace("Message arrived at peer."); + } + + @Override + public void onStoredInMailbox() { + log.trace("Message stored in mailbox."); + latch2.countDown(); + } + + @Override + public void onFault() { + log.error("onFault"); + } + } + ); + latch2.await(); + Thread.sleep(2000); + + + // start node 3 + p2PService3 = TestUtils.getAndAuthenticateP2PService(8003, encryptionService3, keyRing3, useLocalhost, seedNodes); + Thread.sleep(sleepTime); + CountDownLatch latch3 = new CountDownLatch(1); + p2PService3.addDecryptedMailboxListener((decryptedMessageWithPubKey, senderAddress) -> { + log.debug("decryptedMessageWithPubKey " + decryptedMessageWithPubKey.toString()); + Assert.assertEquals(mockMessage, decryptedMessageWithPubKey.message); + Assert.assertEquals(p2PService2.getAddress(), ((MailboxMessage) decryptedMessageWithPubKey.message).getSenderAddress()); + latch3.countDown(); + }); + latch3.await(); + } +} diff --git a/network/src/test/java/io/bitsquare/p2p/TestUtils.java b/network/src/test/java/io/bitsquare/p2p/TestUtils.java new file mode 100644 index 0000000000..842f9882e4 --- /dev/null +++ b/network/src/test/java/io/bitsquare/p2p/TestUtils.java @@ -0,0 +1,150 @@ +package io.bitsquare.p2p; + +import io.bitsquare.common.crypto.KeyRing; +import io.bitsquare.crypto.EncryptionService; +import io.bitsquare.p2p.seed.SeedNode; +import io.bitsquare.p2p.seed.SeedNodesRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; +import java.security.*; +import java.util.ArrayList; +import java.util.concurrent.CountDownLatch; + +public class TestUtils { + private static final Logger log = LoggerFactory.getLogger(TestUtils.class); + + public static int sleepTime; + + public static KeyPair generateKeyPair() throws NoSuchAlgorithmException { + long ts = System.currentTimeMillis(); + final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA"); + keyPairGenerator.initialize(1024); + KeyPair keyPair = keyPairGenerator.genKeyPair(); + log.trace("Generate storageSignatureKeyPair needed {} ms", System.currentTimeMillis() - ts); + return keyPair; + } + + + public static byte[] sign(PrivateKey privateKey, Serializable data) + throws SignatureException, NoSuchAlgorithmException, InvalidKeyException { + Signature sig = Signature.getInstance("SHA1withDSA"); + sig.initSign(privateKey); + sig.update(objectToByteArray(data)); + return sig.sign(); + } + + public static byte[] objectToByteArray(Object object) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutput out = null; + byte[] result = null; + try { + out = new ObjectOutputStream(bos); + out.writeObject(object); + result = bos.toByteArray(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (out != null) { + out.close(); + } + } catch (IOException ex) { + // ignore close exception + } + try { + bos.close(); + } catch (IOException ex) { + // ignore close exception + } + } + return result; + } + + public static SeedNode getAndStartSeedNode(int port, EncryptionService encryptionService, KeyRing keyRing, boolean useLocalhost, ArrayList
seedNodes) throws InterruptedException { + SeedNode seedNode; + + if (useLocalhost) { + seedNodes.add(new Address("localhost:8001")); + seedNodes.add(new Address("localhost:8002")); + seedNodes.add(new Address("localhost:8003")); + sleepTime = 100; + seedNode = new SeedNode(); + } else { + seedNodes.add(new Address("3omjuxn7z73pxoee.onion:8001")); + seedNodes.add(new Address("j24fxqyghjetgpdx.onion:8002")); + seedNodes.add(new Address("45367tl6unwec6kw.onion:8003")); + sleepTime = 10000; + seedNode = new SeedNode(); + } + + CountDownLatch latch = new CountDownLatch(1); + seedNode.createAndStartP2PService(encryptionService, keyRing, port, useLocalhost, seedNodes, new P2PServiceListener() { + @Override + public void onAllDataReceived() { + } + + @Override + public void onAuthenticated() { + } + + @Override + public void onTorNodeReady() { + } + + @Override + public void onHiddenServiceReady() { + latch.countDown(); + } + + @Override + public void onSetupFailed(Throwable throwable) { + } + }); + latch.await(); + Thread.sleep(sleepTime); + return seedNode; + } + + public static P2PService getAndAuthenticateP2PService(int port, EncryptionService encryptionService, KeyRing keyRing, boolean useLocalhost, ArrayList
seedNodes) throws InterruptedException { + CountDownLatch latch = new CountDownLatch(1); + SeedNodesRepository seedNodesRepository = new SeedNodesRepository(); + if (seedNodes != null && !seedNodes.isEmpty()) { + if (useLocalhost) + seedNodesRepository.setLocalhostSeedNodeAddresses(seedNodes); + else + seedNodesRepository.setTorSeedNodeAddresses(seedNodes); + } + + P2PService p2PService = new P2PService(seedNodesRepository, port, new File("seed_node_" + port), useLocalhost, encryptionService, keyRing); + p2PService.start(new P2PServiceListener() { + @Override + public void onAllDataReceived() { + } + + @Override + public void onTorNodeReady() { + + } + + @Override + public void onAuthenticated() { + latch.countDown(); + } + + @Override + public void onHiddenServiceReady() { + + } + + @Override + public void onSetupFailed(Throwable throwable) { + + } + }); + latch.await(); + Thread.sleep(2000); + return p2PService; + } +} diff --git a/network/src/test/java/io/bitsquare/p2p/mocks/MockMailboxMessage.java b/network/src/test/java/io/bitsquare/p2p/mocks/MockMailboxMessage.java new file mode 100644 index 0000000000..2977d2b98e --- /dev/null +++ b/network/src/test/java/io/bitsquare/p2p/mocks/MockMailboxMessage.java @@ -0,0 +1,49 @@ +package io.bitsquare.p2p.mocks; + +import io.bitsquare.p2p.Address; +import io.bitsquare.p2p.messaging.MailboxMessage; +import io.bitsquare.p2p.storage.data.ExpirablePayload; + +public class MockMailboxMessage implements MailboxMessage, ExpirablePayload { + public String msg; + public Address senderAddress; + public long ttl; + + public MockMailboxMessage(String msg, Address senderAddress) { + this.msg = msg; + this.senderAddress = senderAddress; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof MockMailboxMessage)) return false; + + MockMailboxMessage that = (MockMailboxMessage) o; + + return !(msg != null ? !msg.equals(that.msg) : that.msg != null); + + } + + @Override + public int hashCode() { + return msg != null ? msg.hashCode() : 0; + } + + @Override + public String toString() { + return "MockData{" + + "msg='" + msg + '\'' + + '}'; + } + + @Override + public long getTTL() { + return ttl; + } + + @Override + public Address getSenderAddress() { + return senderAddress; + } +} diff --git a/network/src/test/java/io/bitsquare/p2p/mocks/MockMessage.java b/network/src/test/java/io/bitsquare/p2p/mocks/MockMessage.java new file mode 100644 index 0000000000..d37ce08bde --- /dev/null +++ b/network/src/test/java/io/bitsquare/p2p/mocks/MockMessage.java @@ -0,0 +1,41 @@ +package io.bitsquare.p2p.mocks; + +import io.bitsquare.p2p.Message; +import io.bitsquare.p2p.storage.data.ExpirablePayload; + +public class MockMessage implements Message, ExpirablePayload { + public String msg; + public long ttl; + + public MockMessage(String msg) { + this.msg = msg; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof MockMessage)) return false; + + MockMessage that = (MockMessage) o; + + return !(msg != null ? !msg.equals(that.msg) : that.msg != null); + + } + + @Override + public int hashCode() { + return msg != null ? msg.hashCode() : 0; + } + + @Override + public String toString() { + return "MockData{" + + "msg='" + msg + '\'' + + '}'; + } + + @Override + public long getTTL() { + return ttl; + } +} diff --git a/network/src/test/java/io/bitsquare/p2p/network/LocalhostNetworkNodeTest.java b/network/src/test/java/io/bitsquare/p2p/network/LocalhostNetworkNodeTest.java new file mode 100644 index 0000000000..d2d0497e4e --- /dev/null +++ b/network/src/test/java/io/bitsquare/p2p/network/LocalhostNetworkNodeTest.java @@ -0,0 +1,84 @@ +package io.bitsquare.p2p.network; + +import io.bitsquare.p2p.Address; +import io.bitsquare.p2p.routing.messages.RequestAuthenticationMessage; +import org.junit.Ignore; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.concurrent.CountDownLatch; + +// TorNode created. Took 6 sec. +// Hidden service created. Took 40-50 sec. +// Connection establishment takes about 4 sec. +@Ignore +public class LocalhostNetworkNodeTest { + private static final Logger log = LoggerFactory.getLogger(LocalhostNetworkNodeTest.class); + + @Test + public void testMessage() throws InterruptedException, IOException { + CountDownLatch msgLatch = new CountDownLatch(2); + LocalhostNetworkNode node1 = new LocalhostNetworkNode(9001); + node1.addMessageListener((message, connection) -> { + log.debug("onMessage node1 " + message); + msgLatch.countDown(); + }); + CountDownLatch startupLatch = new CountDownLatch(2); + node1.start(new SetupListener() { + @Override + public void onTorNodeReady() { + log.debug("onTorNodeReady"); + } + + @Override + public void onHiddenServiceReady() { + log.debug("onHiddenServiceReady"); + startupLatch.countDown(); + } + + @Override + public void onSetupFailed(Throwable throwable) { + log.debug("onSetupFailed"); + } + }); + + LocalhostNetworkNode node2 = new LocalhostNetworkNode(9002); + node2.addMessageListener((message, connection) -> { + log.debug("onMessage node2 " + message); + msgLatch.countDown(); + }); + node2.start(new SetupListener() { + @Override + public void onTorNodeReady() { + log.debug("onTorNodeReady 2"); + } + + @Override + public void onHiddenServiceReady() { + log.debug("onHiddenServiceReady 2"); + startupLatch.countDown(); + } + + @Override + public void onSetupFailed(Throwable throwable) { + log.debug("onSetupFailed 2"); + } + }); + startupLatch.await(); + + node2.sendMessage(new Address("localhost", 9001), new RequestAuthenticationMessage(new Address("localhost", 9002), 1)); + node1.sendMessage(new Address("localhost", 9002), new RequestAuthenticationMessage(new Address("localhost", 9001), 1)); + msgLatch.await(); + + CountDownLatch shutDownLatch = new CountDownLatch(2); + node1.shutDown(() -> { + shutDownLatch.countDown(); + }); + node2.shutDown(() -> { + shutDownLatch.countDown(); + }); + shutDownLatch.await(); + } +} diff --git a/network/src/test/java/io/bitsquare/p2p/network/TorNetworkNodeTest.java b/network/src/test/java/io/bitsquare/p2p/network/TorNetworkNodeTest.java new file mode 100644 index 0000000000..127182db67 --- /dev/null +++ b/network/src/test/java/io/bitsquare/p2p/network/TorNetworkNodeTest.java @@ -0,0 +1,185 @@ +package io.bitsquare.p2p.network; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.SettableFuture; +import io.bitsquare.p2p.Message; +import io.bitsquare.p2p.mocks.MockMessage; +import org.junit.Ignore; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.util.concurrent.CountDownLatch; + +// TorNode created. Took 6 sec. +// Hidden service created. Took 40-50 sec. +// Connection establishment takes about 4 sec. +@Ignore +public class TorNetworkNodeTest { + private static final Logger log = LoggerFactory.getLogger(TorNetworkNodeTest.class); + private CountDownLatch latch; + + @Test + public void testTorNodeBeforeSecondReady() throws InterruptedException, IOException { + latch = new CountDownLatch(1); + int port = 9001; + TorNetworkNode node1 = new TorNetworkNode(port, new File("torNode_" + port)); + node1.start(new SetupListener() { + @Override + public void onTorNodeReady() { + log.debug("onReadyForSendingMessages"); + } + + @Override + public void onHiddenServiceReady() { + log.debug("onReadyForReceivingMessages"); + latch.countDown(); + } + + @Override + public void onSetupFailed(Throwable throwable) { + + } + }); + latch.await(); + + latch = new CountDownLatch(1); + int port2 = 9002; + TorNetworkNode node2 = new TorNetworkNode(port2, new File("torNode_" + port2)); + node2.start(new SetupListener() { + @Override + public void onTorNodeReady() { + log.debug("onReadyForSendingMessages"); + latch.countDown(); + } + + @Override + public void onHiddenServiceReady() { + log.debug("onReadyForReceivingMessages"); + + } + + @Override + public void onSetupFailed(Throwable throwable) { + + } + }); + latch.await(); + + + latch = new CountDownLatch(2); + node1.addMessageListener(new MessageListener() { + @Override + public void onMessage(Message message, Connection connection) { + log.debug("onMessage node1 " + message); + latch.countDown(); + } + }); + SettableFuture future = node2.sendMessage(node1.getAddress(), new MockMessage("msg1")); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(Connection connection) { + log.debug("onSuccess "); + latch.countDown(); + } + + @Override + public void onFailure(Throwable throwable) { + log.debug("onFailure "); + } + }); + latch.await(); + + + latch = new CountDownLatch(2); + node1.shutDown(() -> { + latch.countDown(); + }); + node2.shutDown(() -> { + latch.countDown(); + }); + latch.await(); + } + + //@Test + public void testTorNodeAfterBothReady() throws InterruptedException, IOException { + latch = new CountDownLatch(2); + int port = 9001; + TorNetworkNode node1 = new TorNetworkNode(port, new File("torNode_" + port)); + node1.start(new SetupListener() { + @Override + public void onTorNodeReady() { + log.debug("onReadyForSendingMessages"); + } + + @Override + public void onHiddenServiceReady() { + log.debug("onReadyForReceivingMessages"); + latch.countDown(); + } + + @Override + public void onSetupFailed(Throwable throwable) { + + } + }); + + int port2 = 9002; + TorNetworkNode node2 = new TorNetworkNode(port2, new File("torNode_" + port)); + node2.start(new SetupListener() { + @Override + public void onTorNodeReady() { + log.debug("onReadyForSendingMessages"); + } + + @Override + public void onHiddenServiceReady() { + log.debug("onReadyForReceivingMessages"); + latch.countDown(); + } + + @Override + public void onSetupFailed(Throwable throwable) { + + } + }); + + latch.await(); + + latch = new CountDownLatch(2); + node2.addMessageListener(new MessageListener() { + @Override + public void onMessage(Message message, Connection connection) { + log.debug("onMessage node2 " + message); + latch.countDown(); + } + }); + SettableFuture future = node1.sendMessage(node2.getAddress(), new MockMessage("msg1")); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(Connection connection) { + log.debug("onSuccess "); + latch.countDown(); + } + + @Override + public void onFailure(Throwable throwable) { + log.debug("onFailure "); + } + }); + latch.await(); + + + latch = new CountDownLatch(2); + node1.shutDown(() -> { + latch.countDown(); + }); + node2.shutDown(() -> { + latch.countDown(); + }); + latch.await(); + } +} diff --git a/network/src/test/java/io/bitsquare/p2p/routing/RoutingTest.java b/network/src/test/java/io/bitsquare/p2p/routing/RoutingTest.java new file mode 100644 index 0000000000..8c3a0148ab --- /dev/null +++ b/network/src/test/java/io/bitsquare/p2p/routing/RoutingTest.java @@ -0,0 +1,411 @@ +package io.bitsquare.p2p.routing; + +import io.bitsquare.common.util.Profiler; +import io.bitsquare.p2p.Address; +import io.bitsquare.p2p.P2PService; +import io.bitsquare.p2p.P2PServiceListener; +import io.bitsquare.p2p.network.Connection; +import io.bitsquare.p2p.network.LocalhostNetworkNode; +import io.bitsquare.p2p.seed.SeedNode; +import org.junit.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.concurrent.CountDownLatch; + +// TorNode created. Took 6 sec. +// Hidden service created. Took 40-50 sec. +// Connection establishment takes about 4 sec. + +// need to define seed node addresses first before using tor version +@Ignore +public class RoutingTest { + private static final Logger log = LoggerFactory.getLogger(RoutingTest.class); + + boolean useLocalhost = true; + private CountDownLatch latch; + private ArrayList
seedNodes; + private int sleepTime; + private SeedNode seedNode1, seedNode2, seedNode3; + + @Before + public void setup() throws InterruptedException { + LocalhostNetworkNode.setSimulateTorDelayTorNode(50); + LocalhostNetworkNode.setSimulateTorDelayHiddenService(8); + Routing.setMaxConnections(100); + + seedNodes = new ArrayList<>(); + if (useLocalhost) { + //seedNodes.add(new Address("localhost:8001")); + // seedNodes.add(new Address("localhost:8002")); + seedNodes.add(new Address("localhost:8003")); + sleepTime = 100; + + } else { + seedNodes.add(new Address("3omjuxn7z73pxoee.onion:8001")); + seedNodes.add(new Address("j24fxqyghjetgpdx.onion:8002")); + seedNodes.add(new Address("45367tl6unwec6kw.onion:8003")); + sleepTime = 1000; + } + } + + @After + public void tearDown() throws InterruptedException { + Thread.sleep(sleepTime); + + if (seedNode1 != null) { + CountDownLatch shutDownLatch = new CountDownLatch(1); + seedNode1.shutDown(() -> shutDownLatch.countDown()); + shutDownLatch.await(); + } + if (seedNode2 != null) { + CountDownLatch shutDownLatch = new CountDownLatch(1); + seedNode2.shutDown(() -> shutDownLatch.countDown()); + shutDownLatch.await(); + } + if (seedNode3 != null) { + CountDownLatch shutDownLatch = new CountDownLatch(1); + seedNode3.shutDown(() -> shutDownLatch.countDown()); + shutDownLatch.await(); + } + } + + // @Test + public void testSingleSeedNode() throws InterruptedException { + LocalhostNetworkNode.setSimulateTorDelayTorNode(0); + LocalhostNetworkNode.setSimulateTorDelayHiddenService(0); + seedNodes = new ArrayList<>(); + seedNodes.add(new Address("localhost:8001")); + seedNode1 = new SeedNode(); + latch = new CountDownLatch(2); + seedNode1.createAndStartP2PService(null, null, 8001, useLocalhost, seedNodes, new P2PServiceListener() { + @Override + public void onAllDataReceived() { + latch.countDown(); + } + + @Override + public void onTorNodeReady() { + + } + + @Override + public void onAuthenticated() { + } + + @Override + public void onHiddenServiceReady() { + latch.countDown(); + } + + @Override + public void onSetupFailed(Throwable throwable) { + + } + }); + P2PService p2PService1 = seedNode1.getP2PService(); + latch.await(); + Thread.sleep(500); + Assert.assertEquals(0, p2PService1.getRouting().getAllNeighbors().size()); + } + + @Test + public void test2SeedNodes() throws InterruptedException { + LocalhostNetworkNode.setSimulateTorDelayTorNode(0); + LocalhostNetworkNode.setSimulateTorDelayHiddenService(0); + seedNodes = new ArrayList<>(); + seedNodes.add(new Address("localhost:8001")); + seedNodes.add(new Address("localhost:8002")); + + latch = new CountDownLatch(6); + + seedNode1 = new SeedNode(); + seedNode1.createAndStartP2PService(null, null, 8001, useLocalhost, seedNodes, new P2PServiceListener() { + @Override + public void onAllDataReceived() { + latch.countDown(); + } + + @Override + public void onTorNodeReady() { + + } + + @Override + public void onAuthenticated() { + latch.countDown(); + } + + @Override + public void onHiddenServiceReady() { + latch.countDown(); + } + + @Override + public void onSetupFailed(Throwable throwable) { + + } + }); + P2PService p2PService1 = seedNode1.getP2PService(); + + Thread.sleep(500); + + seedNode2 = new SeedNode(); + seedNode2.createAndStartP2PService(null, null, 8002, useLocalhost, seedNodes, new P2PServiceListener() { + @Override + public void onAllDataReceived() { + latch.countDown(); + } + + @Override + public void onTorNodeReady() { + + } + + @Override + public void onAuthenticated() { + latch.countDown(); + } + + @Override + public void onHiddenServiceReady() { + latch.countDown(); + } + + @Override + public void onSetupFailed(Throwable throwable) { + + } + }); + P2PService p2PService2 = seedNode2.getP2PService(); + latch.await(); + Assert.assertEquals(1, p2PService1.getRouting().getAllNeighbors().size()); + Assert.assertEquals(1, p2PService2.getRouting().getAllNeighbors().size()); + } + + // @Test + public void testAuthentication() throws InterruptedException { + log.debug("### start"); + LocalhostNetworkNode.setSimulateTorDelayTorNode(0); + LocalhostNetworkNode.setSimulateTorDelayHiddenService(0); + SeedNode seedNode1 = getAndStartSeedNode(8001); + log.debug("### seedNode1"); + Thread.sleep(100); + log.debug("### seedNode1 100"); + Thread.sleep(1000); + SeedNode seedNode2 = getAndStartSeedNode(8002); + + // authentication: + // node2 -> node1 RequestAuthenticationMessage + // node1: close connection + // node1 -> node2 ChallengeMessage on new connection + // node2: authentication to node1 done if nonce ok + // node2 -> node1 GetNeighborsMessage + // node1: authentication to node2 done if nonce ok + // node1 -> node2 NeighborsMessage + + // first authentication from seedNode2 to seedNode1, then from seedNode1 to seedNode2 + CountDownLatch latch1 = new CountDownLatch(2); + AuthenticationListener routingListener1 = new AuthenticationListener() { + @Override + public void onConnectionAuthenticated(Connection connection) { + log.debug("onConnectionAuthenticated " + connection); + latch1.countDown(); + } + }; + seedNode1.getP2PService().getRouting().addRoutingListener(routingListener1); + + AuthenticationListener routingListener2 = new AuthenticationListener() { + @Override + public void onConnectionAuthenticated(Connection connection) { + log.debug("onConnectionAuthenticated " + connection); + latch1.countDown(); + } + }; + seedNode2.getP2PService().getRouting().addRoutingListener(routingListener2); + latch1.await(); + seedNode1.getP2PService().getRouting().removeRoutingListener(routingListener1); + seedNode2.getP2PService().getRouting().removeRoutingListener(routingListener2); + + // wait until Neighbors msg finished + Thread.sleep(sleepTime); + + // authentication: + // authentication from seedNode3 to seedNode1, then from seedNode1 to seedNode3 + // authentication from seedNode3 to seedNode2, then from seedNode2 to seedNode3 + SeedNode seedNode3 = getAndStartSeedNode(8003); + CountDownLatch latch2 = new CountDownLatch(3); + seedNode1.getP2PService().getRouting().addRoutingListener(new AuthenticationListener() { + @Override + public void onConnectionAuthenticated(Connection connection) { + log.debug("onConnectionAuthenticated " + connection); + latch2.countDown(); + } + }); + seedNode2.getP2PService().getRouting().addRoutingListener(new AuthenticationListener() { + @Override + public void onConnectionAuthenticated(Connection connection) { + log.debug("onConnectionAuthenticated " + connection); + latch2.countDown(); + } + }); + seedNode3.getP2PService().getRouting().addRoutingListener(new AuthenticationListener() { + @Override + public void onConnectionAuthenticated(Connection connection) { + log.debug("onConnectionAuthenticated " + connection); + latch2.countDown(); + } + }); + latch2.await(); + + // wait until Neighbors msg finished + Thread.sleep(sleepTime); + + + CountDownLatch shutDownLatch = new CountDownLatch(3); + seedNode1.shutDown(() -> shutDownLatch.countDown()); + seedNode2.shutDown(() -> shutDownLatch.countDown()); + seedNode3.shutDown(() -> shutDownLatch.countDown()); + shutDownLatch.await(); + } + + //@Test + public void testAuthenticationWithDisconnect() throws InterruptedException { + LocalhostNetworkNode.setSimulateTorDelayTorNode(0); + LocalhostNetworkNode.setSimulateTorDelayHiddenService(0); + SeedNode seedNode1 = getAndStartSeedNode(8001); + SeedNode seedNode2 = getAndStartSeedNode(8002); + + // authentication: + // node2 -> node1 RequestAuthenticationMessage + // node1: close connection + // node1 -> node2 ChallengeMessage on new connection + // node2: authentication to node1 done if nonce ok + // node2 -> node1 GetNeighborsMessage + // node1: authentication to node2 done if nonce ok + // node1 -> node2 NeighborsMessage + + // first authentication from seedNode2 to seedNode1, then from seedNode1 to seedNode2 + CountDownLatch latch1 = new CountDownLatch(2); + AuthenticationListener routingListener1 = new AuthenticationListener() { + @Override + public void onConnectionAuthenticated(Connection connection) { + log.debug("onConnectionAuthenticated " + connection); + latch1.countDown(); + } + }; + seedNode1.getP2PService().getRouting().addRoutingListener(routingListener1); + + AuthenticationListener routingListener2 = new AuthenticationListener() { + @Override + public void onConnectionAuthenticated(Connection connection) { + log.debug("onConnectionAuthenticated " + connection); + latch1.countDown(); + } + }; + seedNode2.getP2PService().getRouting().addRoutingListener(routingListener2); + latch1.await(); + + // shut down node 2 + Thread.sleep(sleepTime); + seedNode1.getP2PService().getRouting().removeRoutingListener(routingListener1); + seedNode2.getP2PService().getRouting().removeRoutingListener(routingListener2); + CountDownLatch shutDownLatch1 = new CountDownLatch(1); + seedNode2.shutDown(() -> shutDownLatch1.countDown()); + shutDownLatch1.await(); + + // restart node 2 + seedNode2 = getAndStartSeedNode(8002); + CountDownLatch latch3 = new CountDownLatch(1); + routingListener2 = new AuthenticationListener() { + @Override + public void onConnectionAuthenticated(Connection connection) { + log.debug("onConnectionAuthenticated " + connection); + latch3.countDown(); + } + }; + seedNode2.getP2PService().getRouting().addRoutingListener(routingListener2); + latch3.await(); + + Thread.sleep(sleepTime); + + CountDownLatch shutDownLatch = new CountDownLatch(2); + seedNode1.shutDown(() -> shutDownLatch.countDown()); + seedNode2.shutDown(() -> shutDownLatch.countDown()); + shutDownLatch.await(); + } + + //@Test + public void testAuthenticationWithManyNodes() throws InterruptedException { + int authentications = 0; + int length = 3; + SeedNode[] nodes = new SeedNode[length]; + for (int i = 0; i < length; i++) { + SeedNode node = getAndStartSeedNode(8001 + i); + nodes[i] = node; + + latch = new CountDownLatch(i * 2); + authentications += (i * 2); + node.getP2PService().getRouting().addRoutingListener(new AuthenticationListener() { + @Override + public void onConnectionAuthenticated(Connection connection) { + log.debug("onConnectionAuthenticated " + connection); + latch.countDown(); + } + }); + latch.await(); + Thread.sleep(sleepTime); + } + + log.debug("total authentications " + authentications); + Profiler.printSystemLoad(log); + // total authentications at 8 nodes = 56 + // total authentications at com nodes = 90, System load (nr. threads/used memory (MB)): 170/20 + // total authentications at 20 nodes = 380, System load (nr. threads/used memory (MB)): 525/46 + for (int i = 0; i < length; i++) { + nodes[i].getP2PService().getRouting().printConnectedNeighborsMap(); + nodes[i].getP2PService().getRouting().printReportedNeighborsMap(); + } + + CountDownLatch shutDownLatch = new CountDownLatch(length); + for (int i = 0; i < length; i++) { + nodes[i].shutDown(() -> shutDownLatch.countDown()); + } + shutDownLatch.await(); + } + + private SeedNode getAndStartSeedNode(int port) throws InterruptedException { + SeedNode seedNode = new SeedNode(); + + latch = new CountDownLatch(1); + seedNode.createAndStartP2PService(null, null, port, useLocalhost, seedNodes, new P2PServiceListener() { + @Override + public void onAllDataReceived() { + latch.countDown(); + } + + @Override + public void onTorNodeReady() { + + } + + @Override + public void onAuthenticated() { + } + + @Override + public void onHiddenServiceReady() { + + } + + @Override + public void onSetupFailed(Throwable throwable) { + + } + }); + latch.await(); + Thread.sleep(sleepTime); + return seedNode; + } +} diff --git a/network/src/test/java/io/bitsquare/p2p/storage/ProtectedDataStorageTest.java b/network/src/test/java/io/bitsquare/p2p/storage/ProtectedDataStorageTest.java new file mode 100644 index 0000000000..acc122633b --- /dev/null +++ b/network/src/test/java/io/bitsquare/p2p/storage/ProtectedDataStorageTest.java @@ -0,0 +1,338 @@ +package io.bitsquare.p2p.storage; + +import io.bitsquare.common.UserThread; +import io.bitsquare.common.crypto.CryptoException; +import io.bitsquare.common.crypto.CryptoUtil; +import io.bitsquare.common.crypto.KeyRing; +import io.bitsquare.common.crypto.KeyStorage; +import io.bitsquare.common.util.Utilities; +import io.bitsquare.crypto.EncryptionService; +import io.bitsquare.p2p.Address; +import io.bitsquare.p2p.TestUtils; +import io.bitsquare.p2p.messaging.SealedAndSignedMessage; +import io.bitsquare.p2p.mocks.MockMessage; +import io.bitsquare.p2p.network.NetworkNode; +import io.bitsquare.p2p.routing.Routing; +import io.bitsquare.p2p.storage.data.DataAndSeqNr; +import io.bitsquare.p2p.storage.data.ExpirableMailboxPayload; +import io.bitsquare.p2p.storage.data.ProtectedData; +import io.bitsquare.p2p.storage.data.ProtectedMailboxData; +import io.bitsquare.p2p.storage.mocks.MockData; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.security.*; +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.Date; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; + +public class ProtectedDataStorageTest { + private static final Logger log = LoggerFactory.getLogger(ProtectedDataStorageTest.class); + + boolean useClearNet = true; + private ArrayList
seedNodes = new ArrayList<>(); + private NetworkNode networkNode1; + private Routing routing1; + private EncryptionService encryptionService1, encryptionService2; + private ProtectedExpirableDataStorage dataStorage1; + private KeyPair storageSignatureKeyPair1, storageSignatureKeyPair2; + private KeyRing keyRing1, keyRing2; + private MockData mockData; + private int sleepTime = 100; + + @Before + public void setup() throws InterruptedException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, CryptoException, SignatureException, InvalidKeyException { + UserThread.executor = Executors.newSingleThreadExecutor(); + ProtectedExpirableDataStorage.CHECK_TTL_INTERVAL = 10 * 60 * 1000; + + keyRing1 = new KeyRing(new KeyStorage(new File("temp_keyStorage1"))); + storageSignatureKeyPair1 = keyRing1.getStorageSignatureKeyPair(); + encryptionService1 = new EncryptionService(keyRing1); + networkNode1 = TestUtils.getAndStartSeedNode(8001, encryptionService1, keyRing1, useClearNet, seedNodes).getP2PService().getNetworkNode(); + routing1 = new Routing(networkNode1, seedNodes); + dataStorage1 = new ProtectedExpirableDataStorage(routing1, encryptionService1); + + // for mailbox + keyRing2 = new KeyRing(new KeyStorage(new File("temp_keyStorage2"))); + storageSignatureKeyPair2 = keyRing2.getStorageSignatureKeyPair(); + encryptionService2 = new EncryptionService(keyRing2); + + mockData = new MockData("mockData", keyRing1.getStorageSignatureKeyPair().getPublic()); + Thread.sleep(sleepTime); + } + + @After + public void tearDown() throws InterruptedException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, CryptoException, SignatureException, InvalidKeyException { + Thread.sleep(sleepTime); + if (dataStorage1 != null) dataStorage1.shutDown(); + if (routing1 != null) routing1.shutDown(); + + if (networkNode1 != null) { + CountDownLatch shutDownLatch = new CountDownLatch(1); + networkNode1.shutDown(() -> shutDownLatch.countDown()); + shutDownLatch.await(); + } + } + + @Test + public void testAddAndRemove() throws InterruptedException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, CryptoException, SignatureException, InvalidKeyException { + ProtectedData data = dataStorage1.getDataWithSignedSeqNr(mockData, storageSignatureKeyPair1); + Assert.assertTrue(dataStorage1.add(data, null)); + Assert.assertEquals(1, dataStorage1.getMap().size()); + + int newSequenceNumber = data.sequenceNumber + 1; + byte[] hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(data.expirablePayload, newSequenceNumber)); + byte[] signature = CryptoUtil.signStorageData(storageSignatureKeyPair1.getPrivate(), hashOfDataAndSeqNr); + ProtectedData dataToRemove = new ProtectedData(data.expirablePayload, data.ttl, data.ownerStoragePubKey, newSequenceNumber, signature); + Assert.assertTrue(dataStorage1.remove(dataToRemove, null)); + Assert.assertEquals(0, dataStorage1.getMap().size()); + } + + @Test + public void testExpirableData() throws InterruptedException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, CryptoException, SignatureException, InvalidKeyException { + ProtectedExpirableDataStorage.CHECK_TTL_INTERVAL = 10; + // CHECK_TTL_INTERVAL is used in constructor of ProtectedExpirableDataStorage so we recreate it here + dataStorage1 = new ProtectedExpirableDataStorage(routing1, encryptionService1); + mockData.ttl = 50; + + ProtectedData data = dataStorage1.getDataWithSignedSeqNr(mockData, storageSignatureKeyPair1); + Assert.assertTrue(dataStorage1.add(data, null)); + Thread.sleep(5); + Assert.assertEquals(1, dataStorage1.getMap().size()); + // still there + Thread.sleep(20); + Assert.assertEquals(1, dataStorage1.getMap().size()); + + Thread.sleep(40); + // now should be removed + Assert.assertEquals(0, dataStorage1.getMap().size()); + + // add with date in future + data = dataStorage1.getDataWithSignedSeqNr(mockData, storageSignatureKeyPair1); + int newSequenceNumber = data.sequenceNumber + 1; + byte[] hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(data.expirablePayload, newSequenceNumber)); + byte[] signature = CryptoUtil.signStorageData(storageSignatureKeyPair1.getPrivate(), hashOfDataAndSeqNr); + ProtectedData dataWithFutureDate = new ProtectedData(data.expirablePayload, data.ttl, data.ownerStoragePubKey, newSequenceNumber, signature); + dataWithFutureDate.date = new Date(new Date().getTime() + 60 * 60 * sleepTime); + // force serialisation (date check is done in readObject) + ProtectedData newData = Utilities.byteArrayToObject(Utilities.objectToByteArray(dataWithFutureDate)); + Assert.assertTrue(dataStorage1.add(newData, null)); + Thread.sleep(5); + Assert.assertEquals(1, dataStorage1.getMap().size()); + Thread.sleep(50); + Assert.assertEquals(0, dataStorage1.getMap().size()); + } + + @Test + public void testMultiAddRemoveProtectedData() throws InterruptedException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, CryptoException, SignatureException, InvalidKeyException { + MockData mockData = new MockData("msg1", keyRing1.getStorageSignatureKeyPair().getPublic()); + ProtectedData data = dataStorage1.getDataWithSignedSeqNr(mockData, storageSignatureKeyPair1); + Assert.assertTrue(dataStorage1.add(data, null)); + + // remove with not updated seq nr -> failure + int newSequenceNumber = 0; + byte[] hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(data.expirablePayload, newSequenceNumber)); + byte[] signature = CryptoUtil.signStorageData(storageSignatureKeyPair1.getPrivate(), hashOfDataAndSeqNr); + ProtectedData dataToRemove = new ProtectedData(data.expirablePayload, data.ttl, data.ownerStoragePubKey, newSequenceNumber, signature); + Assert.assertFalse(dataStorage1.remove(dataToRemove, null)); + + // remove with too high updated seq nr -> ok + newSequenceNumber = 2; + hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(data.expirablePayload, newSequenceNumber)); + signature = CryptoUtil.signStorageData(storageSignatureKeyPair1.getPrivate(), hashOfDataAndSeqNr); + dataToRemove = new ProtectedData(data.expirablePayload, data.ttl, data.ownerStoragePubKey, newSequenceNumber, signature); + Assert.assertTrue(dataStorage1.remove(dataToRemove, null)); + + // add with updated seq nr below previous -> failure + newSequenceNumber = 1; + hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(data.expirablePayload, newSequenceNumber)); + signature = CryptoUtil.signStorageData(storageSignatureKeyPair1.getPrivate(), hashOfDataAndSeqNr); + ProtectedData dataToAdd = new ProtectedData(data.expirablePayload, data.ttl, data.ownerStoragePubKey, newSequenceNumber, signature); + Assert.assertFalse(dataStorage1.add(dataToAdd, null)); + + // add with updated seq nr over previous -> ok + newSequenceNumber = 3; + hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(data.expirablePayload, newSequenceNumber)); + signature = CryptoUtil.signStorageData(storageSignatureKeyPair1.getPrivate(), hashOfDataAndSeqNr); + dataToAdd = new ProtectedData(data.expirablePayload, data.ttl, data.ownerStoragePubKey, newSequenceNumber, signature); + Assert.assertTrue(dataStorage1.add(dataToAdd, null)); + + // add with same seq nr -> failure + newSequenceNumber = 3; + hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(data.expirablePayload, newSequenceNumber)); + signature = CryptoUtil.signStorageData(storageSignatureKeyPair1.getPrivate(), hashOfDataAndSeqNr); + dataToAdd = new ProtectedData(data.expirablePayload, data.ttl, data.ownerStoragePubKey, newSequenceNumber, signature); + Assert.assertFalse(dataStorage1.add(dataToAdd, null)); + + // add with same data but higher seq nr. -> ok, ignore + newSequenceNumber = 4; + hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(data.expirablePayload, newSequenceNumber)); + signature = CryptoUtil.signStorageData(storageSignatureKeyPair1.getPrivate(), hashOfDataAndSeqNr); + dataToAdd = new ProtectedData(data.expirablePayload, data.ttl, data.ownerStoragePubKey, newSequenceNumber, signature); + Assert.assertTrue(dataStorage1.add(dataToAdd, null)); + + // remove with with same seq nr as prev. ignored -> failed + newSequenceNumber = 4; + hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(data.expirablePayload, newSequenceNumber)); + signature = CryptoUtil.signStorageData(storageSignatureKeyPair1.getPrivate(), hashOfDataAndSeqNr); + dataToRemove = new ProtectedData(data.expirablePayload, data.ttl, data.ownerStoragePubKey, newSequenceNumber, signature); + Assert.assertFalse(dataStorage1.remove(dataToRemove, null)); + + // remove with with higher seq nr -> ok + newSequenceNumber = 5; + hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(data.expirablePayload, newSequenceNumber)); + signature = CryptoUtil.signStorageData(storageSignatureKeyPair1.getPrivate(), hashOfDataAndSeqNr); + dataToRemove = new ProtectedData(data.expirablePayload, data.ttl, data.ownerStoragePubKey, newSequenceNumber, signature); + Assert.assertTrue(dataStorage1.remove(dataToRemove, null)); + } + + @Test + public void testAddAndRemoveMailboxData() throws InterruptedException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, CryptoException, SignatureException, InvalidKeyException { + // sender + MockMessage mockMessage = new MockMessage("MockMessage"); + SealedAndSignedMessage sealedAndSignedMessage = encryptionService1.encryptAndSignMessage(keyRing1.getPubKeyRing(), mockMessage); + ExpirableMailboxPayload expirableMailboxPayload = new ExpirableMailboxPayload(sealedAndSignedMessage, + keyRing1.getStorageSignatureKeyPair().getPublic(), + keyRing2.getStorageSignatureKeyPair().getPublic()); + + ProtectedMailboxData data = dataStorage1.getMailboxDataWithSignedSeqNr(expirableMailboxPayload, storageSignatureKeyPair1, storageSignatureKeyPair2.getPublic()); + Assert.assertTrue(dataStorage1.add(data, null)); + Thread.sleep(sleepTime); + Assert.assertEquals(1, dataStorage1.getMap().size()); + + // receiver (storageSignatureKeyPair2) + int newSequenceNumber = data.sequenceNumber + 1; + byte[] hashOfDataAndSeqNr = CryptoUtil.getHash(new DataAndSeqNr(data.expirablePayload, newSequenceNumber)); + + byte[] signature; + ProtectedMailboxData dataToRemove; + + // wrong sig -> fail + signature = CryptoUtil.signStorageData(storageSignatureKeyPair1.getPrivate(), hashOfDataAndSeqNr); + dataToRemove = new ProtectedMailboxData(expirableMailboxPayload, data.ttl, storageSignatureKeyPair2.getPublic(), newSequenceNumber, signature, storageSignatureKeyPair2.getPublic()); + Assert.assertFalse(dataStorage1.removeMailboxData(dataToRemove, null)); + + // wrong seq nr + signature = CryptoUtil.signStorageData(storageSignatureKeyPair2.getPrivate(), hashOfDataAndSeqNr); + dataToRemove = new ProtectedMailboxData(expirableMailboxPayload, data.ttl, storageSignatureKeyPair2.getPublic(), data.sequenceNumber, signature, storageSignatureKeyPair2.getPublic()); + Assert.assertFalse(dataStorage1.removeMailboxData(dataToRemove, null)); + + // wrong signingKey + signature = CryptoUtil.signStorageData(storageSignatureKeyPair2.getPrivate(), hashOfDataAndSeqNr); + dataToRemove = new ProtectedMailboxData(expirableMailboxPayload, data.ttl, data.ownerStoragePubKey, newSequenceNumber, signature, storageSignatureKeyPair2.getPublic()); + Assert.assertFalse(dataStorage1.removeMailboxData(dataToRemove, null)); + + // wrong peerPubKey + signature = CryptoUtil.signStorageData(storageSignatureKeyPair2.getPrivate(), hashOfDataAndSeqNr); + dataToRemove = new ProtectedMailboxData(expirableMailboxPayload, data.ttl, storageSignatureKeyPair2.getPublic(), newSequenceNumber, signature, storageSignatureKeyPair1.getPublic()); + Assert.assertFalse(dataStorage1.removeMailboxData(dataToRemove, null)); + + // receiver can remove it (storageSignatureKeyPair2) -> all ok + Assert.assertEquals(1, dataStorage1.getMap().size()); + signature = CryptoUtil.signStorageData(storageSignatureKeyPair2.getPrivate(), hashOfDataAndSeqNr); + dataToRemove = new ProtectedMailboxData(expirableMailboxPayload, data.ttl, storageSignatureKeyPair2.getPublic(), newSequenceNumber, signature, storageSignatureKeyPair2.getPublic()); + Assert.assertTrue(dataStorage1.removeMailboxData(dataToRemove, null)); + + Assert.assertEquals(0, dataStorage1.getMap().size()); + } + + + /*@Test + public void testTryToHack() throws InterruptedException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, CryptoException, SignatureException, InvalidKeyException { + ProtectedData data = dataStorage1.getDataWithSignedSeqNr(mockData, storageSignatureKeyPair1); + Assert.assertTrue(dataStorage1.add(data, null)); + Thread.sleep(sleepTime); + Assert.assertEquals(1, dataStorage1.getMap().size()); + Assert.assertEquals(1, dataStorage2.getMap().size()); + + // hackers key pair is storageSignatureKeyPair2 + // change seq nr. and signature: fails on both own and peers dataStorage + int newSequenceNumber = data.sequenceNumber + 1; + byte[] hashOfDataAndSeqNr = cryptoService2.getHash(new DataAndSeqNr(data.expirablePayload, newSequenceNumber)); + byte[] signature = cryptoService2.signStorageData(storageSignatureKeyPair2.getPrivate(), hashOfDataAndSeqNr); + ProtectedData dataToAdd = new ProtectedData(data.expirablePayload, data.ttl, data.ownerStoragePubKey, newSequenceNumber, signature); + Assert.assertFalse(dataStorage1.add(dataToAdd, null)); + Assert.assertFalse(dataStorage2.add(dataToAdd, null)); + + // change seq nr. and signature and data pub key. fails on peers dataStorage, succeeds on own dataStorage + newSequenceNumber = data.sequenceNumber + 2; + hashOfDataAndSeqNr = cryptoService2.getHash(new DataAndSeqNr(data.expirablePayload, newSequenceNumber)); + signature = cryptoService2.signStorageData(storageSignatureKeyPair2.getPrivate(), hashOfDataAndSeqNr); + dataToAdd = new ProtectedData(data.expirablePayload, data.ttl, storageSignatureKeyPair2.getPublic(), newSequenceNumber, signature); + Assert.assertTrue(dataStorage2.add(dataToAdd, null)); + Assert.assertFalse(dataStorage1.add(dataToAdd, null)); + Thread.sleep(sleepTime); + Assert.assertEquals(1, dataStorage2.getMap().size()); + Thread.sleep(sleepTime); + Assert.assertEquals(1, dataStorage1.getMap().size()); + Assert.assertEquals(data, dataStorage1.getMap().values().stream().findFirst().get()); + Assert.assertEquals(dataToAdd, dataStorage2.getMap().values().stream().findFirst().get()); + Assert.assertNotEquals(data, dataToAdd); + + newSequenceNumber = data.sequenceNumber + 3; + hashOfDataAndSeqNr = cryptoService1.getHash(new DataAndSeqNr(data.expirablePayload, newSequenceNumber)); + signature = cryptoService1.signStorageData(storageSignatureKeyPair1.getPrivate(), hashOfDataAndSeqNr); + ProtectedData dataToRemove = new ProtectedData(data.expirablePayload, data.ttl, data.ownerStoragePubKey, newSequenceNumber, signature); + Assert.assertTrue(dataStorage1.remove(dataToRemove, null)); + Assert.assertEquals(0, dataStorage1.getMap().size()); + }*/ + + /* //@Test + public void testTryToHackMailboxData() throws InterruptedException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, CryptoException, SignatureException, InvalidKeyException { + MockMessage mockMessage = new MockMessage("MockMessage"); + SealedAndSignedMessage sealedAndSignedMessage = cryptoService1.encryptAndSignMessage(keyRing1.getPubKeyRing(), mockMessage); + ExpirableMailboxPayload expirableMailboxPayload = new ExpirableMailboxPayload(sealedAndSignedMessage, + keyRing1.getStorageSignatureKeyPair().getPublic(), + keyRing2.getStorageSignatureKeyPair().getPublic()); + + // sender + ProtectedMailboxData data = dataStorage1.getMailboxDataWithSignedSeqNr(expirableMailboxPayload, storageSignatureKeyPair1, storageSignatureKeyPair2.getPublic()); + Assert.assertTrue(dataStorage1.add(data, null)); + Thread.sleep(sleepTime); + Assert.assertEquals(1, dataStorage1.getMap().size()); + + // receiver (storageSignatureKeyPair2) + int newSequenceNumber = data.sequenceNumber + 1; + byte[] hashOfDataAndSeqNr = cryptoService2.getHash(new DataAndSeqNr(expirableMailboxPayload, newSequenceNumber)); + + byte[] signature; + ProtectedMailboxData dataToRemove; + + // wrong sig -> fail + signature = cryptoService2.signStorageData(storageSignatureKeyPair1.getPrivate(), hashOfDataAndSeqNr); + dataToRemove = new ProtectedMailboxData(expirableMailboxPayload, data.ttl, storageSignatureKeyPair2.getPublic(), newSequenceNumber, signature, storageSignatureKeyPair2.getPublic()); + Assert.assertFalse(dataStorage1.removeMailboxData(dataToRemove, null)); + + // wrong seq nr + signature = cryptoService2.signStorageData(storageSignatureKeyPair2.getPrivate(), hashOfDataAndSeqNr); + dataToRemove = new ProtectedMailboxData(expirableMailboxPayload, data.ttl, storageSignatureKeyPair2.getPublic(), data.sequenceNumber, signature, storageSignatureKeyPair2.getPublic()); + Assert.assertFalse(dataStorage1.removeMailboxData(dataToRemove, null)); + + // wrong signingKey + signature = cryptoService2.signStorageData(storageSignatureKeyPair2.getPrivate(), hashOfDataAndSeqNr); + dataToRemove = new ProtectedMailboxData(expirableMailboxPayload, data.ttl, data.ownerStoragePubKey, newSequenceNumber, signature, storageSignatureKeyPair2.getPublic()); + Assert.assertFalse(dataStorage1.removeMailboxData(dataToRemove, null)); + + // wrong peerPubKey + signature = cryptoService2.signStorageData(storageSignatureKeyPair2.getPrivate(), hashOfDataAndSeqNr); + dataToRemove = new ProtectedMailboxData(expirableMailboxPayload, data.ttl, storageSignatureKeyPair2.getPublic(), newSequenceNumber, signature, storageSignatureKeyPair1.getPublic()); + Assert.assertFalse(dataStorage1.removeMailboxData(dataToRemove, null)); + + // all ok + Assert.assertEquals(1, dataStorage1.getMap().size()); + signature = cryptoService2.signStorageData(storageSignatureKeyPair2.getPrivate(), hashOfDataAndSeqNr); + dataToRemove = new ProtectedMailboxData(expirableMailboxPayload, data.ttl, storageSignatureKeyPair2.getPublic(), newSequenceNumber, signature, storageSignatureKeyPair2.getPublic()); + Assert.assertTrue(dataStorage1.removeMailboxData(dataToRemove, null)); + + Assert.assertEquals(0, dataStorage1.getMap().size()); + } +*/ +} diff --git a/network/src/test/java/io/bitsquare/p2p/storage/mocks/MockData.java b/network/src/test/java/io/bitsquare/p2p/storage/mocks/MockData.java new file mode 100644 index 0000000000..b103dbbc83 --- /dev/null +++ b/network/src/test/java/io/bitsquare/p2p/storage/mocks/MockData.java @@ -0,0 +1,49 @@ +package io.bitsquare.p2p.storage.mocks; + +import io.bitsquare.p2p.storage.data.PubKeyProtectedExpirablePayload; + +import java.security.PublicKey; + +public class MockData implements PubKeyProtectedExpirablePayload { + public final String msg; + public final PublicKey publicKey; + public long ttl; + + public MockData(String msg, PublicKey publicKey) { + this.msg = msg; + this.publicKey = publicKey; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof MockData)) return false; + + MockData that = (MockData) o; + + return !(msg != null ? !msg.equals(that.msg) : that.msg != null); + + } + + @Override + public int hashCode() { + return msg != null ? msg.hashCode() : 0; + } + + @Override + public String toString() { + return "MockData{" + + "msg='" + msg + '\'' + + '}'; + } + + @Override + public long getTTL() { + return ttl; + } + + @Override + public PublicKey getPubKey() { + return publicKey; + } +}