From d832cfe74be2f6e182d4a9cab017bc4a3d912be6 Mon Sep 17 00:00:00 2001
From: Manfred Karrer <mk@nucleo.io>
Date: Sat, 7 Mar 2015 23:46:57 +0100
Subject: [PATCH] Add CreateOfferProtocolTests

---
 .../app/{cli => bootstrap}/BootstrapNode.java |   2 +-
 .../{cli => bootstrap}/BootstrapNodeMain.java |   2 +-
 .../bitsquare/app/gui/BitsquareAppMain.java   |   1 +
 .../java/io/bitsquare/btc/AddressEntry.java   |  14 +
 .../main/java/io/bitsquare/btc/FeePolicy.java |   4 +-
 .../java/io/bitsquare/btc/WalletService.java  |   5 +-
 .../io/bitsquare/gui/main/MainViewModel.java  |  16 +-
 .../gui/main/trade/offerbook/OfferBook.java   |  26 +-
 .../msg/tomp2p/BootstrappedPeerBuilder.java   |   2 +-
 .../msg/tomp2p/TomP2PMessageService.java      |   2 +-
 .../io/bitsquare/network/BootstrapNodes.java  |   2 +-
 .../java/io/bitsquare/offer/OfferModule.java  |   6 -
 ...erRepository.java => RemoteOfferBook.java} |   5 +-
 ...erRepository.java => TomP2POfferBook.java} |  35 +-
 .../offer/tomp2p/TomP2POfferModule.java       |  27 +-
 .../java/io/bitsquare/trade/TradeManager.java |  18 +-
 .../java/io/bitsquare/trade/TradeMessage.java |  29 ++
 .../java/io/bitsquare/trade/TradeState.java   |  29 ++
 .../createoffer/CreateOfferCoordinator.java   | 101 ------
 .../createoffer/CreateOfferProtocol.java      | 146 ++++++++
 .../main/java/io/bitsquare/util/Bytes.java    |  68 ++++
 .../java/io/bitsquare/util/Utilities.java     |  25 ++
 gui/src/main/resources/logback.xml            |   6 +-
 .../java/io/bitsquare/msg/TomP2PTests.java    |   2 +-
 .../createoffer/CreateOfferProtocolTest.java  | 318 ++++++++++++++++++
 net/pom.xml                                   |   2 +-
 26 files changed, 721 insertions(+), 172 deletions(-)
 rename gui/src/main/java/io/bitsquare/app/{cli => bootstrap}/BootstrapNode.java (99%)
 rename gui/src/main/java/io/bitsquare/app/{cli => bootstrap}/BootstrapNodeMain.java (97%)
 rename gui/src/main/java/io/bitsquare/offer/{OfferRepository.java => RemoteOfferBook.java} (92%)
 rename gui/src/main/java/io/bitsquare/offer/tomp2p/{TomP2POfferRepository.java => TomP2POfferBook.java} (92%)
 create mode 100644 gui/src/main/java/io/bitsquare/trade/TradeMessage.java
 create mode 100644 gui/src/main/java/io/bitsquare/trade/TradeState.java
 delete mode 100644 gui/src/main/java/io/bitsquare/trade/protocol/createoffer/CreateOfferCoordinator.java
 create mode 100644 gui/src/main/java/io/bitsquare/trade/protocol/createoffer/CreateOfferProtocol.java
 create mode 100644 gui/src/main/java/io/bitsquare/util/Bytes.java
 create mode 100644 gui/src/test/java/io/bitsquare/trade/protocol/createoffer/CreateOfferProtocolTest.java

diff --git a/gui/src/main/java/io/bitsquare/app/cli/BootstrapNode.java b/gui/src/main/java/io/bitsquare/app/bootstrap/BootstrapNode.java
similarity index 99%
rename from gui/src/main/java/io/bitsquare/app/cli/BootstrapNode.java
rename to gui/src/main/java/io/bitsquare/app/bootstrap/BootstrapNode.java
index 3ac6a27880..6b61548341 100644
--- a/gui/src/main/java/io/bitsquare/app/cli/BootstrapNode.java
+++ b/gui/src/main/java/io/bitsquare/app/bootstrap/BootstrapNode.java
@@ -15,7 +15,7 @@
  * along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
  */
 
-package io.bitsquare.app.cli;
+package io.bitsquare.app.bootstrap;
 
 import io.bitsquare.network.Node;
 
diff --git a/gui/src/main/java/io/bitsquare/app/cli/BootstrapNodeMain.java b/gui/src/main/java/io/bitsquare/app/bootstrap/BootstrapNodeMain.java
similarity index 97%
rename from gui/src/main/java/io/bitsquare/app/cli/BootstrapNodeMain.java
rename to gui/src/main/java/io/bitsquare/app/bootstrap/BootstrapNodeMain.java
index 186dccbbf4..6f3ddd1fe3 100644
--- a/gui/src/main/java/io/bitsquare/app/cli/BootstrapNodeMain.java
+++ b/gui/src/main/java/io/bitsquare/app/bootstrap/BootstrapNodeMain.java
@@ -15,7 +15,7 @@
  * along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
  */
 
-package io.bitsquare.app.cli;
+package io.bitsquare.app.bootstrap;
 
 import io.bitsquare.app.BitsquareEnvironment;
 import io.bitsquare.app.BitsquareExecutable;
diff --git a/gui/src/main/java/io/bitsquare/app/gui/BitsquareAppMain.java b/gui/src/main/java/io/bitsquare/app/gui/BitsquareAppMain.java
index e8a31672d4..517faa8022 100644
--- a/gui/src/main/java/io/bitsquare/app/gui/BitsquareAppMain.java
+++ b/gui/src/main/java/io/bitsquare/app/gui/BitsquareAppMain.java
@@ -58,6 +58,7 @@ public class BitsquareAppMain extends BitsquareExecutable {
         // We don't want to do the full argument parsing here as that might easily change in update versions
         // So we only handle the absolute minimum which is APP_NAME, APP_DATA_DIR_KEY and USER_DATA_DIR
         OptionParser parser = new OptionParser();
+        parser.allowsUnrecognizedOptions();
         parser.accepts(USER_DATA_DIR_KEY, description("User data directory", DEFAULT_USER_DATA_DIR))
                 .withRequiredArg();
         parser.accepts(APP_NAME_KEY, description("Application name", DEFAULT_APP_NAME))
diff --git a/gui/src/main/java/io/bitsquare/btc/AddressEntry.java b/gui/src/main/java/io/bitsquare/btc/AddressEntry.java
index 797a7814ff..16e28f1ab4 100644
--- a/gui/src/main/java/io/bitsquare/btc/AddressEntry.java
+++ b/gui/src/main/java/io/bitsquare/btc/AddressEntry.java
@@ -24,6 +24,8 @@ import org.bitcoinj.crypto.DeterministicKey;
 
 import java.io.Serializable;
 
+import java.util.Arrays;
+
 /**
  * Is a minimalistic wallet abstraction used to separate transactions between different activities like:
  * Registration, trade and arbiter deposit.
@@ -87,4 +89,16 @@ public class AddressEntry implements Serializable {
         TRADE,
         ARBITRATOR_DEPOSIT
     }
+
+    @Override
+    public String toString() {
+        return "AddressEntry{" +
+                "addressString=" + getAddress().toString() +
+                "key=" + key +
+                ", params=" + params +
+                ", addressContext=" + addressContext +
+                ", offerId='" + offerId + '\'' +
+                ", pubKeyHash=" + Arrays.toString(pubKeyHash) +
+                '}';
+    }
 }
diff --git a/gui/src/main/java/io/bitsquare/btc/FeePolicy.java b/gui/src/main/java/io/bitsquare/btc/FeePolicy.java
index dfadbe1980..c2fed1861b 100644
--- a/gui/src/main/java/io/bitsquare/btc/FeePolicy.java
+++ b/gui/src/main/java/io/bitsquare/btc/FeePolicy.java
@@ -55,8 +55,8 @@ public class FeePolicy {
                 takeOfferFeeAddress = "1BVxNn3T12veSK6DgqwU4Hdn7QHcDDRag7";
                 break;
             case REGTEST:
-                createOfferFeeAddress = "n2upbsaKAe4PD3cc4JfS7UCqPC5oNd7Ckg";
-                takeOfferFeeAddress = "n2upbsaKAe4PD3cc4JfS7UCqPC5oNd7Ckg";
+                createOfferFeeAddress = "mmdXHjPSmLCAShckfQ1jwnLYpbP2pKKF7y";
+                takeOfferFeeAddress = "mmdXHjPSmLCAShckfQ1jwnLYpbP2pKKF7y";
                 break;
             default:
                 throw new BitsquareException("Unknown bitcoin network: %s", bitcoinNetwork);
diff --git a/gui/src/main/java/io/bitsquare/btc/WalletService.java b/gui/src/main/java/io/bitsquare/btc/WalletService.java
index b73d59e66d..a5600f5c93 100644
--- a/gui/src/main/java/io/bitsquare/btc/WalletService.java
+++ b/gui/src/main/java/io/bitsquare/btc/WalletService.java
@@ -534,8 +534,9 @@ public class WalletService {
         sendRequest.shuffleOutputs = false;
         // we allow spending of unconfirmed tx (double spend risk is low and usability would suffer if we need to
         // wait for 1 confirmation)
-        sendRequest.coinSelector = new AddressBasedCoinSelector(params, getAddressInfoByTradeID(offerId), true);
-        sendRequest.changeAddress = getAddressInfoByTradeID(offerId).getAddress();
+        AddressEntry addressEntry = getAddressInfoByTradeID(offerId);
+        sendRequest.coinSelector = new AddressBasedCoinSelector(params, addressEntry, true);
+        sendRequest.changeAddress = addressEntry.getAddress();
         wallet.completeTx(sendRequest);
         printInputs("payCreateOfferFee", tx);
         return tx;
diff --git a/gui/src/main/java/io/bitsquare/gui/main/MainViewModel.java b/gui/src/main/java/io/bitsquare/gui/main/MainViewModel.java
index c822a7cff6..996ba9c551 100644
--- a/gui/src/main/java/io/bitsquare/gui/main/MainViewModel.java
+++ b/gui/src/main/java/io/bitsquare/gui/main/MainViewModel.java
@@ -160,9 +160,9 @@ class MainViewModel implements ViewModel {
                 error -> log.error(error.toString()),
                 () -> Platform.runLater(() -> setBitcoinNetworkSyncProgress(1.0)));
 
-        Observable<BootstrapState> message = messageService.init();
-        message.publish();
-        message.subscribe(
+        Observable<BootstrapState> messageObservable = messageService.init();
+        messageObservable.publish();
+        messageObservable.subscribe(
                 state -> Platform.runLater(() -> setBootstrapState(state)),
                 error -> Platform.runLater(() -> {
                     log.error(error.toString());
@@ -173,8 +173,8 @@ class MainViewModel implements ViewModel {
                 }),
                 () -> log.trace("message completed"));
 
-        Observable<Object> wallet = walletService.initialize(Platform::runLater);
-        wallet.subscribe(
+        Observable<Object> walletServiceObservable = walletService.initialize(Platform::runLater);
+        walletServiceObservable.subscribe(
                 next -> {
                     log.trace("wallet next");
                 },
@@ -186,8 +186,8 @@ class MainViewModel implements ViewModel {
                     log.trace("wallet completed");
                 });
 
-        Observable<UpdateProcess.State> updateProcess = this.updateProcess.getProcess();
-        updateProcess.subscribe(next -> {
+        Observable<UpdateProcess.State> updateProcessObservable = this.updateProcess.getProcess();
+        updateProcessObservable.subscribe(next -> {
                     log.trace("updateProcess next");
                 },
                 error -> {
@@ -197,7 +197,7 @@ class MainViewModel implements ViewModel {
                     log.trace("updateProcess completed");
                 });
 
-        Observable<?> allTasks = Observable.merge(message, wallet, updateProcess);
+        Observable<?> allTasks = Observable.merge(messageObservable, walletServiceObservable, updateProcessObservable);
         allTasks.subscribe(
                 next -> {
                 },
diff --git a/gui/src/main/java/io/bitsquare/gui/main/trade/offerbook/OfferBook.java b/gui/src/main/java/io/bitsquare/gui/main/trade/offerbook/OfferBook.java
index d0753a084a..ede8d01773 100644
--- a/gui/src/main/java/io/bitsquare/gui/main/trade/offerbook/OfferBook.java
+++ b/gui/src/main/java/io/bitsquare/gui/main/trade/offerbook/OfferBook.java
@@ -21,7 +21,7 @@ import io.bitsquare.bank.BankAccount;
 import io.bitsquare.locale.Country;
 import io.bitsquare.locale.CurrencyUtil;
 import io.bitsquare.offer.Offer;
-import io.bitsquare.offer.OfferRepository;
+import io.bitsquare.offer.RemoteOfferBook;
 import io.bitsquare.user.User;
 import io.bitsquare.util.Utilities;
 
@@ -50,11 +50,11 @@ public class OfferBook {
 
     private static final Logger log = LoggerFactory.getLogger(OfferBook.class);
 
-    private final OfferRepository offerRepository;
+    private final RemoteOfferBook remoteOfferBook;
     private final User user;
 
     private final ObservableList<OfferBookListItem> offerBookListItems = FXCollections.observableArrayList();
-    private final OfferRepository.Listener offerRepositoryListener;
+    private final RemoteOfferBook.Listener remoteOfferBookListener;
     private final ChangeListener<BankAccount> bankAccountChangeListener;
     private final ChangeListener<Number> invalidationListener;
     private String fiatCode;
@@ -68,14 +68,14 @@ public class OfferBook {
     ///////////////////////////////////////////////////////////////////////////////////////////
 
     @Inject
-    OfferBook(OfferRepository offerRepository, User user) {
-        this.offerRepository = offerRepository;
+    OfferBook(RemoteOfferBook remoteOfferBook, User user) {
+        this.remoteOfferBook = remoteOfferBook;
         this.user = user;
 
         bankAccountChangeListener = (observableValue, oldValue, newValue) -> setBankAccount(newValue);
         invalidationListener = (ov, oldValue, newValue) -> requestOffers();
 
-        offerRepositoryListener = new OfferRepository.Listener() {
+        remoteOfferBookListener = new RemoteOfferBook.Listener() {
             @Override
             public void onOfferAdded(Offer offer) {
                 addOfferToOfferBookListItems(offer);
@@ -142,15 +142,15 @@ public class OfferBook {
     private void addListeners() {
         log.debug("addListeners ");
         user.currentBankAccountProperty().addListener(bankAccountChangeListener);
-        offerRepository.addListener(offerRepositoryListener);
-        offerRepository.invalidationTimestampProperty().addListener(invalidationListener);
+        remoteOfferBook.addListener(remoteOfferBookListener);
+        remoteOfferBook.invalidationTimestampProperty().addListener(invalidationListener);
     }
 
     private void removeListeners() {
         log.debug("removeListeners ");
         user.currentBankAccountProperty().removeListener(bankAccountChangeListener);
-        offerRepository.removeListener(offerRepositoryListener);
-        offerRepository.invalidationTimestampProperty().removeListener(invalidationListener);
+        remoteOfferBook.removeListener(remoteOfferBookListener);
+        remoteOfferBook.invalidationTimestampProperty().removeListener(invalidationListener);
     }
 
     private void addOfferToOfferBookListItems(Offer offer) {
@@ -160,7 +160,7 @@ public class OfferBook {
     }
 
     private void requestOffers() {
-        offerRepository.getOffers(fiatCode);
+        remoteOfferBook.getOffers(fiatCode);
     }
 
 
@@ -173,11 +173,11 @@ public class OfferBook {
         addListeners();
         setBankAccount(user.getCurrentBankAccount().get());
         pollingTimer = Utilities.setInterval(3000, (animationTimer) -> {
-            offerRepository.requestInvalidationTimeStampFromDHT(fiatCode);
+            remoteOfferBook.requestInvalidationTimeStampFromDHT(fiatCode);
             return null;
         });
 
-        offerRepository.getOffers(fiatCode);
+        remoteOfferBook.getOffers(fiatCode);
     }
 
     private void stopPolling() {
diff --git a/gui/src/main/java/io/bitsquare/msg/tomp2p/BootstrappedPeerBuilder.java b/gui/src/main/java/io/bitsquare/msg/tomp2p/BootstrappedPeerBuilder.java
index 1756ba825c..93d8c016c1 100644
--- a/gui/src/main/java/io/bitsquare/msg/tomp2p/BootstrappedPeerBuilder.java
+++ b/gui/src/main/java/io/bitsquare/msg/tomp2p/BootstrappedPeerBuilder.java
@@ -68,7 +68,7 @@ import io.netty.util.concurrent.DefaultEventExecutorGroup;
 /**
  * Creates a DHT peer and bootstraps to the network via a bootstrap node
  */
-class BootstrappedPeerBuilder {
+public class BootstrappedPeerBuilder {
     private static final Logger log = LoggerFactory.getLogger(BootstrappedPeerBuilder.class);
 
     static final String BOOTSTRAP_NODE_KEY = "bootstrapNode";
diff --git a/gui/src/main/java/io/bitsquare/msg/tomp2p/TomP2PMessageService.java b/gui/src/main/java/io/bitsquare/msg/tomp2p/TomP2PMessageService.java
index 7e9323b56d..8dd5be54ab 100644
--- a/gui/src/main/java/io/bitsquare/msg/tomp2p/TomP2PMessageService.java
+++ b/gui/src/main/java/io/bitsquare/msg/tomp2p/TomP2PMessageService.java
@@ -66,7 +66,7 @@ import rx.Observable;
  * <p>
  * TODO: improve callbacks that Platform.runLater is not necessary. We call usually that methods form teh UI thread.
  */
-class TomP2PMessageService implements MessageService {
+public class TomP2PMessageService implements MessageService {
     private static final Logger log = LoggerFactory.getLogger(TomP2PMessageService.class);
     private static final String ARBITRATORS_ROOT = "ArbitratorsRoot";
 
diff --git a/gui/src/main/java/io/bitsquare/network/BootstrapNodes.java b/gui/src/main/java/io/bitsquare/network/BootstrapNodes.java
index bb2713a175..70643018b0 100644
--- a/gui/src/main/java/io/bitsquare/network/BootstrapNodes.java
+++ b/gui/src/main/java/io/bitsquare/network/BootstrapNodes.java
@@ -30,7 +30,7 @@ public interface BootstrapNodes {
     Node DEFAULT = DIGITAL_OCEAN_1;
 
     /**
-     * A locally-running {@link io.bitsquare.app.cli.BootstrapNode} instance.
+     * A locally-running {@link io.bitsquare.app.bootstrap.BootstrapNode} instance.
      * Typically used only for testing. Not included in results from {@link #all()}.
      */
     Node LOCALHOST = Node.at("localhost", "127.0.0.1");
diff --git a/gui/src/main/java/io/bitsquare/offer/OfferModule.java b/gui/src/main/java/io/bitsquare/offer/OfferModule.java
index 227c30656b..dc3be4bbab 100644
--- a/gui/src/main/java/io/bitsquare/offer/OfferModule.java
+++ b/gui/src/main/java/io/bitsquare/offer/OfferModule.java
@@ -27,10 +27,4 @@ public abstract class OfferModule extends BitsquareModule {
         super(env);
     }
 
-    @Override
-    protected void configure() {
-        bind(OfferRepository.class).to(offerRepository()).asEagerSingleton();
-    }
-
-    protected abstract Class<? extends OfferRepository> offerRepository();
 }
diff --git a/gui/src/main/java/io/bitsquare/offer/OfferRepository.java b/gui/src/main/java/io/bitsquare/offer/RemoteOfferBook.java
similarity index 92%
rename from gui/src/main/java/io/bitsquare/offer/OfferRepository.java
rename to gui/src/main/java/io/bitsquare/offer/RemoteOfferBook.java
index 2b8c60f0c7..a643cc34e1 100644
--- a/gui/src/main/java/io/bitsquare/offer/OfferRepository.java
+++ b/gui/src/main/java/io/bitsquare/offer/RemoteOfferBook.java
@@ -21,10 +21,13 @@ import io.bitsquare.util.task.FaultHandler;
 import io.bitsquare.util.task.ResultHandler;
 
 import java.util.List;
+import java.util.concurrent.Executor;
 
 import javafx.beans.property.LongProperty;
 
-public interface OfferRepository {
+public interface RemoteOfferBook {
+
+    void setExecutor(Executor executor);
 
     void getOffers(String fiatCode);
 
diff --git a/gui/src/main/java/io/bitsquare/offer/tomp2p/TomP2POfferRepository.java b/gui/src/main/java/io/bitsquare/offer/tomp2p/TomP2POfferBook.java
similarity index 92%
rename from gui/src/main/java/io/bitsquare/offer/tomp2p/TomP2POfferRepository.java
rename to gui/src/main/java/io/bitsquare/offer/tomp2p/TomP2POfferBook.java
index bfb8c850ff..2ed6c7f756 100644
--- a/gui/src/main/java/io/bitsquare/offer/tomp2p/TomP2POfferRepository.java
+++ b/gui/src/main/java/io/bitsquare/offer/tomp2p/TomP2POfferBook.java
@@ -19,7 +19,7 @@ package io.bitsquare.offer.tomp2p;
 
 import io.bitsquare.msg.tomp2p.TomP2PNode;
 import io.bitsquare.offer.Offer;
-import io.bitsquare.offer.OfferRepository;
+import io.bitsquare.offer.RemoteOfferBook;
 import io.bitsquare.util.task.FaultHandler;
 import io.bitsquare.util.task.ResultHandler;
 
@@ -28,10 +28,10 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
 
-import javafx.application.Platform;
 import javafx.beans.property.LongProperty;
 import javafx.beans.property.SimpleLongProperty;
 
@@ -48,20 +48,25 @@ import net.tomp2p.storage.Data;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-class TomP2POfferRepository implements OfferRepository {
+public class TomP2POfferBook implements RemoteOfferBook {
 
-    private static final Logger log = LoggerFactory.getLogger(TomP2POfferRepository.class);
+    private static final Logger log = LoggerFactory.getLogger(TomP2POfferBook.class);
 
     private final List<Listener> offerRepositoryListeners = new ArrayList<>();
     private final LongProperty invalidationTimestamp = new SimpleLongProperty(0);
 
     private final TomP2PNode p2pNode;
+    private Executor executor;
 
     @Inject
-    public TomP2POfferRepository(TomP2PNode p2pNode) {
+    public TomP2POfferBook(TomP2PNode p2pNode) {
         this.p2pNode = p2pNode;
     }
 
+    public void setExecutor(Executor executor) {
+        this.executor = executor;
+    }
+
     @Override
     public void addOffer(Offer offer, ResultHandler resultHandler, FaultHandler faultHandler) {
         Number160 locationKey = Number160.createHash(offer.getCurrency().getCurrencyCode());
@@ -78,13 +83,13 @@ class TomP2POfferRepository implements OfferRepository {
                 @Override
                 public void operationComplete(BaseFuture future) throws Exception {
                     if (future.isSuccess()) {
-                        Platform.runLater(() -> {
+                        executor.execute(() -> {
                             resultHandler.handleResult();
                             offerRepositoryListeners.stream().forEach(listener -> {
                                 try {
                                     Object offerDataObject = offerData.object();
                                     if (offerDataObject instanceof Offer) {
-                                        log.error("Added offer to DHT with ID: " + offerDataObject);
+                                        log.info("Added offer to DHT with ID: " + offerDataObject);
                                         listener.onOfferAdded((Offer) offerDataObject);
                                     }
                                 } catch (ClassNotFoundException | IOException e) {
@@ -102,15 +107,11 @@ class TomP2POfferRepository implements OfferRepository {
 
                 @Override
                 public void exceptionCaught(Throwable ex) throws Exception {
-                    Platform.runLater(() -> {
-                        faultHandler.handleFault("Failed to add offer to DHT", ex);
-                    });
+                    executor.execute(() -> faultHandler.handleFault("Failed to add offer to DHT", ex));
                 }
             });
         } catch (IOException ex) {
-            Platform.runLater(() -> {
-                faultHandler.handleFault("Failed to add offer to DHT", ex);
-            });
+            executor.execute(() -> faultHandler.handleFault("Failed to add offer to DHT", ex));
         }
     }
 
@@ -130,7 +131,7 @@ class TomP2POfferRepository implements OfferRepository {
                     // it might change in future to something like foundAndRemoved and notFound
                     // See discussion at: https://github.com/tomp2p/TomP2P/issues/57#issuecomment-62069840
 
-                    Platform.runLater(() -> {
+                    executor.execute(() -> {
                         offerRepositoryListeners.stream().forEach(listener -> {
                             try {
                                 Object offerDataObject = offerData.object();
@@ -182,7 +183,7 @@ class TomP2POfferRepository implements OfferRepository {
                             }
                         }
 
-                        Platform.runLater(() -> offerRepositoryListeners.stream().forEach(listener ->
+                        executor.execute(() -> offerRepositoryListeners.stream().forEach(listener ->
                                 listener.onOffersReceived(offers)));
                     }
 
@@ -193,7 +194,7 @@ class TomP2POfferRepository implements OfferRepository {
                     final Map<Number640, Data> dataMap = futureGet.dataMap();
                     if (dataMap == null || dataMap.size() == 0) {
                         log.trace("Get offers from DHT delivered empty dataMap.");
-                        Platform.runLater(() -> offerRepositoryListeners.stream().forEach(listener ->
+                        executor.execute(() -> offerRepositoryListeners.stream().forEach(listener ->
                                 listener.onOffersReceived(new ArrayList<>())));
                     }
                     else {
@@ -262,7 +263,7 @@ class TomP2POfferRepository implements OfferRepository {
                     Data data = futureGet.data();
                     if (data != null && data.object() instanceof Long) {
                         final Object object = data.object();
-                        Platform.runLater(() -> {
+                        executor.execute(() -> {
                             Long timeStamp = (Long) object;
                             //log.trace("Get invalidationTimestamp from DHT was successful. TimeStamp=" + timeStamp);
                             invalidationTimestamp.set(timeStamp);
diff --git a/gui/src/main/java/io/bitsquare/offer/tomp2p/TomP2POfferModule.java b/gui/src/main/java/io/bitsquare/offer/tomp2p/TomP2POfferModule.java
index af2262573d..935670be59 100644
--- a/gui/src/main/java/io/bitsquare/offer/tomp2p/TomP2POfferModule.java
+++ b/gui/src/main/java/io/bitsquare/offer/tomp2p/TomP2POfferModule.java
@@ -17,8 +17,15 @@
 
 package io.bitsquare.offer.tomp2p;
 
+import io.bitsquare.msg.tomp2p.TomP2PNode;
 import io.bitsquare.offer.OfferModule;
-import io.bitsquare.offer.OfferRepository;
+import io.bitsquare.offer.RemoteOfferBook;
+
+import com.google.inject.Provider;
+
+import javax.inject.Inject;
+
+import javafx.application.Platform;
 
 import org.springframework.core.env.Environment;
 
@@ -29,7 +36,21 @@ public class TomP2POfferModule extends OfferModule {
     }
 
     @Override
-    public Class<? extends OfferRepository> offerRepository() {
-        return TomP2POfferRepository.class;
+    protected void configure() {
+        bind(RemoteOfferBook.class).toProvider(RemoteOfferBookProvider.class).asEagerSingleton();
     }
 }
+
+class RemoteOfferBookProvider implements Provider<RemoteOfferBook> {
+    private final TomP2POfferBook remoteOfferBook;
+
+    @Inject
+    public RemoteOfferBookProvider(TomP2PNode p2pNode) {
+        remoteOfferBook = new TomP2POfferBook(p2pNode);
+        remoteOfferBook.setExecutor(Platform::runLater);
+    }
+
+    public RemoteOfferBook get() {
+        return remoteOfferBook;
+    }
+}
\ No newline at end of file
diff --git a/gui/src/main/java/io/bitsquare/trade/TradeManager.java b/gui/src/main/java/io/bitsquare/trade/TradeManager.java
index dd89ca4220..04ea0b0f73 100644
--- a/gui/src/main/java/io/bitsquare/trade/TradeManager.java
+++ b/gui/src/main/java/io/bitsquare/trade/TradeManager.java
@@ -28,10 +28,10 @@ import io.bitsquare.msg.listeners.OutgoingMessageListener;
 import io.bitsquare.network.Peer;
 import io.bitsquare.offer.Direction;
 import io.bitsquare.offer.Offer;
-import io.bitsquare.offer.OfferRepository;
+import io.bitsquare.offer.RemoteOfferBook;
 import io.bitsquare.persistence.Persistence;
 import io.bitsquare.trade.handlers.TransactionResultHandler;
-import io.bitsquare.trade.protocol.createoffer.CreateOfferCoordinator;
+import io.bitsquare.trade.protocol.createoffer.CreateOfferProtocol;
 import io.bitsquare.trade.protocol.trade.TradeMessage;
 import io.bitsquare.trade.protocol.trade.offerer.BuyerAcceptsOfferProtocol;
 import io.bitsquare.trade.protocol.trade.offerer.BuyerAcceptsOfferProtocolListener;
@@ -83,7 +83,7 @@ public class TradeManager {
     private final BlockChainService blockChainService;
     private final WalletService walletService;
     private final SignatureService signatureService;
-    private final OfferRepository offerRepository;
+    private final RemoteOfferBook remoteOfferBook;
 
     //TODO store TakerAsSellerProtocol in trade
     private final Map<String, SellerTakesOfferProtocol> takerAsSellerProtocolMap = new HashMap<>();
@@ -106,7 +106,7 @@ public class TradeManager {
     public TradeManager(User user, AccountSettings accountSettings, Persistence persistence,
                         MessageService messageService, BlockChainService blockChainService,
                         WalletService walletService, SignatureService signatureService,
-                        OfferRepository offerRepository) {
+                        RemoteOfferBook remoteOfferBook) {
         this.user = user;
         this.accountSettings = accountSettings;
         this.persistence = persistence;
@@ -114,7 +114,7 @@ public class TradeManager {
         this.blockChainService = blockChainService;
         this.walletService = walletService;
         this.signatureService = signatureService;
-        this.offerRepository = offerRepository;
+        this.remoteOfferBook = remoteOfferBook;
 
         Object offersObject = persistence.read(this, "offers");
         if (offersObject instanceof Map) {
@@ -172,7 +172,7 @@ public class TradeManager {
                 accountSettings.getAcceptedCountries(),
                 accountSettings.getAcceptedLanguageLocales());
 
-        CreateOfferCoordinator createOfferCoordinator = new CreateOfferCoordinator(
+        CreateOfferProtocol createOfferCoordinator = new CreateOfferProtocol(
                 offer,
                 walletService,
                 (transactionId) -> {
@@ -186,9 +186,9 @@ public class TradeManager {
                     }
                 },
                 (message, throwable) -> errorMessageHandler.handleErrorMessage(message),
-                offerRepository);
+                remoteOfferBook);
 
-        createOfferCoordinator.start();
+        createOfferCoordinator.createOffer();
     }
 
     private void addOffer(Offer offer) {
@@ -206,7 +206,7 @@ public class TradeManager {
         offers.remove(offer.getId());
         persistOffers();
 
-        offerRepository.removeOffer(offer);
+        remoteOfferBook.removeOffer(offer);
     }
 
 
diff --git a/gui/src/main/java/io/bitsquare/trade/TradeMessage.java b/gui/src/main/java/io/bitsquare/trade/TradeMessage.java
new file mode 100644
index 0000000000..bc2e7ff492
--- /dev/null
+++ b/gui/src/main/java/io/bitsquare/trade/TradeMessage.java
@@ -0,0 +1,29 @@
+/*
+ * This file is part of Bitsquare.
+ *
+ * Bitsquare is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bitsquare is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.bitsquare.trade;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class TradeMessage {
+    private static final Logger log = LoggerFactory.getLogger(TradeMessage.class);
+
+    public TradeMessage() {
+
+    }
+}
diff --git a/gui/src/main/java/io/bitsquare/trade/TradeState.java b/gui/src/main/java/io/bitsquare/trade/TradeState.java
new file mode 100644
index 0000000000..d3da1acc06
--- /dev/null
+++ b/gui/src/main/java/io/bitsquare/trade/TradeState.java
@@ -0,0 +1,29 @@
+/*
+ * This file is part of Bitsquare.
+ *
+ * Bitsquare is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bitsquare is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.bitsquare.trade;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class TradeState {
+    private static final Logger log = LoggerFactory.getLogger(TradeState.class);
+
+    public TradeState() {
+
+    }
+}
diff --git a/gui/src/main/java/io/bitsquare/trade/protocol/createoffer/CreateOfferCoordinator.java b/gui/src/main/java/io/bitsquare/trade/protocol/createoffer/CreateOfferCoordinator.java
deleted file mode 100644
index 9d5f4912b8..0000000000
--- a/gui/src/main/java/io/bitsquare/trade/protocol/createoffer/CreateOfferCoordinator.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * This file is part of Bitsquare.
- *
- * Bitsquare is free software: you can redistribute it and/or modify it
- * under the terms of the GNU Affero General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or (at
- * your option) any later version.
- *
- * Bitsquare is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
- * License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
- */
-
-package io.bitsquare.trade.protocol.createoffer;
-
-import io.bitsquare.btc.WalletService;
-import io.bitsquare.offer.Offer;
-import io.bitsquare.offer.OfferRepository;
-import io.bitsquare.trade.handlers.TransactionResultHandler;
-import io.bitsquare.util.task.FaultHandler;
-
-import org.bitcoinj.core.InsufficientMoneyException;
-import org.bitcoinj.core.Transaction;
-
-import com.google.common.util.concurrent.FutureCallback;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Responsible for coordinating tasks involved in the create offer process.
- */
-public class CreateOfferCoordinator {
-
-    private static final Logger log = LoggerFactory.getLogger(CreateOfferCoordinator.class);
-
-    private final Offer offer;
-    private final WalletService walletService;
-    private final TransactionResultHandler resultHandler;
-    private final FaultHandler faultHandler;
-    private final OfferRepository offerRepository;
-
-    public CreateOfferCoordinator(Offer offer, WalletService walletService, TransactionResultHandler resultHandler,
-                                  FaultHandler faultHandler, OfferRepository offerRepository) {
-        this.offer = offer;
-        this.walletService = walletService;
-        this.resultHandler = resultHandler;
-        this.faultHandler = faultHandler;
-        this.offerRepository = offerRepository;
-    }
-
-    public void start() {
-        try {
-            offer.validate();
-        } catch (Exception ex) {
-            faultHandler.handleFault("Offer validation failed", ex);
-            return;
-        }
-
-        Transaction transaction;
-
-        try {
-            transaction = walletService.createOfferFeeTx(offer.getId());
-            offer.setOfferFeePaymentTxID(transaction.getHashAsString());
-        } catch (InsufficientMoneyException ex) {
-            faultHandler.handleFault(
-                    "Offer fee payment failed because there is insufficient money in the trade wallet", ex);
-            return;
-        } catch (Throwable ex) {
-            faultHandler.handleFault("Offer fee payment failed because of an exception occurred", ex);
-            return;
-        }
-
-        try {
-            walletService.broadcastCreateOfferFeeTx(transaction, new FutureCallback<Transaction>() {
-                @Override
-                public void onSuccess(Transaction transaction) {
-                    log.info("sendResult onSuccess:" + transaction);
-                    if (transaction == null) {
-                        faultHandler.handleFault("Offer fee payment failed.",
-                                new Exception("Offer fee payment failed. Transaction = null."));
-                        return;
-                    }
-                    offerRepository.addOffer(offer, () -> resultHandler.onResult(transaction), faultHandler);
-                }
-
-                @Override
-                public void onFailure(Throwable t) {
-                    faultHandler.handleFault("Offer fee payment failed with an exception.", t);
-                }
-            });
-        } catch (Throwable t) {
-            faultHandler.handleFault("Offer fee payment failed because an exception occurred.", t);
-            return;
-        }
-    }
-}
diff --git a/gui/src/main/java/io/bitsquare/trade/protocol/createoffer/CreateOfferProtocol.java b/gui/src/main/java/io/bitsquare/trade/protocol/createoffer/CreateOfferProtocol.java
new file mode 100644
index 0000000000..3145def0d8
--- /dev/null
+++ b/gui/src/main/java/io/bitsquare/trade/protocol/createoffer/CreateOfferProtocol.java
@@ -0,0 +1,146 @@
+/*
+ * This file is part of Bitsquare.
+ *
+ * Bitsquare is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bitsquare is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.bitsquare.trade.protocol.createoffer;
+
+import io.bitsquare.btc.WalletService;
+import io.bitsquare.offer.Offer;
+import io.bitsquare.offer.RemoteOfferBook;
+import io.bitsquare.trade.handlers.TransactionResultHandler;
+import io.bitsquare.util.task.FaultHandler;
+
+import org.bitcoinj.core.InsufficientMoneyException;
+import org.bitcoinj.core.Transaction;
+
+import com.google.common.util.concurrent.FutureCallback;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Responsible for coordinating tasks involved in the create offer process.
+ * Executed on UI thread (single threaded)
+ */
+public class CreateOfferProtocol {
+
+    private static final Logger log = LoggerFactory.getLogger(CreateOfferProtocol.class);
+
+    private final Offer offer;
+    private final WalletService walletService;
+    private final TransactionResultHandler resultHandler;
+    private final FaultHandler faultHandler;
+    private final RemoteOfferBook remoteOfferBook;
+    private int repeatAddOfferCallCounter = 0;
+
+    public CreateOfferProtocol(Offer offer, WalletService walletService, TransactionResultHandler resultHandler,
+                               FaultHandler faultHandler, RemoteOfferBook remoteOfferBook) {
+        this.offer = offer;
+        this.walletService = walletService;
+        this.resultHandler = resultHandler;
+        this.faultHandler = faultHandler;
+        this.remoteOfferBook = remoteOfferBook;
+    }
+
+    public void createOffer() {
+        try {
+            validateOffer();
+            Transaction transaction = createOfferFeeTx();
+
+            TransactionResultHandler resultHandler1 = transaction1 -> addOffer(transaction1);
+            FaultHandler faultHandler1 = (message, throwable) -> faultHandler.handleFault(message, throwable);
+            broadcastCreateOfferFeeTx(transaction, resultHandler1, faultHandler1);
+
+        } catch (Throwable t) {
+        }
+
+    }
+
+    // 1. Validate offer data
+    // Sync
+    // In case of an error: No rollback activity needed
+    void validateOffer() throws Exception {
+        try {
+            offer.validate();
+        } catch (Exception ex) {
+            faultHandler.handleFault("Offer validation failed", ex);
+            throw ex;
+        }
+    }
+
+    // 2. createOfferFeeTx
+    // Sync
+    // In case of an error: No rollback activity needed
+    Transaction createOfferFeeTx() throws Exception {
+        try {
+            return walletService.createOfferFeeTx(offer.getId());
+        } catch (InsufficientMoneyException ex) {
+            faultHandler.handleFault(
+                    "Offer fee payment failed because there is insufficient money in the trade wallet", ex);
+            throw ex;
+        } catch (Throwable t) {
+            faultHandler.handleFault("Offer fee payment failed because of an exception occurred", t);
+            throw t;
+        }
+    }
+
+    // 3. broadcastCreateOfferFeeTx
+    // Async
+    // In case of an error: Not sure if there can be an inconsistent state in failure case. Assuming not but need to check further.
+    void broadcastCreateOfferFeeTx(Transaction transaction, TransactionResultHandler resultHandler1, FaultHandler faultHandler1) throws Exception {
+        try {
+            walletService.broadcastCreateOfferFeeTx(transaction, new FutureCallback<Transaction>() {
+                @Override
+                public void onSuccess(Transaction transaction) {
+                    log.info("Broadcast of offer fee payment succeeded: transaction = " + transaction.toString());
+                    if (transaction == null) {
+                        Exception ex = new Exception("Broadcast of offer fee payment failed because transaction = null.");
+                        faultHandler.handleFault("Broadcast of offer fee payment failed.", ex);
+                    }
+                    resultHandler1.onResult(transaction);
+                }
+
+                @Override
+                public void onFailure(Throwable t) {
+                    faultHandler1.handleFault("Broadcast of offer fee payment failed with an exception.", t);
+                }
+            });
+        } catch (Throwable t) {
+            faultHandler1.handleFault("Broadcast of offer fee payment failed with an exception.", t);
+            throw t;
+        }
+    }
+
+    // 4. addOffer
+    // Async
+    // In case of an error: Try again, afterwards give up.
+    void addOffer(Transaction transaction) {
+        remoteOfferBook.addOffer(offer,
+                () -> {
+                    offer.setOfferFeePaymentTxID(transaction.getHashAsString());
+                    resultHandler.onResult(transaction);
+                },
+                (message, throwable) -> {
+                    repeatAddOfferCallCounter++;
+                    if (repeatAddOfferCallCounter > 1) {
+                        faultHandler.handleFault(message, throwable);
+                    }
+                    else {
+                        addOffer(transaction);
+                    }
+                });
+    }
+}
diff --git a/gui/src/main/java/io/bitsquare/util/Bytes.java b/gui/src/main/java/io/bitsquare/util/Bytes.java
new file mode 100644
index 0000000000..fa6d9edbf1
--- /dev/null
+++ b/gui/src/main/java/io/bitsquare/util/Bytes.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 <http://www.gnu.org/licenses/>.
+ */
+
+package io.bitsquare.util;
+
+import org.bitcoinj.core.Utils;
+
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+
+import java.io.IOException;
+
+import java.util.Arrays;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class Bytes {
+    private static final Logger log = LoggerFactory.getLogger(Bytes.class);
+
+    public final byte[] bytes;
+    public final String string;
+
+    public Bytes(byte[] bytes) {
+        this.bytes = Arrays.copyOf(bytes, bytes.length);
+        this.string = Utils.HEX.encode(bytes);
+    }
+
+    public Bytes(String string) {
+        this.string = string;
+        this.bytes = Utils.HEX.decode(string);
+    }
+
+    @Override
+    public String toString() {
+        return string;
+    }
+
+    public static class GsonAdapter extends TypeAdapter<Bytes> {
+        @Override
+        public Bytes read(JsonReader reader) throws IOException {
+            return new Bytes(reader.nextString());
+        }
+
+        @Override
+        public void write(JsonWriter out, Bytes value) throws IOException {
+            if (value == null)
+                out.nullValue();
+            else
+                out.value(value.string);
+        }
+    }
+}
diff --git a/gui/src/main/java/io/bitsquare/util/Utilities.java b/gui/src/main/java/io/bitsquare/util/Utilities.java
index 8c3ddfd277..b784e2d642 100644
--- a/gui/src/main/java/io/bitsquare/util/Utilities.java
+++ b/gui/src/main/java/io/bitsquare/util/Utilities.java
@@ -25,6 +25,7 @@ import java.awt.*;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
+import java.io.File;
 import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
@@ -177,6 +178,30 @@ public class Utilities {
         return obj;
     }
 
+    /**
+     * Empty and delete a folder (and subfolders).
+     * @param folder
+     *            folder to empty
+     */
+    public static void removeDirectory(final File folder) {
+        // check if folder file is a real folder
+        if (folder.isDirectory()) {
+            File[] list = folder.listFiles();
+            if (list != null) {
+                for (int i = 0; i < list.length; i++) {
+                    File tmpF = list[i];
+                    if (tmpF.isDirectory()) {
+                        removeDirectory(tmpF);
+                    }
+                    tmpF.delete();
+                }
+            }
+            if (!folder.delete()) {
+                log.warn("can't delete folder : " + folder);
+            }
+        }
+    }
+
     public static AnimationTimer setTimeout(int delay, Function<AnimationTimer, Void> callback) {
         AnimationTimer animationTimer = new AnimationTimer() {
             final long lastTimeStamp = System.currentTimeMillis();
diff --git a/gui/src/main/resources/logback.xml b/gui/src/main/resources/logback.xml
index bf5c415ea0..1d2e3cd137 100644
--- a/gui/src/main/resources/logback.xml
+++ b/gui/src/main/resources/logback.xml
@@ -26,9 +26,9 @@
 
     <logger name="io.bitsquare" level="TRACE"/>
 
-    <logger name="org.bitcoinj" level="INFO"/>
-    <logger name="net.tomp2p" level="INFO"/>
-    <logger name="com.vinumeris.updatefx" level="TRACE"/>
+    <logger name="org.bitcoinj" level="WARN"/>
+    <logger name="net.tomp2p" level="WARN"/>
+    <logger name="com.vinumeris.updatefx" level="INFO"/>
 
 
     <logger name="net.tomp2p.message.Encoder" level="WARN"/>
diff --git a/gui/src/test/java/io/bitsquare/msg/TomP2PTests.java b/gui/src/test/java/io/bitsquare/msg/TomP2PTests.java
index f138fc73ea..2021dea704 100644
--- a/gui/src/test/java/io/bitsquare/msg/TomP2PTests.java
+++ b/gui/src/test/java/io/bitsquare/msg/TomP2PTests.java
@@ -74,7 +74,7 @@ import static org.junit.Assert.*;
  * Test bootstrapping, DHT operations like put/get/add/remove and sendDirect in both LAN and WAN environment
  * Test scenarios in direct connection, auto port forwarding or relay mode.
  * <p>
- * To start a bootstrap node code use the {@link io.bitsquare.app.cli.BootstrapNode} class.
+ * To start a bootstrap node code use the {@link io.bitsquare.app.bootstrap.BootstrapNode} class.
  * <p>
  * To configure your test environment edit the static fields for id, IP and port.
  * In the configure method and the connectionType you can define your test scenario.
diff --git a/gui/src/test/java/io/bitsquare/trade/protocol/createoffer/CreateOfferProtocolTest.java b/gui/src/test/java/io/bitsquare/trade/protocol/createoffer/CreateOfferProtocolTest.java
new file mode 100644
index 0000000000..6fb09fb6e6
--- /dev/null
+++ b/gui/src/test/java/io/bitsquare/trade/protocol/createoffer/CreateOfferProtocolTest.java
@@ -0,0 +1,318 @@
+/*
+ * This file is part of Bitsquare.
+ *
+ * Bitsquare is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bitsquare is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package io.bitsquare.trade.protocol.createoffer;
+
+import io.bitsquare.arbitrator.Arbitrator;
+import io.bitsquare.bank.BankAccountType;
+import io.bitsquare.btc.BitcoinNetwork;
+import io.bitsquare.btc.FeePolicy;
+import io.bitsquare.btc.UserAgent;
+import io.bitsquare.btc.WalletService;
+import io.bitsquare.locale.CountryUtil;
+import io.bitsquare.locale.LanguageUtil;
+import io.bitsquare.msg.tomp2p.BootstrappedPeerBuilder;
+import io.bitsquare.msg.tomp2p.TomP2PMessageService;
+import io.bitsquare.msg.tomp2p.TomP2PNode;
+import io.bitsquare.network.BootstrapState;
+import io.bitsquare.network.Node;
+import io.bitsquare.offer.Direction;
+import io.bitsquare.offer.Offer;
+import io.bitsquare.offer.RemoteOfferBook;
+import io.bitsquare.offer.tomp2p.TomP2POfferBook;
+import io.bitsquare.persistence.Persistence;
+import io.bitsquare.trade.handlers.TransactionResultHandler;
+import io.bitsquare.user.User;
+import io.bitsquare.util.DSAKeyUtil;
+import io.bitsquare.util.task.FaultHandler;
+
+import org.bitcoinj.core.Address;
+import org.bitcoinj.core.Coin;
+import org.bitcoinj.core.Transaction;
+import org.bitcoinj.utils.Threading;
+
+import java.io.File;
+import java.io.IOException;
+
+import java.util.Arrays;
+import java.util.Currency;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import rx.Observable;
+
+import static org.junit.Assert.*;
+
+/**
+ * That test is ignored for automated testing as it needs custom setup.
+ * <p/>
+ * It uses RegTest mode of Bitcoin network and localhost TomP2P network.
+ * <p/>
+ * 1. Need a first run to get the wallet receiving address.
+ * 2. Fund that from regtest Bitcoin Core client.
+ * 3. Create a block on regtest Bitcoin Core (setgenerate true) to get the balance.
+ * 4. Start BootstrapNodeMain at localhost with program args: --node.name localhost
+ */
+@Ignore
+public class CreateOfferProtocolTest {
+    private static final Logger log = LoggerFactory.getLogger(CreateOfferProtocolTest.class);
+
+    private WalletService walletService;
+    private TomP2PMessageService messageService;
+    private RemoteOfferBook remoteOfferBook;
+    private final File dir = new File("./temp");
+    private final static String OFFER_ID = "offerID";
+    private Address address;
+
+    @Before
+    public void setup() throws InterruptedException {
+        CountDownLatch countDownLatch = new CountDownLatch(1);
+        dir.mkdirs();
+
+        Persistence persistence = new Persistence(dir, "prefs");
+        persistence.init();
+
+        // messageService
+        Node bootstrapNode = Node.at("localhost", "127.0.0.1");
+        User user = new User();
+        user.applyPersistedUser(null);
+        BootstrappedPeerBuilder bootstrappedPeerBuilder = new BootstrappedPeerBuilder(Node.DEFAULT_PORT, false, bootstrapNode, "<unspecified>");
+        TomP2PNode p2pNode = new TomP2PNode(bootstrappedPeerBuilder);
+        messageService = new TomP2PMessageService(user, p2pNode);
+
+        Observable<BootstrapState> messageObservable = messageService.init();
+        messageObservable.publish();
+        messageObservable.subscribe(
+                state -> log.trace("state changed: " + state),
+                error -> {
+                    log.error(error.toString());
+                },
+                () -> {
+                    log.trace("message completed");
+
+                    remoteOfferBook = new TomP2POfferBook(p2pNode);
+                    remoteOfferBook.setExecutor(Threading.SAME_THREAD);
+                }
+        );
+        bootstrappedPeerBuilder.start();
+
+        // WalletService
+        walletService = new WalletService(BitcoinNetwork.REGTEST,
+                new FeePolicy(BitcoinNetwork.REGTEST),
+                null,
+                persistence,
+                new UserAgent("", ""),
+                dir,
+                "Tests"
+        );
+
+        Observable<Object> walletServiceObservable = walletService.initialize(Threading.SAME_THREAD);
+        walletServiceObservable.subscribe(
+                next -> {
+                    // log.trace("wallet next");
+                },
+                error -> {
+                    log.trace("wallet error");
+                },
+                () -> {
+                    log.trace("wallet complete");
+                });
+
+        Observable<?> allTasks = Observable.merge(messageObservable, walletServiceObservable);
+        allTasks.subscribe(
+                next -> {
+                    //log.trace("next");
+                },
+                error -> log.error(error.toString()),
+                () -> {
+                    log.trace("wallet completed");
+                    // 1. Use that address for funding the trading wallet
+                    address = walletService.getAddressInfoByTradeID(OFFER_ID).getAddress();
+                    log.info("address for funding wallet = " + address.toString());//muoTvFHJmQwPKYoA8Fr7t87UCSfZM4fciG
+                    log.info("Balance = " + walletService.getBalanceForAddress(address));
+                    countDownLatch.countDown();
+                });
+
+        countDownLatch.await();
+    }
+
+    @After
+    public void shutDown() throws IOException, InterruptedException {
+        walletService.shutDown();
+        messageService.shutDown();
+    }
+
+    @Test
+    public void validateOfferTest() throws InterruptedException {
+        try {
+            Offer offer = getOffer();
+            getCreateOfferCoordinator(offer).validateOffer();
+            assertTrue(true);
+        } catch (Exception e) {
+            e.printStackTrace();
+            fail(e.getMessage());
+        }
+    }
+
+    @Test
+    public void createOfferFeeTxTest() throws InterruptedException {
+        try {
+            Offer offer = getOffer();
+            Transaction transaction = getCreateOfferCoordinator(offer).createOfferFeeTx();
+            assertNotNull(transaction);
+        } catch (Exception e) {
+            log.info("address for funding wallet = " + address.toString());
+            log.info("Balance = " + walletService.getBalanceForAddress(address));
+            e.printStackTrace();
+            fail(e.getMessage());
+        }
+    }
+
+    @Test
+    public void broadcastCreateOfferFeeTxTest() throws InterruptedException {
+        try {
+            log.info("Balance pre = " + walletService.getBalanceForAddress(address));
+            Offer offer = getOffer();
+            TransactionResultHandler resultHandler = transaction -> assertNotNull(transaction);
+            FaultHandler faultHandler = (message, throwable) -> {
+                log.error(message);
+                throwable.printStackTrace();
+                fail(throwable.getMessage());
+            };
+            CreateOfferProtocol createOfferCoordinator = getCreateOfferCoordinator(offer);
+            Transaction transaction = createOfferCoordinator.createOfferFeeTx();
+            createOfferCoordinator.broadcastCreateOfferFeeTx(transaction, resultHandler, faultHandler);
+            log.info("Balance post = " + walletService.getBalanceForAddress(address));
+
+        } catch (Exception e) {
+            log.info("address for funding wallet = " + address.toString());
+            log.info("Balance = " + walletService.getBalanceForAddress(address));
+            e.printStackTrace();
+            fail(e.getMessage());
+        }
+    }
+
+    @Test
+    public void addOfferTest() throws InterruptedException {
+        CountDownLatch countDownLatch = new CountDownLatch(2);
+        try {
+            Offer offer = getOffer();
+            remoteOfferBook.addListener(new RemoteOfferBook.Listener() {
+                @Override
+                public void onOfferAdded(Offer offer1) {
+                    assertEquals("Offer matching", offer.getId(), offer1.getId());
+                    countDownLatch.countDown();
+                }
+
+                @Override
+                public void onOffersReceived(List<Offer> offers) {
+                }
+
+                @Override
+                public void onOfferRemoved(Offer offer) {
+                }
+            });
+
+            TransactionResultHandler resultHandler = transaction -> {
+                assertNotNull(transaction);
+                countDownLatch.countDown();
+            };
+            FaultHandler faultHandler = (message, throwable) -> {
+                log.error(message);
+                throwable.printStackTrace();
+                fail(throwable.getMessage());
+                countDownLatch.countDown();
+                countDownLatch.countDown();
+            };
+            CreateOfferProtocol createOfferCoordinator = getCreateOfferCoordinator(offer, resultHandler, faultHandler);
+            Transaction transaction = createOfferCoordinator.createOfferFeeTx();
+            createOfferCoordinator.addOffer(transaction);
+            countDownLatch.await();
+            log.info("Finished");
+        } catch (Exception e) {
+            e.printStackTrace();
+            fail(e.getMessage());
+            countDownLatch.countDown();
+            countDownLatch.countDown();
+        }
+    }
+
+    @Test
+    public void createOfferTest() throws InterruptedException {
+        CountDownLatch countDownLatch = new CountDownLatch(1);
+        TransactionResultHandler resultHandler = transaction -> {
+            assertNotNull(transaction);
+            countDownLatch.countDown();
+        };
+        FaultHandler faultHandler = (message, throwable) -> {
+            log.error(message);
+            throwable.printStackTrace();
+            fail(throwable.getMessage());
+            countDownLatch.countDown();
+            countDownLatch.countDown();
+        };
+        CreateOfferProtocol createOfferCoordinator = getCreateOfferCoordinator(getOffer(), resultHandler, faultHandler);
+        createOfferCoordinator.createOffer();
+        countDownLatch.await();
+    }
+
+    
+    private CreateOfferProtocol getCreateOfferCoordinator(Offer offer) throws InterruptedException {
+        TransactionResultHandler resultHandler = transaction -> log.debug("result transaction=" + transaction.toString());
+        FaultHandler faultHandler = (message, throwable) -> {
+            log.error(message);
+            throwable.printStackTrace();
+            log.info("Balance = " + walletService.getBalanceForAddress(walletService.getAddressInfoByTradeID(OFFER_ID).getAddress()));
+        };
+        return getCreateOfferCoordinator(offer, resultHandler, faultHandler);
+    }
+
+    private CreateOfferProtocol getCreateOfferCoordinator(Offer offer, TransactionResultHandler resultHandler, FaultHandler faultHandler) throws
+            InterruptedException {
+        return new CreateOfferProtocol(offer,
+                walletService,
+                resultHandler,
+                faultHandler,
+                remoteOfferBook);
+    }
+
+    private Offer getOffer() {
+        return new Offer(OFFER_ID,
+                DSAKeyUtil.generateKeyPair().getPublic(),
+                Direction.BUY,
+                100L,
+                Coin.CENT,
+                Coin.CENT,
+                BankAccountType.INTERNATIONAL,
+                Currency.getInstance("EUR"),
+                CountryUtil.getDefaultCountry(),
+                "bankAccountUID",
+                Arrays.asList(new Arbitrator()),
+                Coin.CENT,
+                Arrays.asList(CountryUtil.getDefaultCountry()),
+                Arrays.asList(LanguageUtil.getDefaultLanguageLocale())
+        );
+    }
+}
diff --git a/net/pom.xml b/net/pom.xml
index d4133a39e6..2da859bd74 100644
--- a/net/pom.xml
+++ b/net/pom.xml
@@ -41,7 +41,7 @@
                     <minimizeJar>false</minimizeJar>
                     <transformers>
                         <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
-                            <mainClass>io.bitsquare.app.cli.BootstrapNodeMain</mainClass>
+                            <mainClass>io.bitsquare.app.bootstrap.BootstrapNodeMain</mainClass>
                         </transformer>
                     </transformers>
                     <filters>